
简介面向药品包装视觉质检与目标检测教学场景这份数据集收录了111张板蓝根颗粒袋装实拍图并同时提供Pascal VOC与YOLO双格式标注可直接用于YOLO系列、Faster R-CNN等主流检测模型的训练与效果验证。图像内容覆盖999感冒灵与板蓝根两类目标共标注134个边界框框位置与类别信息一目了然每张图对应同名xml与txt文件省去自行转换标注的繁琐环节。压缩包共335个文件主要构成为111个jpg原图、111个xml标注文件与111个txt标签文件整体仅3.71MB下载与解压都非常轻量。所有标注均通过labelImg工具人工逐框完成类别边界划分清晰数据量适中既适合初学者逐张对照两种标注格式的差异也可以作为快速验证模型管线的轻量测试集。目前已有121人学习浏览适合希望尽快进入目标检测实操的开发者或学生。1. 药品板蓝根颗粒检测数据集110张双格式标注图的真实分量做药品包装外观检测的同行应该都有感受模型不难训难的是数据。板蓝根颗粒这种袋装药品市面公开数据集少自己拿labelImg一张张框又太耗时间。这份数据集是板蓝根颗粒袋装检测的VOCYOLO双格式标注资源111张JPEG原图对应111份XML和111份TXT标注类别只有两个——999ganmaoling和banlangen一共134个边界框。适合的任务很聚焦药品包装检测、小目标分类、生产线外观质检的算法预研。数据量不大但胜在格式标准、类别干净拿来跑通整个目标检测训练链路或者验证YOLO系列在自己业务场景的可行性都合适。需要迁移到自己的产线场景重点看标注规范和格式转换这部分后面我会把坑一并说清。2. VOC与YOLO格式的底层差异XML和TXT字段对照与转换逻辑2.1 XML和TXT字段逐项对照从绝对坐标到归一化坐标Pascal VOC的XML标注是这套数据最完整的源头。每张图如banlangen_xyxr_3.jpg都有一个同名XML文件里面记录的不是某个模型专用的框而是结构化的场景信息。把XML打开后核心字段集中在annotation根节点下的object节点里annotation folderbanlangen/folder filenamebanlangen_xyxr_3.jpg/filename path/home/user/banlangen_xyxr_3.jpg/path source databaseUnknown/database /source size width640/width height640/height depth3/depth /size segmented0/segmented object namebanlangen/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin198/xmin ymin215/ymin xmax437/xmax ymax468/ymax /bndbox /object /annotation注意这里的xmin、ymin、xmax、ymax都是绝对像素坐标单位对应原图分辨率。size节点记录了图片宽高这个信息在后续转YOLO格式时至关重要——归一化公式要以它为分母。segmented0表示没有做分割标注difficult0意味着这张图里的目标没有被标记为识别困难的样本这两个字段在纯检测场景中可以忽略。文件夹里有111张图解压后第一件事我建议先抽查图片尺寸是否统一。用Python读几张图把宽度、高度打出来import cv2 import os img_dir banlangen files [f for f in os.listdir(img_dir) if f.endswith(.jpg)] sizes {} for f in files: img cv2.imread(os.path.join(img_dir, f)) h, w img.shape[:2] sizes[(w, h)] sizes.get((w, h), 0) 1 for size, count in sorted(sizes.items()): print(f尺寸 {size} 的数量: {count})这段脚本用OpenCV读取每张图的shape把宽高组合作为字典key统计出现次数。如果输出只有一种尺寸说明这批数据拍摄设备统一后续YOLO训练的imgsz参数可以直接按这个尺寸来如果出现两种以上的尺寸就要做缩放或填充预处理。多数公开小数据集图片尺寸在640附近但板蓝根这类拍摄场景如果用了不同手机或相机尺寸可能参差不齐这个检查不能跳过。YOLO的TXT标注则完全换了一套坐标系。每行代表一个目标格式是类别索引 中心点x 中心点y 宽度 高度四个坐标值全部除以图片宽高归一化到0到1区间。这段数据里两个类别按字母序排列999ganmaoling索引为0banlangen索引为1。对应上面的框TXT行内容如下1 0.496094 0.533594 0.373438 0.395313这个数字怎么来的x中心 (198437)/2/640 317.5/640 ≈ 0.4961宽度 (437-198)/640 ≈ 0.3734。换算不复杂但错一位小数点框就偏得离谱。所以不管是自己标注还是拿到外部数据集第一道检查永远是抽一张图把TXT里的坐标用脚本画回原图上用肉眼看框是否贴合目标。两个类别的框数差异也要留意——999ganmaoling是49框、banlangen是85框意味着某些图上只出现了其中一个类别训练时类别不均衡的影响会在第4章展开说。2.2 labelImg的两种保存模式为什么XML和TXT能同时存在这套数据用的是labelImg标注工具从标注框的样子就能看出来——labelImg保存的XML里带有pose、truncated这些字段是它的标准输出。labelImg默认保存VOC格式的XML但工具栏右上角有一个PascalVOC和YOLO的切换按钮切到YOLO模式后保存的就是TXT。拿到两份格式意味着原来的标注人员手动切了模式或者用脚本统一转换了一遍。这个细节对使用者的提示是两份文件的数值源头一致但坐标精度在转换过程中可能有舍入误差。实际使用时我一般会做一次坐标回验把XML里的box转成YOLO格式和TXT逐一对比差的绝对值超过0.001就要警惕。尤其注意类别顺序——YOLO的TXT只存整数索引不存类别名索引错位整张图的标注就废了。这份数据的类别只有两个属于风险最低的情况如果类别数量多建议先跑一个脚本把所有TXT里的索引值去重检查是否都在有效区间内。一个小习惯解压后第一件事不是急着训练而是先确认类别顺序的映射规则。用脚本把TXT第一列的所有索引值扫一遍最大值若大于或等于类别数说明有越界索引这种文件通常来自标注中途切换格式时产生的数据残留。索引越界是小数据集最常见的低级错误训练时不会报错只会让模型学到错误映射。2.3 类别索引与names顺序训练前必查的映射关系YOLO训练框架读取TXT时会把第一列的整数当成类别索引去data.yaml里的names列表中查对应的类别名。如果names列表的顺序和TXT标注时的索引顺序不一致模型训练的loss曲线看起来完全正常但预测结果会错位——把999ganmaoling识别成banlangen反之亦然。这个错位在训练过程中不会暴露直到你拿真实图片测试才会发现。检查映射关系的脚本很简单import os txt_dir banlangen/annotations_txt indices set() files [f for f in os.listdir(txt_dir) if f.endswith(.txt)] for f in files: with open(os.path.join(txt_dir, f)) as fp: for line in fp: line line.strip() if line: idx int(line.split()[0]) indices.add(idx) print(TXT中出现的类别索引:, sorted(indices))这段代码遍历所有TXT标注文件把每一行第一列的整数值收集到set集合中。集合自动去重最后打印出所有出现过的索引值。如果输出是{0, 1}说明两个类别都有样本索引在合理范围内如果出现2、3之类的数字说明标注文件和说明文档里写的类别清单对不上需要重新核对来源。数据集的描述里明确标注类别数为2对应索引0和1因此以上输出ok。3. 用YOLOv8训练自己的数据集目录布局、YAML配置与完整命令3.1 目录结构规范和train/val划分脚本YOLO系列训练框架对数据集目录结构的要求很明确。当前主流的ultralytics仓库在训练时会自动读取images和labels子目录下的train、val子文件夹不需要手动写路径。常见的做法是把数据集按如下结构整理banlangen_dataset/ ├── images/ │ ├── train/ │ │ ├── banlangen_xyxr_3.jpg │ │ ├── banlangen_xyxr_45.jpg │ │ └── ... │ └── val/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── banlangen_xyxr_3.txt │ │ ├── banlangen_xyxr_45.txt │ │ └── ... │ └── val/ │ └── ... └── data.yaml111张图的量级不需要复杂的划分策略8比2的随机划分就够用train约88张、val约23张。但注意随机划分时要固定随机种子不然每次跑数据划分脚本得到不同的train/val组合实验结果没法复现。我写的划分脚本会按文件名排序后做分层抽样保证两个类别的样本在train和val中的比例接近原始分布import os import random import shutil random.seed(42) img_dir banlangen/images_all txt_dir banlangen/annotations_txt train_img_dir banlangen/images/train val_img_dir banlangen/images/val train_txt_dir banlangen/labels/train val_txt_dir banlangen/labels/val for d in [train_img_dir, val_img_dir, train_txt_dir, val_txt_dir]: os.makedirs(d, exist_okTrue) files [f for f in os.listdir(img_dir) if f.endswith(.jpg)] 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.copy(os.path.join(img_dir, f), os.path.join(train_img_dir, f)) shutil.copy(os.path.join(txt_dir, stem .txt), os.path.join(train_txt_dir, stem .txt)) else: shutil.copy(os.path.join(img_dir, f), os.path.join(val_img_dir, f)) shutil.copy(os.path.join(txt_dir, stem .txt), os.path.join(val_txt_dir, stem .txt)) print(train:, len(os.listdir(train_img_dir)), val:, len(os.listdir(val_img_dir)))这段脚本做的事很直接固定随机种子为42保证可复现80%的图进train目录20%进val目录对应的TXT标注同步拷贝。参数方面split值可以按需调整——如果做交叉验证可以改成5折循环如果训练时发现val样本里某个类别一个框都没有就得回到这步重新划分确保每类在train和val中都有代表性样本。我一般会额外生成一个classes.txt放在数据集根目录内容为两个类别名逐行排列。这个文件不是YOLO训练必需的但在后续转成其他格式、或者给别人复用数据时能省去不少沟通成本。3.2 data.yaml配置与YOLOv8训练命令训练前需要写data.yaml注意path的写法用相对路径更稳妥。ultralytics框架会在当前工作目录下解析path字段绝对路径虽然也能用但换机器跑就得改配置。data.yaml的内容如下path: banlangen_dataset train: images/train val: images/val names: 0: 999ganmaoling 1: banlangen注意一个细节data.yaml里的nc类别数可以省略ultralytics会自动依据names长度推断但显式写出来能避免意外。names的顺序必须和TXT标注的索引顺序完全一致——0对应999ganmaoling、1对应banlangen这个顺序由标注时的设定决定改顺序等于重新标注。训练命令我一般用固定参数方便对比不同实验yolo detect train databanlangen_dataset/data.yaml modelyolov8n.pt epochs150 batch8 imgsz640 device0几个参数的取舍逻辑epochs设150是因为数据集小模型容易过拟合早点收敛后靠早停机制截断。ultralytics框架默认patience50意思是连续50个epoch验证指标不提升就停如果想让训练更早收住可以显式加patience20。batch8在常规消费级显卡8GB到12GB显存上刚好跑满显存如果你的卡显存小降到4也能跑只是梯度更新频率变低收敛会慢一些。yolov8n是轻量骨架111张图的数据量喂不动yolov8l这类大模型强行用大模型只会过拟合。如果资源紧张把device改成cpu也能跑只是单轮epoch会慢很多建议先用nano模型验证流程再根据结果决定是否升级模型。训练完成后模型会自动保存在runs/detect/train目录下。验证集的指标在训练结束后自动打印包括mAP50和mAP50-95要重点看mAP50——它只统计IOU大于0.5的预测框对边界框精度要求不那么苛刻是小数据集上最能反映模型是否学到了类别特征的指标。3.3 XML反向校验TXT一张图都不放过的检查脚本训练前最后一道防线是写个脚本把XML转成YOLO格式和TXT对比确保标注完全一致。我自己写过一个版本核心逻辑如下import xml.etree.ElementTree as ET import os def xml_to_yolo(xml_path, w, h): tree ET.parse(xml_path) root tree.getroot() boxes [] for obj in root.iter(object): cls obj.find(name).text idx 0 if cls 999ganmaoling else 1 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) x_center (xmin xmax) / 2 / w y_center (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h boxes.append(f{idx} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) return boxes xml_dir banlangen/annotations_xml txt_dir banlangen/annotations_txt for xml_file in os.listdir(xml_dir): stem os.path.splitext(xml_file)[0] xml_path os.path.join(xml_dir, xml_file) txt_path os.path.join(txt_dir, stem .txt) tree ET.parse(xml_path) root tree.getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) yolo_lines xml_to_yolo(xml_path, width, height) with open(txt_path) as f: txt_lines [line.strip() for line in f.readlines()] if yolo_lines ! txt_lines: print(f不一致: {xml_file})这个脚本比前面的版本多了一个关键步骤从XML的size节点动态读取width和height而不是硬编码640。xml_to_yolo函数里类别名字符串被映射为整数索引999ganmaoling对应0banlangen对应1。中心坐标和宽高全部归一化后格式化成6位小数与TXT中保存的精度保持一致避免因为四舍五入差异造成误报。3.4 预测验证从val目录观察模型表现训练结束后用训练好的权重跑一遍验证集预测把结果可视化出来是最直观的检验方式yolo detect predict modelruns/detect/train/weights/best.pt sourcebanlangen_dataset/images/val save_txtTrue save_cropTruepredict命令会把每张图的预测结果保存到runs/detect/predict目录包括画了框的原图、TXT格式的预测结果以及按类别裁剪出的目标图片。其中save_cropTrue这个参数特别适合小数据集——生成的裁剪图可以直接看出模型是否把药袋的印刷文字、包装边缘等高相似度区域错误识别成目标。预测的TXT格式和标注TXT完全一致第一列是类别索引后面四个数是归一化坐标可以直接拿来做对比分析。我习惯从val目录里挑三张图重点观察一张含两个类别同时出现、一张只有banlangen单类、一张目标较小或者有遮挡的图片。这三类情况能覆盖小数据集模型性能评估的大多数场景。4. 避坑排查小数据集训练中几个高频翻车点4.1 类别顺序错位导致迁移学习失败现象使用YOLOv8预训练权重做迁移学习训练几轮后验证集的mAP始终低于0.3且预测结果中两个类别的名称相互混淆。原因预训练模型在COCO数据集上学习的是80类我们自己的数据集只有2类。模型最后一层的分类头维度不同会重新初始化。如果data.yaml里的类别顺序与标注TXT里的索引不一致——比如脚本里写反了0和1——模型就会把999ganmaoling学成banlangen反之亦然。TXT文件的第一列是纯数字没有类别名直接对应关系这种错位不会报错只会反映在最终预测上。解决训练前用第3章的校验脚本跑一遍再叠加一个更直观的检查从val目录随机挑几张图用训练完的模型预测把预测框的类别名打印出来人工看一遍。实际操作中我一般会在训练前就把类别顺序固定下来写进data.yaml之后不再改动。如果是从COCO预训练权重开始还会刻意检查模型最后一层的输出维度是否等于2。4.2 类别比例不均导致mAP虚高现象训练结束后mAP50显示0.85以上但实际部署时对999ganmaoling这个类别的检测效果很差漏检率高。原因查看数据集标注分布999ganmaoling类别是49个框banlangen类别是85个框。模型在训练时看到banlangen的样本多对这个类别的特征学习更充分999ganmaoling样本少模型学到的模式不够稳定。小数据集上的mAP是两类AP的平均值如果banlangen的AP接近1.0999ganmaoling只有0.6平均下来也有0.8看起来不错但实际用起来就是另外一回事。解决评估时不要只看总的mAP按类别拆分看AP。ultralytics训练结束后会生成confusion_matrix.png和results.csv在results.csv里可以看到每个类别的AP值。如果某个类别AP明显偏低优先补充该类别的数据增强或者把该类别的图片在训练时提高采样权重。YOLOv8支持按类别设置loss权重但目前主流方式还是数据层面做平衡比如对样本少的类别做更多随机裁剪增强。4.3 遮挡与堆叠样本不足导致的漏检现象验证集上推理效果尚可但模型在真实场景里对堆叠的板蓝根颗粒袋漏检严重多个目标距离近时只检测出一个。原因拍摄场景是药品包装实际使用中经常出现多袋堆叠、互相遮挡。这份数据集的134个框分布里两个类别的框大多数是独立摆放的遮挡重叠的样本比例不高。模型学到的更多是“完整外形的药袋”特征遇到遮挡就失效。解决针对遮挡场景做数据增强。YOLOv8默认启用Mosaic增强它把四张图拼接成一张在一定程度上模拟了目标裁剪和拼接的效果但模拟不了真实遮挡。可以额外用Random Erasing把图片中随机区域替换成噪声或灰色像素强迫模型学习非完整特征的目标。此外在标注层面如果原始数据里确实有遮挡样本但被遗漏了可以用labelImg补标这些图把这些样本单独挑出来加入训练集效果比单纯增强好得多。4.4 图片尺寸不统一导致框漂移现象模型训练不报错但推理时框的位置明显偏左或偏右或者框的大小和实际目标不符。原因YOLO的TXT坐标全部基于原图尺寸归一化训练时会按imgsz参数把图片缩放到统一尺寸。如果原图宽高比不是1比1模型会自动做letterbox填充在图片两侧补灰边。TXT里的归一化坐标本身没有错但如果训练时imgsz设置得和图片尺寸比例差太远比如图片是1024x768imgsz设为640letterbox后目标的位置会被压缩标注坐标虽然归一化了但模型学到的空间关系和推理时不匹配。解决拿到数据先统一图片尺寸。用脚本把所有图片缩放到同一分辨率比如把宽高都缩放到640超过640的按比例缩小不足的用灰色填充。缩放后必须同步重算标注坐标公式是新xmin xmin * (新宽度/原宽度)。更简单的方式是用ultralytics自带的letterbox逻辑训练时imgsz和原图比例一致就不需要预处理。但输入数据的图片尺寸如果五花八门建议还是先做一次统一的预处理省得以后排查问题时分不清是模型问题还是数据问题。5. 把134个框的价值榨干数据增强组合与验证习惯5.1 适合药品包装场景的增强策略针对板蓝根颗粒袋装数据我实际会用这组增强组合来跑实验Mosaic加亮度抖动加轻微旋转加随机平移。Mosaic是YOLOv8默认启用的它把四张图拼成一张能在小数据集上显著提升模型对不同尺度目标的适应力亮度抖动解决前面说的光照单一问题旋转在±15度以内即可超过这个范围药袋的印刷文字会产生畸变反而干扰特征学习随机平移则弥补拍摄时构图不够居中的情况。如果你的部署环境是生产线固定相机那增强策略可以更保守如果是移动端随手拍旋转和尺度抖动的范围应该加大。这个权衡没有标准答案必须根据实际部署场景来定——增强的本质是模拟推理时可能遇到的变化不是把样本量硬撑大。5.2 小数据集mAP解读与错误预测排查用验证集评估时我不光看总mAP50还会单独跑每个类别的AP和召回率。134个框的体量下一个类别多检或少检两三个框百分比就会波动5个百分点。所以在小数据集上的正确姿势是训练一次记录mAP的同时把错误预测图片导出逐张看是误检把非目标框出来还是漏检没框出目标。导出错误预测可以用YOLOv8的predict模式加save_txt参数配合一个简单脚本把预测结果与原图合成人工过一遍。一个可复用的检查模板先确认类别索引映射——TXT第一列的0是999ganmaoling、1是banlangen再确认标注框和图片尺寸匹配——用OpenCV把TXT坐标乘回宽高画框看是否贴合最后确认train和val的类别分布——如果val里只有一个类别的框那训练出的模型在另一个类别的表现就成了盲盒。5.3 我现在的固定流程数据集的完整度验证完毕之后我现在每次拿到新数据都会强制走一遍解压后检查结构 → XML和TXT交叉校验 → 固定随机种子划分train/val → 用yolov8n跑50个epoch快速验证 → 逐张查看错误预测。这套流程在110张到上万张的数据集上都跑得通区别只是后面的epoch数要增加、增强策略要按数据规模调整。那次在板蓝根数据上踩了类别顺序的坑之后我就养成了这个习惯不管时间多紧训练前一定把TXT的第一列统计一遍绝不让模型偷偷学错映射。数据集的量级不重要标注的准确性才决定模型的上限。希望帮到你。本文还有配套的精品资源点击获取