☰
药品泡罩板检测数据集:VOC+YOLO双格式标注与YOLOv8小样本训练实战
2026/9/30 1:06:33 网站建设 项目流程

简介:这是一份面向目标检测初学者与药品识别方向开发者的布洛芬检测数据集,可用于训练和验证单类别目标检测模型,适用于药品分拣、智能零售货架识别等场景。数据集共476张jpg图片,每张均配有对应的VOC格式xml标注与YOLO格式txt标注,标注类别为buluofen,共657个矩形框,采用labelImg工具完成画框标注,可直接用于YOLO系列或Pascal VOC流程的模型训练。压缩包内文件总数1430个,包含478个txt、476个xml与476个jpg,整体约19.62MB,体积轻量便于快速下载与本地部署。目前已有175人学习下载,适合作为小样本单类别检测的练手素材,帮助读者省去从零采集与标注的成本,快速搭建训练管线并验证模型效果。

1. 药品泡罩板检测为什么值得单独做一份数据集

药品布洛芬检测数据集,476 张 VOC 加 YOLO 双格式标注,这个体量放在通用目标检测里连零头都算不上,但放在药品视觉质检这个细分场景里,它恰好卡在一个很实用的位置上。布洛芬这类片剂药品在产线上最常见的包装形态是铝塑泡罩板,泡罩板上的药片缺失、破损、异物混入、铝箔封口不严,都是药厂灯检工位每天要面对的问题。传统做法靠人工盯着传送带看,一条线两班倒至少四个人,漏检率还压不下去。用目标检测做自动灯检,核心瓶颈从来不是模型结构,而是有没有一批标注干净、格式对得上、能直接喂给 YOLO 训练的小样本数据。

这份数据集解决的就是这个冷启动问题。476 张图覆盖了泡罩板在不同光照、不同角度、不同批次下的成像,VOC 格式给的是 XML 标注,YOLO 格式给的是归一化后的 txt 标注,两种格式并存意味着你既可以用现成的 YOLOv8 训练脚本直接跑,也可以拿 VOC 那套去接 MMDetection 或者做数据增强后再转换。适合谁用?做药品视觉质检的算法工程师、想验证小样本目标检测方案的学生、以及需要快速搭一个药片检测 demo 的产品团队。不适合谁?指望拿它直接上产线的人——476 张的规模只够做原型验证和迁移学习起点,真要落地还得按我后面讲的采集策略补数据。

2. 拆开这份 VOC+YOLO 数据集:目录结构、标注格式与类别定义

2.1 拿到压缩包先看什么:目录树与文件命名

解压之后不要急着写训练脚本,先把目录结构摸清楚。这类双格式数据集常见的组织方式有两种:一种是 VOC 和 YOLO 各占一个顶层目录,各自带 images 和 labels;另一种是共享一份 images,VOC 的 XML 和 YOLO 的 txt 分别放在 Annotations 和 labels 下。两种都能用,但转换脚本的路径写法完全不同,先确认再动手能省掉半小时的报错排查。

# 先看顶层结构,确认是分离式还是共享式 find ./ibuprofen_dataset -maxdepth 2 -type d | sort # 统计图片数量和标注数量是否对得上 find ./ibuprofen_dataset -name "*.jpg" -o -name "*.png" | wc -l find ./ibuprofen_dataset -name "*.xml" | wc -l find ./ibuprofen_dataset -name "*.txt" | wc -l

上面三条命令分别做三件事:第一条列出两层目录,判断组织方式;第二条统计图片总数,正常应该是 476;第三条分别统计 XML 和 txt 数量。如果 XML 数量和图片数量不一致,说明有图没标或者标注文件命名对不上,这种情况在二手数据集里很常见,必须先修掉再训练,否则 YOLO 训练时会静默跳过没有标注的图,你看到的 loss 曲线是正常的,但模型实际少学了那部分样本。

提示:文件名里的空格和中文是 YOLO 数据加载的隐形杀手。如果发现图片名带空格,先用rename 's/ /_/g' *.jpg批量替换,再往下走。

2.2 VOC XML 与 YOLO txt 的字段对照

