☰
板蓝根颗粒目标检测数据集:111图134框,VOC/YOLO双格式训练全流程
2026/10/1 3:08:09 网站建设 项目流程

简介:面向药品包装检测与目标检测算法练习场景,这份数据集收录了板蓝根颗粒袋装药品的实拍图像及对应标注,适合正在学习YOLO、Faster R-CNN等检测框架的开发者、学生以及需要快速验证药品识别模型的算法工程师。压缩包共包含335个文件,以111张jpg原图为核心,配合111个Pascal VOC格式xml标注文件与113个YOLO格式txt标注文件,整体仅3.71MB,几乎可以秒级下载,便于直接接入现有训练流程。标注内容覆盖999ganmaoling与banlangen两个类别,总框数134个,数据已通过labelImg人工标注,类别边界与目标框位置可直接用于模型训练;由于图片数量适中,既可作为目标检测入门练习集,也可作为小规模药品外观识别实验的验证集。目前已有121人学习下载,资源量小而精,对需要快速搭建检测基线或演示项目的人员尤其友好。

1. 板蓝根颗粒目标检测数据集:111 张图、134 个框,VOC 和 YOLO 双格式拿来就能训

药品颗粒袋检测这类目标检测需求很具体:产线上要识别板蓝根颗粒的袋装包装,药房盘点要把板蓝根和感冒灵这类外观接近的颗粒袋区分开。公开数据集里几乎找不到这种细分品类,所以这份板蓝根颗粒检测数据集的价值在于把特定场景的样本直接给你。它包含 111 张 jpg 图片、111 个 Pascal VOC 格式的 xml 标注和 111 个 YOLO 格式的 txt 标注,类别有 999ganmaoling 和 banlangen 两个,总框数 134 个,标注工具是 labelImg。适合从公开数据集过渡到自己项目数据的算法新手做毕业设计或小规模验证,也适合想快速跑通 YOLO 训练流程的工程师:解压、配置、训练,三步就能起来。

2. 先拆压包:VOC 与 YOLO 双格式的文件结构、同名规则和坐标换算

拿到压缩包第一件事不是急着配环境,而是把三个文件类型之间的对应关系理清楚。111 张照片对应 111 个 xml 和 111 个 txt,文件名是 banlangen_xyxr_编号 的规律,比如 banlangen_xyxr_3.jpg、banlangen_xyxr_45.jpg。这份数据集的标注用 labelImg 完成,labelImg 导出 VOC 后可以再转成 YOLO 格式,所以两类格式并存并不是冗余,而是为了适配不同训练框架和工具链。

2.1 三类文件之间到底怎么对应:同名规则与「没有分割 txt」的含义

压缩包里的文件结构很干净,没有嵌套一堆子目录,主体就是三种文件。图片是训练输入,xml 是 labelImg 直接导出的 Pascal VOC 标注,txt 是 YOLO 训练实际读取的标注。它们靠文件名关联:banlangen_xyxr_3.jpg 对应 banlangen_xyxr_3.xml 和 banlangen_xyxr_3.txt,同名不同后缀。这个规则是 YOLO 系框架的硬约定,改任何一边的名字都会导致训练时找不到标注。

文件类型数量格式说明训练中的作用
jpg 图片111RGB 图像模型输入
xml 标注111Pascal VOC,存绝对坐标与类别名可视化与格式转换
txt 标注111YOLO 格式,存归一化坐标与类别 ID训练时直接读取

注意摘要里那句「不包含分割路径的 txt 文件」:意思是压缩包里没有 train.txt、val.txt 这种列出图片路径的清单文件,只给了图片和标注本身。这有好处也有代价。好处是目录结构简单,YOLOv8 这类新框架不需要路径清单,靠目录结构就能划分数据集;代价是如果你要跑 darknet 或老版 YOLOv5 的某些分支,train.txt、val.txt 得自己生成,后面第 4 章会给生成命令。

2.2 同一张图的两套坐标:xml 的绝对坐标如何换算成 txt 的归一化坐标

VOC 格式和 YOLO 格式标注的是同一个目标,只是坐标系不同。VOC 存绝对像素坐标,YOLO 存相对原图宽高的归一化值。看一个典型的 xml 标注结构,由 labelImg 导出:

