
简介药品包装盒数据集包含1032张真实药品包装盒图片已按Pascal VOC与YOLO两种主流格式完成标注可直接用于目标检测模型的训练、验证和算法教学。包内共2000个文件涵盖1032张jpg图像、1032个xml标注文件及1032个txt标注文件压缩包大小约27.95MB。标注类别为单一“box”共绘制1468个矩形框使用labelImg工具统一标注确保数据格式规范、标注合理。需要说明的是该数据集仅提供准确标注不附带训练好的模型或权重文件因此更适合需要标准数据格式、希望自主完成训练与调参的开发者。当前已有1285人学习下载对刚接触目标检测或正在准备自定义数据集的读者具有一定参考价值。1. 药品包装盒数据集1032张VOCYOLO双格式够不够用全看你怎么拆药品包装盒检测听着小众真做起来就知道有多麻烦药盒反光、小字密集、批次喷码清晰度参差公开数据集几乎没有。所以当有人拿到一份“药品包装盒数据集1032张VOCYOLO格式”的压缩包时第一反应往往不是“太好了”而是“这1032张够训吗、标注靠谱吗、两种格式到底怎么组织”。我的回答是1032张对小目标检测来说确实不算大但它同时给了VOC的XML和YOLO的TXT两套标注意味着你不用花两三天改格式就能直接进训练管线更适合当“模型体检台”和“迁移学习起点”。这篇就按我自己的落地路径把这个数据集从解压、对账、转换、训练到踩坑完整拆开讲。适合正在做药品视觉盘点、药盒OCR区域检测、或者想快速验证一套YOLO训练流程的开发者。2. 解压与数据体检先别急着训练把1032张图的账算清楚2.1 压缩包结构长什么样先摸清再动手这类数据集通常解压后是两层结构外层一个图片目录一个标注目录有的还带一个说明文档。图片目录里是JPG或PNG标注目录里要么是XMLVOC风格要么是TXTYOLO风格或者是两边都有。我先说常见做法拿到手第一步不是打开图片看而是先看目录树确认图片和标注是不是一一对应、有没有子目录套子目录。dataset/ ├── images/ # 图片命名一般是 0001.jpg 这种序号 │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── annotations/ # VOC 的 XML │ ├── 0001.xml │ ├── 0002.xml │ └── ... └── labels/ # YOLO 的 TXT ├── 0001.txt ├── 0002.txt └── ...注意目录名不一定长这样有的压缩包用 JPEGImages、xml、yolo_labels 之类的命名还有的把两个标注格式放在同一个文件夹里按扩展名区分。先跑一条 bash 命令把结构打出来再决定后续脚本怎么设计find . -maxdepth 2 -type d | sort find ./images -type f | wc -l find ./annotations -type f -name *.xml | wc -l find ./labels -type f -name *.txt | wc -l第一行的 find 只看两层目录够你判断有没有嵌套后面三行统计三种文件数量如果三者数量不一致多半是解压不完整或者某些图片本来就没有标注空标注文件被省略了。这一步虽然基础但能拦住后面训练时报“No labels found”这类尴尬。2.2 用脚本对账孤儿文件、无标签图片、类别分布一次看全数量对上了不代表内容对得上。我习惯写一个体检脚本把四件事一起查图片和XML是否有孤儿、每个XML里有没有object、类别都有哪些、每张图的框数量分布。这样能提前知道哪些图是“空图”哪些类别的样本特别少直接决定后面训练时要不要做类别均衡或数据增强。import os import xml.etree.ElementTree as ET from collections import Counter img_dir images xml_dir annotations imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} xmls {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)} print(图片数:, len(imgs), XML数:, len(xmls)) print(有图无XML:, len(imgs - xmls)) print(有XML无图:, len(xmls - imgs)) cls_counter Counter() box_counter Counter() empty_xmls [] for name in sorted(xmls): tree ET.parse(os.path.join(xml_dir, name .xml)) objs tree.findall(object) box_counter[len(objs)] 1 if not objs: empty_xmls.append(name) for obj in objs: cls_counter[obj.find(name).text.strip()] 1 print(类别分布:, dict(cls_counter)) print(每图框数分布:, dict(sorted(box_counter.items()))) print(空XML数量:, len(empty_xmls), 前5个:, empty_xmls[:5])这段代码的逻辑不复杂先拿文件名集合求差集找孤儿再遍历XML统计类别和框数。重点是后面两个输出类别分布决定你训练配置文件里的names列表顺序框数分布能告诉你数据集的标注密度。如果发现某张图有十几个框、某张图只有1个框说明标注粒度不一致后面训练时对不同尺度的目标会有明显差异。提示如果压缩包里同时有TXT格式的标注建议也按同样的思路统计一下TXT的行数和XML的object数对比。行数对不上说明两种格式不是同一次标注生成的这种情况下以XML为准重新生成TXT最稳妥。2.3 抽检验证把标注画回图片上肉眼过一遍脚本对账只能发现“数量”问题发现不了“位置”问题。比如XML里的坐标和图片实际内容对不上、框没框住盒子整体、类别标签贴错这些只能靠眼睛看。我一般会随机抽50到100张把XML的bndbox用OpenCV画回原图拼成一张大图快速扫一遍。import cv2 import random import xml.etree.ElementTree as ET xmls [f for f in os.listdir(xml_dir) if f.endswith(.xml)] random.seed(0) sample random.sample(xmls, 50) for name in sample: img_path os.path.join(img_dir, name.replace(.xml, .jpg)) img cv2.imread(img_path) h, w img.shape[:2] tree ET.parse(os.path.join(xml_dir, name)) for obj in tree.findall(object): bbox obj.find(bndbox) x1 int(float(bbox.find(xmin).text)) y1 int(float(bbox.find(ymin).text)) x2 int(float(bbox.find(xmax).text)) y2 int(float(bbox.find(ymax).text)) label obj.find(name).text.strip() cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, label, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(fcheck_{name}.jpg, img)画框代码里有个细节ymin可能小于字号高度所以putText的y坐标用了max(0, y1 - 5)防止文字写到图像外面。另一个值得注意的点是坐标字符串转float再int因为XML里可能写的是浮点数。抽检时重点看三类问题框是不是歪的、框住的是盒子整体还是只有药品名区域、有没有把盒子和背景里的手掌或桌面框进去。这批抽检图看完没有问题再进转换和训练环节。3. VOC转YOLO转换脚本、类名映射与四个注意位3.1 两种格式的本质差异先搞清楚再写代码VOC格式的标注存在XML里坐标是绝对值单位是像素记录的是左上角和右下角两个点的坐标xmin, ymin, xmax, ymax。YOLO格式的标注存在TXT里每行一条目标格式是“class x_center y_center width height”全部是相对于图片宽高的归一化值范围在0到1之间。两种格式的对应关系如下表项目VOC XMLYOLO TXT坐标xmin, ymin, xmax, ymax 绝对像素x_center, y_center, w, h 归一化类别object/name 字符串每行开头一个整数索引尺寸来源自行读取XML或图片必须依赖已知的图片宽高文件后缀.xml.txt读取方式ElementTree等XML解析纯文本按行读如果原始数据里已经给了YOLO格式的TXT理论上不需要转换。但我还是建议自己做一遍转换脚本一方面能验证TXT是不是和XML完全同步另一方面你拿到的TXT可能类别顺序和你用的训练框架要求的names顺序不一致。自己生成的TXT类别索引才心里有数。3.2 转换脚本XML到TXT的完整实现这一节直接给完整可跑的脚本默认目录是 images、annotations、labels 三层结构类别名写死在映射表里。顺序很重要因为YOLO的类别索引就是列表的下标动一下顺序整个模型训练的语义就变了。import os import xml.etree.ElementTree as ET # 类别映射表顺序即索引训练配置里的names必须和这里完全一致 CLASSES [box, label, text_area, batch_code] xml_dir annotations img_dir images out_dir labels os.makedirs(out_dir, exist_okTrue) def convert_xml(xml_path, img_w, img_h, out_txt): tree ET.parse(xml_path) lines [] for obj in tree.findall(object): name obj.find(name).text.strip() if name not in CLASSES: print(f未知类别: {name} in {xml_path}) continue cls_id CLASSES.index(name) bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 夹紧坐标防止越界产生负数 x1, x2 max(0, min(x1, img_w)), max(0, min(x2, img_w)) y1, y2 max(0, min(y1, img_h)), max(0, min(y2, img_h)) if x2 x1 or y2 y1: continue x_center (x1 x2) / 2.0 / img_w y_center (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines)) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem xml_file[:-4] img_path os.path.join(img_dir, stem .jpg) if not os.path.exists(img_path): print(f图片缺失跳过: {img_path}) continue img cv2.imread(img_path) if img is None: print(f图片无法读取: {img_path}) continue h, w img.shape[:2] convert_xml(os.path.join(xml_dir, xml_file), w, h, os.path.join(out_dir, stem .txt))逻辑上分两步走外层循环遍历XML内层每个object解析坐标并归一化。参数说明有几点值得注意坐标夹紧是防御性写法因为标注工具偶尔会画出超出图片边界的框归一化用了6位小数训练框架读取float精度足够算中心点和宽高时先求像素值再除不要先归一化再加减否则会有精度漂移。3.3 三个必须对得上的参数位错一个全盘翻车第一是类别映射表CLASSES的顺序。这个表不光是转换脚本用训练时的data.yaml里的names列表必须和它一模一样。很多人习惯按字母排序类别名但XML里的标注顺序未必是字母序一旦索引错位模型训练完预测出来的标签全是错乱的。第二是图片宽高到底以谁为准。XML里其实有 节点记录width和height但有些标注工具写的是原始分辨率有些是缩略图的还有的是空的。最稳的办法是直接用cv2.imread读图片实际shape以这个为准。如果图省事用XML里的size遇到标注工具写了错误值时所有框都会偏移。第三是TXT文件名必须和图片名完全同构。YOLO系列训练框架默认按前缀匹配图片和标签比如images/0001.jpg找labels/0001.txt。差一个字符、大小写不一致、或者多一层目录都找不到。转换脚本里用stem去掉扩展名再拼接能保证这一点。注意转换完成后再跑一次对账脚本检查labels目录的TXT数量和images数量是否一致、没有孤儿TXT。转换过程不是终点能自动验证才算做完。4. 数据划分与YOLO训练配置1032张小样本怎么稳定训练4.1 按盒型分组划分而不是随机打散1032张图听起来够用但如果你直接随机拆train/val多半会把同一个药盒的不同角度、不同光线下的照片分进两个集合里val精度虚高部署到真实场景立刻被打回原形。我在某药品信息化项目的教训是必须按“盒型”分组划分。数据集文件名如果是纯序号看不出分组那就看XML里标注的类别组合把所有含相同类别组合的图归为一个“场景桶”再按桶划分。import os import random import xml.etree.ElementTree as ET xml_dir annotations group_map {} for f in os.listdir(xml_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, f)) classes tuple(sorted(set(obj.find(name).text.strip() for obj in tree.findall(object)))) group_map.setdefault(classes, []).append(f[:-4]) names list(group_map.keys()) random.seed(42) random.shuffle(names) train_groups names[:int(len(names) * 0.7)] val_groups names[int(len(names) * 0.7):int(len(names) * 0.85)] test_groups names[int(len(names) * 0.85):] def write_file(path, groups): with open(path, w) as f: for g in groups: for name in group_map[g]: f.write(name \n) write_file(train.txt, train_groups) write_file(val.txt, val_groups) write_file(test.txt, test_groups) print(train图数:, sum(len(group_map[g]) for g in train_groups)) print(val图数:, sum(len(group_map[g]) for g in val_groups)) print(test图数:, sum(len(group_map[g]) for g in test_groups))这段脚本把类别组合作为分组键保证同一个盒型的所有图像落在同一个集合里。如果数据集里本身有大量“只有1个框”的图按这个逻辑它们会各自成组划分后train可能包含很多单目标图这没关系关键是不泄漏。如果文件名本身带盒型前缀比如阿莫西林_001.jpg改成按前缀划分更直接代码逻辑是同样的思路先提取前缀建桶再按桶切分。4.2 数据组织与data.yaml目录结构、nc、names划分完成后把文件组织成YOLO训练工具认识的目录结构。常见做法是images和labels同层级train.txt/val.txt记录的是图片路径一行一个。也有的框架直接按目录结构识别不管哪种核心是把对应索引的文件路径写对。# data.yaml path: ./drug_box_dataset train: train.txt val: val.txt test: test.txt nc: 4 names: [box, label, text_area, batch_code]注意这里names的顺序必须和转换脚本里CLASSES的索引一一对应。path路径建议用相对路径放到训练项目根目录下避免不同机器上绝对路径漂移。如果某个框架不支持test字段直接删掉那行即可。目录结构上一章已经生成labels再加上转换出来的TXT整个数据管线就闭环了。4.3 训练参数与增强开关小数据集的保守起手式1032张图训练目标检测模型参数设置要偏保守。下面这张表是我在这种量级数据上试过比较稳的起点参数建议值说明imgsz640药盒上小字太多512会丢细节batch16显存有限梯度也够稳epochs100小数据集收敛快配早停防止过拟合lr00.01预训练权重微调用默认即可mosaic0.5 或关闭小数据集开满mosaic容易把目标切碎close_mosaic10最后10轮关闭mosaic做精修patience20连续20轮没提升就停optimizerSGD或AdamW数据量小时SGD反而更稳开启预训练权重是关键一步。用预训练权重最终的mAP和收敛速度会明显优于从零训练尤其是这种千张量级的数据集。训练命令以常见的YOLO训练脚本为例模型规模选s量级别一上来就上大模型python train.py --data drug_box.yaml --weights yolov5s.pt \ --img 640 --batch 16 --epochs 100 \ --mosaic 0.5 --close_mosaic 10 --patience 20参数说明weights用s模型参数量适中在小数据集上不易过拟合mosaic只留0.5既能有增强效果又不会把药盒上的小字切没close_mosaic 10的意义是最后10轮关闭拼接增强让模型在接近真实分布的图上精调。训练过程里盯两个指标val/box_loss是不是持续下降、val精度是不是在epoch 60以后还在涨。小数据集上如果精度到第40轮就开始过拟合说明增强和正则不够下一轮把mosaic关掉或减少epoch。提示如果训练日志里出现大量“unable to find label”报警先回到第2章的对账流程查TXT文件命名不要调训练参数问题百分之百在数据路径上。5. 药品包装盒数据集踩坑记录与排查清单5.1 训练时报“No labels found in train images”图片和XML账没对上现象训练刚开始就报找不到标签或者train图片数量正常但标签数为0。原因常见的有三种——XML缺失导致该图没有对应TXT、XML里所有object的类别都不在映射表里导致生成的TXT是空文件、图片和XML文件名大小写或后缀不一致。解决回到第2章体检脚本先跑对账看差集再检查TXT文件是否有0字节文件最后确认xml里的name字符串是否有多余空格或换行。我遇到过标注文件里类别名带“\u3000”全角空格的情况strip()都不一定救得回来要直接打印name的长度逐一排查。5.2 中文路径导致图片读取失败训练默默跳过一批图现象部分图片能读取部分读不了肉眼看起来文件都在。原因解压后路径里带中文文件夹名或者文件名是中文药名缩写OpenCV和某些训练框架的底层文件读取对非ASCII路径支持不一致。解决图片和标签统一改成英文或数字命名目录不要有中文和空格。批量重命名时可以保留原文件名做映射表方便后面回溯。这类问题最阴的地方在于脚本不报错只是图片为零张等你发现时已经白训了几个小时。5.3 类别编号错位loss正常下降预测结果全是错的现象训练一切正常val精度也不差但实际推理时把“label”框出来标成“batch_code”。原因转换脚本里的CLASSES顺序、data.yaml里的names顺序、预训练权重原先的类别语义三者没有对齐。特别是用了预训练权重时模型输出头会继承原权重的类别个数和顺序如果你的yaml改了nc但没同步改模型输出头索引自然乱掉。解决转换脚本、data.yaml、模型定义处的类别列表用同一份配置文件生成不要手敲三遍。我在项目里直接把CLASSES写到一个单独的classes.txt三个环节都从这个文件读取一次改到位。5.4 小字信息漏检批号、有效期这类小目标成了重灾区现象盒子本体检测没问题但“batch_code”这类小区域mAP特别低很多直接漏检。原因药盒的批号通常只有十几个像素高在640分辨率下很小加上mosaic增强里小目标容易被裁掉一半模型根本没学到有效特征。解决训练时把imgsz提到736或768显存够的前提下关闭mosaic或调低到0.3并专门统计一下这类小框的数量。如果整个数据集里batch_code类别的标注框不足100个优先做两次该类别图集的过采样复制而不是盲目加旋转增强。5.5 val精度虚高真实场景翻车同一个盒型被随机拆进两个集合现象val集mAP有0.9部署到真实货架上一塌糊涂。原因随机划分数据集时同一个药盒的正面、反面、不同角度的图被分进了train和val模型在val上见过的“同伴”太多相当于开卷考试。解决按第4章的分组划分逻辑重新切分以盒型或以类目组合为粒度保证同一实体只出现在一个集合里。这是小数据集最隐蔽的陷阱也是 zui 容易被忽略的一环。宁可val集图少一点也要保证划分粒度干净。6. 进阶玩法把这个数据集变成你自己的模型体检台6.1 批量推理与置信度分布复盘1032张的训练集训出来的模型真正价值不在精度数字而在于它能帮你建立“哪些场景图还不够”的判断力。我训练完会在测试集上跑批量推理然后按类别统计置信度分布找出预测分低于阈值的样本它们就是下一轮补数据的候选。这类复盘脚本长这样import os import cv2 import numpy as np # 假定已加载模型result是检测结果列表 # 每个元素为 (cls_id, conf, x1, y1, x2, y2) results_by_cls {0: [], 1: [], 2: [], 3: []} for img_path in test_images: img cv2.imread(img_path) dets model.predict(img) # 示意调用 for cls_id, conf, box in dets: results_by_cls[cls_id].append(conf) for cls_id, confs in results_by_cls.items(): low [c for c in confs if c 0.5] print(fclass {cls_id}: 总数{len(confs)} 低分{len(low)} 占比{len(low)/max(len(confs),1):.2%})这段代码可复现的关键是“按类别看低分占比”如果某类低分占比超过三成说明特征没学好优先补图而不是调参。判定阈值先定0.5后续看需求调高到0.6或0.7。6.2 迁移到新盒型的最小操作换个新药盒上线最怕从零训练。正确做法是先用这个模型在新盒型的小样本上做权重微调主干冻结训练20轮让新盒型的特征只更新检测头再解冻全部层微调30轮。前20轮学习率降到0.001以下防止破坏已有特征。冻结解冻在训练脚本里通常通过freeze参数控制比如freeze10表示冻结前10层主干。微调时数据量要求极低每种盒型准备20到30张标注图就能跑出可用的版本这就是当初做好数据集账目的回报。我踩过最深的坑就是拿到新数据集直接全参训练结果把预训练权重学到的通用特征全冲掉了新盒型精度没上去旧盒型反而掉了不少。从那以后任何小样本微调都默认先冻结主干解冻顺序永远是“检测头优先、高层特征其次、底层最后”。这个习惯帮我省下大量重训时间希望也能帮你少走弯路。本文还有配套的精品资源点击获取