VOC 的 XML 标注里,一张图对应一个文件,核心字段是filename、size里的宽高、以及每个object下的name和bndbox的 xmin/ymin/xmax/ymax。YOLO 的 txt 每行一个目标,格式是class_id cx cy w h,全部归一化到 0 到 1 之间。这两套格式的转换关系不复杂,但有两个地方容易翻车:一是 VOC 的坐标是左上角加右下角的绝对像素值,YOLO 要的是中心点加宽高再除以图像尺寸;二是类别名到 class_id 的映射必须全局一致,不能这张图里布洛芬是 0,那张图里变成 1。

字段VOC XMLYOLO txt转换注意
坐标原点左上角图像中心先算中心点再归一化
坐标单位绝对像素0-1 归一化除以 width 和 height
宽高表示xmax-xminw 直接给别把 xmax 当宽用
类别表示name 字符串class_id 整数建全局映射表
一图多目标多个 object 节点多行行序无所谓

这张表建议打印出来贴在显示器边上,写转换脚本的时候对着看。我见过太多人把xmax直接当成w塞进 YOLO 格式,训练出来的框全部偏到左上角,还以为是模型结构有问题。

2.3 类别定义与标注质量自查

药品泡罩板检测的类别定义通常不会太复杂,常见的是intact(完好药片)、missing(缺片)、broken(破损)、foreign(异物)这几类。但这份数据集具体定义了哪些类,必须自己打开 XML 看,不能猜。用下面这段脚本快速统计类别分布,顺便检查有没有拼写不一致的类别名。

import os import xml.etree.ElementTree as ET from collections import Counter xml_dir = "./ibuprofen_dataset/Annotations" counter = Counter() for fname in os.listdir(xml_dir): if not fname.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, fname)) root = tree.getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() counter[name] += 1 for cls, cnt in counter.most_common(): print(f"{cls}: {cnt}")

这段脚本遍历所有 XML,把每个 object 的 name 抽出来计数。跑完之后重点看两件事:一是类别总数是不是和你预期的一致,二是每个类别的样本量差距有多大。如果missing类只有几十个框,而intact有上千个,直接训练会导致模型严重偏向多数类,后面第 4 章会讲怎么处理这种不均衡。

3. 从 VOC 到 YOLO:转换脚本、数据划分与训练配置

3.1 写一个能复用的 VOC 转 YOLO 脚本

网上能找到的转换脚本一大把,但大部分只处理单目录、不检查边界、不生成类别映射文件,跑完还得手动补。下面这个版本把这三个坑都填了,直接改路径就能用。

import os import xml.etree.ElementTree as ET import shutil VOC_IMG_DIR = "./ibuprofen_dataset/JPEGImages" VOC_XML_DIR = "./ibuprofen_dataset/Annotations" OUT_IMG_DIR = "./yolo_dataset/images" OUT_LBL_DIR = "./yolo_dataset/labels" os.makedirs(OUT_IMG_DIR, exist_ok=True) os.makedirs(OUT_LBL_DIR, exist_ok=True) # 先扫一遍所有类别,建立稳定的映射 classes = set() for f in os.listdir(VOC_XML_DIR): if f.endswith(".xml"): root = ET.parse(os.path.join(VOC_XML_DIR, f)).getroot() for obj in root.findall("object"): classes.add(obj.find("name").text.strip()) classes = sorted(classes) cls_map = {c: i for i, c in enumerate(classes)} print("类别映射:", cls_map) for f in os.listdir(VOC_XML_DIR): if not f.endswith(".xml"): continue root = ET.parse(os.path.join(VOC_XML_DIR, f)).getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 边界裁剪,防止标注越界导致归一化后超出 0-1 xmin = max(0, min(xmin, w)) xmax = max(0, min(xmax, w)) ymin = max(0, min(ymin, h)) ymax = max(0, min(ymax, h)) cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") stem = os.path.splitext(f)[0] with open(os.path.join(OUT_LBL_DIR, stem + ".txt"), "w") as fp: fp.write("\n".join(lines)) # 拷贝对应图片 for ext in (".jpg", ".png", ".jpeg"): src = os.path.join(VOC_IMG_DIR, stem + ext) if os.path.exists(src): shutil.copy(src, os.path.join(OUT_IMG_DIR, stem + ext)) break