<annotation> <folder>banlangen_xyxr</folder> <filename>banlangen_xyxr_3.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>banlangen</name> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>300</xmax> <ymax>200</ymax> </bndbox> </object> </annotation>

一个 object 节点就是一个标注框,多个 object 就是多目标。name 存类别字符串,bndbox 存左上角和右下角的绝对坐标。xml 的 size 节点是关键,坐标换算时要用它做分母。把上面的框转成 YOLO 格式,就是下面这行:

0 0.328125 0.291667 0.281250 0.250000

五个数字分别是:类别 ID、中心点 x 归一化、中心点 y 归一化、框宽归一化、框高归一化。换算公式是:

# 假设原图宽高为 width, height cx = (xmin + xmax) / 2 / width # 中心点 x 归一化 cy = (ymin + ymax) / 2 / height # 中心点 y 归一化 w = (xmax - xmin) / width # 框宽归一化 h = (ymax - ymin) / height # 框高归一化

这套换算里最容易翻车的点:分母用的必须是原图分辨率,也就是 xml size 节点里的 width 和 height,不是训练时的 imgsz。很多人在这一步把分母写成 640 或 416,结果训练出来的框整体偏移,后面第 5 章会单独讲这个坑。txt 里的第一个数字是类别 ID,xml 里 name 是字符串,两者靠类别列表对应,这份数据集的类别列表只有两个:

对比项VOC xmlYOLO txt
坐标形式xmin, ymin, xmax, ymax 绝对像素cx, cy, w, h 归一化浮点
类别表示字符串 name整数类别 ID
文件读取方labelImg、darknet、各类转换脚本ultralytics 训练直接读取

提示:如果 xml 的 size 和实际图片分辨率不一致,txt 的归一化坐标就是错的,训练时框的位置会直接偏移。拿到数据先随机挑两张图,把 xml 里的框画在原图上,确认标注是贴合的再往下走。

3. 摸清两个类别的底细:134 个框怎么分布,直接决定训练策略

训练之前先回答一个问题:这 134 个框到底是怎么分布的?数据集的形态决定了你后面要用什么策略,盲目套标准流程容易浪费大量时间。这一章把类别含义和样本分布拆开讲,顺便给一个统计脚本,让你用自己的眼睛确认数据,而不是靠压缩包描述猜。

3.1 类别名的背后:999ganmaoling 与 banlangen 分别是什么目标

从命名习惯看,banlangen 就是板蓝根颗粒的袋装包装,999ganmaoling 是 999 感冒灵颗粒的拼音缩写,两者都是药品颗粒袋装,外观差异主要体现在包装配色和文字上。这份数据集的标注工具是 labelImg,它画出来的是外接矩形 bndbox,矩形内会带一部分背景或相邻物体,这是正常现象,不需要刻意清理干净,保留这些边界噪声反而能让模型在真实场景里更稳。

两个类别在语义上很接近,都是小尺寸的袋装目标,检测器的难点在于区分包装而不是找目标。如果你要把类别换成自己的业务名称,比如 banlangen_v3、999ganmaoling_new,只需要同步修改 xml 里的 name 字符串和 txt 里的类别 ID,但两边必须保持一致,否则可视化时会出现框和类别对不上的情况。我的习惯是先用脚本批量替换,再随机抽几张图做可视化检查,不手动改文件。

3.2 49 框 vs 85 框:样本不均衡在小数据集里有多致命

先看总量:111 张图、134 个框,平均每张 1.2 个目标。这意味着不少图里同时出现两袋颗粒,而不是每张图只有一个目标。如果你按每张图最多两个目标来推,至少有 23 张图是双目标的,实际存在三目标图的话,多目标图的数量只会更多。所以训练时要注意,模型不仅要学「框住目标」,还要学「一张图里有多个同类的响应」。

再看类别分布:999ganmaoling 框数 49,banlangen 框数 85,后者约是前者的 1.7 倍。小数据集里这种不均衡是致命的,因为模型会天然偏向多数类,导致 999ganmaoling 的召回率偏低。常见的处理办法有三个:一是对少数类做 Copy-Paste 增强,把 999ganmaoling 的框贴到空白背景上合成新样本;二是调整损失权重,让少数类的分类损失权重更高;三是先不追求均衡,跑一版看 Per-class 的 PR 曲线,确认少数类到底掉在哪再动手。我一般会先跑基线,用混淆矩阵说话,不凭感觉调参。