脚本的逻辑分四步:先扫全部 XML 建立类别到 id 的稳定映射,保证所有图用同一套编号;再逐图解析尺寸和每个目标框;然后做边界裁剪,把越界的坐标拉回图像范围内;最后归一化写入 txt 并拷贝图片。参数上唯一需要改的是顶部四个路径,classes排序是为了让映射可复现,如果你有指定的类别顺序,把sorted(classes)换成固定列表即可。

3.2 训练集验证集划分:476 张怎么分才不浪费

476 张图,按 8:2 分是 380 训练加 96 验证。这个量级下我一般不会单独切测试集,因为切完之后测试集只有几十张,指标波动大到没有参考意义。更稳的做法是训练集和验证集按 8:2 分,验证集同时承担调参和最终评估的角色,等模型定型后再用产线新采的数据做一次独立验证。

import os import random import shutil random.seed(42) img_dir = "./yolo_dataset/images" lbl_dir = "./yolo_dataset/labels" train_img = "./yolo_dataset/images/train" val_img = "./yolo_dataset/images/val" train_lbl = "./yolo_dataset/labels/train" val_lbl = "./yolo_dataset/labels/val" for d in (train_img, val_img, train_lbl, val_lbl): os.makedirs(d, exist_ok=True) files = [f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png", ".jpeg"))] random.shuffle(files) split = int(len(files) * 0.8) for i, f in enumerate(files): stem = os.path.splitext(f)[0] if i < split: shutil.move(os.path.join(img_dir, f), os.path.join(train_img, f)) shutil.move(os.path.join(lbl_dir, stem + ".txt"), os.path.join(train_lbl, stem + ".txt")) else: shutil.move(os.path.join(img_dir, f), os.path.join(val_img, f)) shutil.move(os.path.join(lbl_dir, stem + ".txt"), os.path.join(val_lbl, stem + ".txt"))

random.seed(42)是为了让划分可复现,团队协作时每个人跑出来的划分一致,对比实验才有意义。划分比例 0.8 可以调,但 476 张的规模下不建议低于 0.75,否则训练样本太少,模型连基本特征都学不稳。

3.3 YOLOv8 训练配置:小样本下的参数取舍

数据准备好之后,写 data.yaml 指向训练和验证目录,然后启动训练。小样本场景下有几个参数和默认值不一样,需要手动调。

# data.yaml path: ./yolo_dataset train: images/train val: images/val nc: 4 names: ["intact", "missing", "broken", "foreign"]
yolo detect train \ data=./data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.001 \ patience=30 \ augment=True \ mosaic=0.5 \ degrees=10.0 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4

参数逐个说:model=yolov8n.pt选 nano 版本是因为 476 张图撑不起大模型,n 版在 CPU 上也能跑推理,适合先验证流程;epochs=150比默认的 100 多,小样本需要更多轮次收敛;patience=30是早停耐心值,验证指标 30 轮不涨就停,防止过拟合;mosaic=0.5把默认的 1.0 降下来,因为药品泡罩板图像里目标排列有规律,过度拼接会破坏空间关系;degrees=10.0限制旋转角度,泡罩板在产线上不会大角度翻转,增强过头反而引入噪声。hsv_h/s/v三组是色调、饱和度、明度扰动,药品铝箔反光强,适度扰动能提升光照鲁棒性。

注意:如果训练时看到mosaic增强后的图里药片被裁掉一半,把mosaic降到 0.3 或者关掉,小目标在拼接边缘容易被切碎。

4. 小样本药品检测的避坑与排查清单

4.1 类别不均衡导致模型只认完好药片

现象:训练完看混淆矩阵,missing和broken的召回率不到 0.3,模型几乎把所有框都预测成intact。原因:完好药片在每张图里数量最多,缺片和破损是少数样本,损失函数被多数类主导。解决:在 data.yaml 同级加一个cls_weights配置,或者用 YOLOv8 的fraction参数配合过采样,把少数类图片复制多份参与训练。更直接的办法是在 loss 里给少数类加权,YOLOv8 支持通过class_weights传入。

4.2 标注框越界导致训练 loss 突然变 NaN

现象:训练到第 20 轮左右 loss 突然变成 NaN,重启后从断点继续还是 NaN。原因:部分 XML 里的 bndbox 坐标超出了图像宽高,转换时没裁剪,归一化后出现大于 1 或小于 0 的值,YOLO 在计算 CIoU 时对非法框没有保护。解决:回到 3.1 的转换脚本,确认边界裁剪那段逻辑生效,转换完用下面这行快速检查。

awk '{if ($2<0 || $2>1 || $3<0 || $3>1 || $4<0 || $4>1 || $5<0 || $5>1) print FILENAME": "$0}' ./yolo_dataset/labels/train/*.txt

4.3 图片和标注文件名对不上导致静默丢样本

现象:训练日志里train: Scanning...显示的图片数比实际少,比如 476 张只扫到 430 张。原因:YOLO 按图片名去找同名 txt,找不到就跳过,不报错。常见于图片是.JPG大写扩展名而标注是.txt小写,或者文件名里有不可见字符。解决:用diff <(ls images/train | sed 's/\.[^.]*$//' | sort) <(ls labels/train | sed 's/\.txt$//' | sort)对比两边文件名,差异项就是问题所在。

4.4 验证集指标虚高但实际推理漏检

现象:验证集 mAP 跑到 0.85,拿产线新拍的图一测,漏检一半。原因:476 张图如果来自同一批次、同一光照条件,训练集和验证集分布几乎一样,模型学到的是这批数据的特定特征,不是药品缺陷的通用特征。解决:划分验证集时按批次或按光照条件分层抽样,确保验证集里有训练集没见过的成像条件。如果数据本身批次单一,那验证集指标只能当参考,不能当落地依据。

4.5 推理时置信度阈值设太高把缺片漏掉

现象:模型训练指标正常,部署时conf=0.5过滤后缺片全没了。原因:缺片这类缺陷的预测置信度天然比完好药片低,因为缺陷区域的特征不如完好药片规整。解决:分类别设阈值,完好药片可以用 0.5,缺片和破损降到 0.25 到 0.3,用 YOLO 推理时的classes参数配合后处理逻辑分开处理。

5. 把 476 张用出 4760 张的效果:增强策略与迁移验证

476 张的规模,单靠原始数据训练出来的模型泛化能力有限,但通过合理的增强和迁移策略,可以把这个小数据集的效用放大。我一般会做三件事。

第一件是离线增强扩样本。YOLO 自带的在线增强每次 epoch 随机变换,但小样本下模型可能在同一张图上过拟合。用 Albumentations 做一轮离线增强,把训练集扩到 3 到 5 倍,重点加高斯噪声、运动模糊和局部遮挡,模拟产线相机抖动和药片反光。代码不复杂,核心是A.Compose里把GaussNoise、MotionBlur、CoarseDropout串起来,对图片和 bbox 同步变换。

第二件是用预训练权重做迁移。yolov8n.pt本身是在 COCO 上训过的,已经学到了边缘、纹理这些底层特征,药品检测需要的是在这些特征上微调出泡罩板和药片的语义。冻结 backbone 前 10 层先训 20 轮,再解冻全量训 100 轮,这个两阶段策略在小样本上比直接全量训练稳定得多。冻结的层数可以通过model.model[:10]查看,不同版本索引不一样,以实际打印为准。

第三件是交叉验证代替单次划分。476 张做 5 折交叉验证,每折用 380 张训练、96 张验证,轮换五次,最终指标取五折平均。这样比单次 8:2 划分的评估结果可靠得多,也能看出模型在不同数据子集上的稳定性。如果五折的 mAP 方差超过 0.1,说明数据分布不均匀,需要回头检查采集环节。

策略数据量效果训练时间适用阶段
原始 476 张基线最短流程验证
离线增强 3 倍约 1400 张中等正式训练
5 折交叉验证等效 5 次训练最长指标评估
两阶段迁移不变中等小样本首选

最后说一个我踩过的坑:有次拿这份数据训完模型,验证集 mAP 0.88,兴冲冲接到产线相机上,结果传送带速度一快,运动模糊直接把缺片检测干到 0.4 以下。后来在增强里加了MotionBlur(blur_limit=7),重新训了一版才稳住。小样本数据集的价值不在于它本身有多大,而在于它能让你用最低成本把整条训练和部署链路跑通,跑通之后补数据的方向也就清楚了。希望帮到你。

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

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

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

立即咨询