3.3 用脚本统计框数:确认类别分布而不是凭感觉

写一个简单的统计脚本,读 YOLO txt 就能拿到类别分布、每张图框数和空图数量。这段脚本在任何数据集上都能用,训练前跑一遍不亏。

from pathlib import Path from collections import Counter label_dir = Path("labels") # txt 所在目录 num_boxes = Counter() # 每张图的框数分布 class_counts = Counter() # 每个类别的框数 empty_files = 0 # 空标注文件数量 for txt in label_dir.glob("*.txt"): n = 0 for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) < 5: continue # 跳过格式不完整的行 class_counts[int(parts[0])] += 1 n += 1 num_boxes[n] += 1 if n == 0: empty_files += 1 print("类别 ID 与框数:", dict(class_counts)) print("每张图框数分布:", dict(sorted(num_boxes.items()))) print("空标注文件数:", empty_files)

统计结果直接告诉你三件事:类别 ID 到底有几个、是不是和 yaml 里的 names 对得上、空图多不多。如果空标注文件多,划分验证集时要特意保留一部分,让评估场景贴近真实拍照环境。这一跑通常用不了几秒钟,却能把后面几小时训练的方向定下来。

4. 把数据集跑成模型:目录组织、dataset.yaml 与 YOLOv8 训练参数

数据摸清了,这一章进入实操。以 YOLOv8 为例,因为它是现在跑自定义数据集最常用的框架之一,对目录结构的要求简单,而且这份数据集的 YOLO txt 可以直接被 ultralytics 读取。从目录整理开始,到配置文件、训练命令、结果判读,一条线走完。

4.1 目录规范:images 与 labels 分层,train/val 按 8:2 划分

ultralytics 的目录约定是 images 和 labels 同级,各自下面分 train、val 子目录,txt 与 jpg 严格同名。原始压缩包里图片和标注大概率是混在一起的,先建目录再按比例划分。下面这段脚本用 8:2 划分,并固定随机种子保证每次结果一致:

import random from pathlib import Path import shutil random.seed(42) # 固定种子,保证可复现 src_images = Path("jpg") # 原始图片目录 src_labels = Path("labels") # 原始 txt 目录 split = 0.8 # 训练集比例 images = sorted(src_images.glob("*.jpg")) random.shuffle(images) for i, img in enumerate(images): role = "train" if i < int(len(images) * split) else "val" name = img.stem # 不含后缀的文件名 dst_img = Path("images") / role / img.name dst_lbl = Path("labels") / role / f"{name}.txt" shutil.copy2(img, dst_img) shutil.copy2(src_labels / f"{name}.txt", dst_lbl)

逻辑说明:脚本先按文件名排序再打乱,避免文件系统顺序带来的分组偏差;train 和 val 的图片在 src 里只复制不移动,原目录原样保留,方便后续重新划分。参数 split 就是训练集比例,想改成 9:1 直接换数字。如果你的验证集里某个类别一个框都没有,说明随机划分不够均匀,需要按类别分层:先统计哪些文件包含 999ganmaoling 框,再分别从两类里抽验证集。

如果你是 darknet 用户,还需要生成路径清单文件,用 find 命令即可:

find "$(pwd)/images/train" -name "*.jpg" > train.txt find "$(pwd)/images/val" -name "*.jpg" > val.txt

YOLOv8 不需要这两行,darknet 才需要。路径要写绝对路径,否则 darknet 在解析图片时容易因为相对路径报错。

4.2 dataset.yaml 与训练命令:小数据集上的参数怎么设才不白跑

目录整理好之后,在数据集根目录写一个 dataset.yaml。注意 names 的顺序必须和 txt 里的类别 ID 对应,第 3.3 节的统计脚本已经告诉你有几个 ID、各是几,直接用那个顺序写:

path: /path/to/banlangen_xyxr # 改成你的绝对路径 train: images/train val: images/val nc: 2 names: 0: 999ganmaoling 1: banlangen

训练命令用 ultralytics 的命令行即可,关键在小数据集参数怎么选:

yolo detect train data=banlangen.yaml model=yolov8n.pt \ epochs=200 imgsz=640 batch=8 device=0 patience=50 \ project=runs/banlangen name=exp1

参数设置的逻辑用表说明:

参数取值理由
modelyolov8n.pt111 张数据只能用 nano 级,大模型必过拟合
epochs200数据量小收敛慢,多跑几轮给早停留空间
imgsz640默认值;显存不够降到 480,对小目标影响不大
batch8小显存的常见值,有更大显卡可以加到 16
patience50小数据集 loss 波动大,太低会在还没收敛时停掉

数据量少的时候,增强策略要适度。mosaic 默认是开的,但在 111 张图上,过强的 mosaic 会把目标裁切得支离破碎,反而干扰学习。我一般会把 mosaic 降到 0.7 左右,或者干脆关闭,用 export 参数控制:

yolo detect train data=banlangen.yaml model=yolov8n.pt epochs=200 mosaic=0.7

另外 yolov8n.pt 第一次运行会自动下载预训练权重,离线环境需要提前准备权重文件放到项目目录下,训练命令里改成 model=/path/to/yolov8n.pt 就行。

4.3 训练完看什么:混淆矩阵、PR 曲线和 results.png 的读法

训练结束后去 runs/banlangen/exp1 目录下看结果,重点看三个文件。一是 weights/best.pt 和 last.pt,best 是按验证集指标挑出来的最优权重,部署时用 best。二是混淆矩阵图,直接看两个类别互相误检的情况,如果 999ganmaoling 被大量判成 banlangen,说明两类特征区分度不够。三是 PR_curve.png,看每个类别的 AP 曲线,曲线越靠近右上角越好。

results.png 里最值得关注的是 train loss 和 val loss 的分离趋势。val loss 快速上扬而 train loss 继续下降,就是过拟合的典型信号。111 张数据很容易过拟合,所以遇到这种情况优先想到的是换更小的模型或增强正则,而不是继续加 epochs。

还要提醒一句:小数据集上单次训练的 mAP 波动很大,一次 0.85 一次 0.78 都是正常现象,不代表模型能力有这么大差距。更可靠的做法是固定训练参数,跑 3 个不同随机种子,取 mAP 均值作为模型的真实水平。

5. 从翻车到避坑:111 张数据训练时最容易踩的五个坑与排查顺序

这一段是血泪经验整理。小数据集训练报错的表现五花八门,但根因高度集中在标注一致性、数据划分和早停这三个方向上。我把最常见的五种现象按「现象→原因→解决」写清楚,你照着排错能省下大把时间。

5.1 现象:训练一开始提示 no labels found,或者日志里出现 labels.cache 找不到标注

原因:YOLO 训练时会在数据目录下生成 labels.cache 缓存文件,如果 images/train 和 labels/train 里的文件名对不上,或者 labels 目录层级放错,缓存就找不到对应的 txt。压缩包里的图片和 txt 是平铺的,没整理成标准目录就直接训,最容易触发这个问题。

解决:第一步检查同名文件,用一行命令对比:

ls images/train | sed 's/.jpg//' | sort > a.txt ls labels/train | sed 's/.txt//' | sort > b.txt diff a.txt b.txt

有差异就说明命名或文件缺失。修正后删除数据集目录下残留的 labels.cache 文件再重新训练,因为旧缓存不会自动刷新。

5.2 现象:loss 正常下降,但验证集 mAP 几乎为 0

原因:这是最隐蔽的坑。data.yaml 里 names 的书写顺序和 txt 里类别 ID 不一致,比如 txt 里 0 是 banlangen,而 yaml 里 0 写成了 999ganmaoling。模型学到的类别 0 特征和 ground truth 的类别 0 是两种东西,导致分类完全错位,但 loss 看起来一切正常。

解决:用第 3.3 节的统计脚本读一遍 txt 的类别 ID,有多少个 ID、各是几,然后严格按这个顺序写 yaml 的 names。改动后重新训练。不要凭压缩包描述里的顺序猜,训练前跑 3 秒脚本比训练完发现白跑 2 小时划算得多。

5.3 现象:验证集图片只有二十几张,PR 曲线像锯齿,某个类别的曲线直接消失

原因:随机划分数据集时,某一类的框全部落进训练集,验证集里一个都没有。这样验证集指标缺失,模型对少数类的真实能力完全没评估到。

解决:划分时做分层处理,保证验证集里两个类别至少各有 5 个框。更稳妥的方式是 5 折交叉验证,每折单独训练并评估,把 5 次结果的 mAP 和混淆矩阵取平均,这是小数据集评估的标准做法。虽然要多跑 5 次训练,但比单次划分的随机结果可信得多。

5.4 现象:训练能跑通,但检测出来的框整体偏移,位置像是被缩放过的

原因:txt 里坐标的归一化分母用的是原图宽高,有人却误以为是在 imgsz=640 下归一化的,训练前对 txt 做了错误的缩放;或者 xml 的 size 和实际图片尺寸不一致,转 txt 时就标错了。

解决:用 PIL 读原图宽高,把 txt 的归一化坐标反算回绝对坐标:

from PIL import Image w, h = Image.open("banlangen_xyxr_3.jpg").size # txt 中 cx, cy, bw, bh 为归一化值 x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h)

再画到原图上对比,确认标注贴合后重新生成 txt。这步通常在拿到数据集时就要做,不要等到训练后才发现。我在第 6 章会给一个完整的自检脚本。

5.5 现象:训练在第 30 轮就停了,best.pt 的 mAP 只有 0.2

原因:patience 设得太小,比如默认 10 或 20,而小数据集验证集 loss 波动剧烈,连续 20 轮没有提升是很常见的事,早停机制过早触发。

解决:把 patience 提到 50 以上,或者先关闭早停跑一版看完整曲线。如果完整跑下来某个类别的 AP 还是低,问题不在早停,回到第 3.2 节的样本均衡和增强策略上,不要在原处反复调参。

5.6 一个通用的排查顺序:先数据、再划分、最后才动模型参数

把这五条串起来,就是我拿到任何小数据集都会走的固定顺序。第一步验证标注:txt 与 xml 是否一致、坐标是否贴合、类别 ID 是否在合理范围。第二步验证划分:train/val 里每个类别框数是否都大于 0。第三步再谈训练参数和增强策略。数据没确认干净之前,任何模型层面的调优都是白搭。

6. 训练前先做一次「标注体检」:自检脚本帮你省掉两小时翻车

以前我拿到数据集的第一反应是直接丢进训练,反正 YOLO 框架会自动建缓存。后来吃了一次大亏:有一个数据集的 txt 类别 ID 顺序和 yaml 反了,训练两小时,看到 mAP 接近 0 才回头查问题,才发现是标注读错位。从那以后,我拿到任何带标注的数据集,都会强制先跑一遍自检脚本再谈训练,这套数据集也不例外。

自检脚本只做三件事:校验 txt 类别 ID 范围、校验归一化坐标是否在 [0,1] 内、按原图尺寸把 txt 转回绝对坐标检查框是否越界。把这段跑完,前面第 5 章说的大部分坑都能提前暴露:

from pathlib import Path from PIL import Image label_ids = ["999ganmaoling", "banlangen"] # 必须与 yaml names 一致 error_count = 0 for img_path in sorted(Path("images/train").glob("*.jpg")): txt_path = Path("labels/train") / f"{img_path.stem}.txt" if not txt_path.exists(): print("缺少标注:", img_path) error_count += 1 continue w, h = Image.open(img_path).size for line in txt_path.read_text().strip().splitlines(): cid, cx, cy, bw, bh = map(float, line.split()) if int(cid) >= len(label_ids) or int(cid) < 0: print("类别 ID 超出范围:", img_path, cid) error_count += 1 if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= bw <= 1 and 0 <= bh <= 1): print("坐标越界:", img_path, line) error_count += 1 x2 = (cx + bw / 2) * w y2 = (cy + bh / 2) * h if x2 > w or y2 > h: # 右下角超出原图边界 print("框越界:", img_path, line) error_count += 1 print("检查完成,错误数:", error_count)

脚本逻辑很直接:遍历训练集图片,找到同名 txt,逐行解析五个字段。类别 ID 超出 names 数量说明 ID 错位;归一化坐标不在 [0,1] 说明标注转换时用了错误的分母;框右下角超出原图尺寸说明 txt 是从错误的 xml 转出来的。任何一个错误命中,都值得在训练前花五分钟修掉。

自从养成这个习惯,小数据集训练翻车的次数明显少了。这份板蓝根数据集压缩包拿回来,我建议你的第一步也做一遍这个检查,再跑第 4 章的命令。数据干净了,模型训练只是时间问题。希望这套流程能帮你在自己的检测任务上少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询