
简介这是一份面向目标检测入门与药品识别应用的数据集聚焦感冒药“999感冒灵”检测场景包含372张JPG图片同时提供Pascal VOC和YOLO两种主流标注格式方便直接接入YOLO、SSD、Faster R-CNN等常见训练管线。资源包共1119个文件主体为372张jpg图像、373份xml标注和374份txt标签压缩后仅20.88MB轻量紧凑便于下载与本地训练。该数据集仅含“999ganmaoling”一个类别由labelImg工具人工标注累计有效框573个。数据可直接用于模型训练与验证也可作为课程设计或毕设实验数据适合医药零售、智能货柜等场景下的感冒药自动识别。目前已有249人浏览/学习是一份低门槛、易上手的专用数据集资源。1. 372张的感冒药检测数据集值不值得用先想清楚这三个问题先别急着解压。手里这份「药品感冒药999感冒灵检测数据集372张VOCYOLO格式.zip」光看名字就知道它同时给了两套标注一套是VOC的XML一套是YOLO的txt。对做过目标检测数据集的人来说这能省不少转换功夫但也藏着一个隐患如果两套标签不是同一次转换生成训练时你根本分不清该信哪一套。我拿到这类药盒检测数据的第一件事永远不是开训练而是先把整包翻开看配对和类别定义。372张听起来不多但用来做货架药盒检测、药店自助结算或者视觉复核的基线验证完全够用真正决定这个数据集值不值得用的不是数量而是标签是否自洽、类别是否统一、图片有没有被缩放后忘改标注。适合谁适合想用现成数据快速跑通 YOLO 训练链路、验证药盒识别方案的工程师和学生。2. 拆开zip之前先弄清 VOC 和 YOLO 标签到底谁对应谁2.1 压缩包里的两种格式不是二选一而是互补大部分按这套命名规范打包的数据集内部结构多半长这样Annotations/放VOC的XMLJPEGImages/放原图ImageSets/Main/放训练验证划分另外单独一个labels/放YOLO的txt。VOC格式的特点是信息全除了边界框还会记录图片文件名、尺寸、通道数以及每个对象的名字、是否截断、是否难以识别YOLO格式则极简一行一个对象五个数字依次是类别id、归一化后的中心x、中心y、宽、高。VOC 与 YOLO 标注字段对照信息VOC XML 字段YOLO txt 字段图片宽度width无需自行读取图片高度height无需自行读取对象类别name第一个数字类别id框左上角xminymin无框右下角xmaxymax无框中心/宽高无第2~5个数字难例标记difficult丢弃或单独处理当你在数据集里同时看到这两种格式时默认应该把 VOC 当成“母版”把 YOLO 当成“派生版”。因为XML里保留了difficult、truncated这类训练策略需要的属性而txt一旦生成就丢了这些信息。常见做法是先做标签体检和修正确认无误后再统一转成YOLO格式而不是反过来改txt。很多所谓“翻车”现场其实就是有人改了XML里的框忘了重新生成txt训练时模型读的是旧标签。2.2 把zip解压并按图片-标注配对一页脚本看透数据集成色解压这个zip本身有讲究尤其是文件名带中文的压缩包。Windows上右键解压一般没事Linux服务器上用unzip可能会把中文文件名解成乱码这是因为zip压缩时文件名编码不规范常见做法是用Python的zipfile模块处理。# -*- coding: utf-8 -*- import zipfile import pathlib src pathlib.Path(药品感冒药999感冒灵检测数据集372张VOCYOLO格式.zip) out_dir pathlib.Path(dataset_raw) with zipfile.ZipFile(src, r) as zf: # 先打印前20个文件名检查有没有乱码 for name in zf.namelist()[:20]: print(repr(name))跑完这段重点看打印出来的文件名字符是不是正常中文。如果看到âå“è´§这类鬼字符说明zip内部编码不是UTF-8需要手动纠正编码后再解压with zipfile.ZipFile(src, r) as zf: for name in zf.namelist(): # cp437是zipfile默认解码方式gbk是Windows中文常见编码 fixed name.encode(cp437).decode(gbk, errorsreplace) # 逐个写出保持目录结构 target out_dir / fixed target.parent.mkdir(parentsTrue, exist_okTrue) if name.endswith(/): target.mkdir(exist_okTrue) else: with zf.open(name) as fsrc, open(target, wb) as fdst: fdst.write(fsrc.read())这段代码的逻辑是先取原始文件名用cp437重新编码成字节流再用gbk解码成正常中文路径。参数说明errorsreplace是兜底策略个别字符解码失败时用替代符不会让整个解压流程中断。解压完成后下一步是做三向配对检查——每一张图片必须同时有XML和txt且两者里的对象数量一致。from pathlib import Path import xml.etree.ElementTree as ET images_dir Path(dataset_raw/JPEGImages) xml_dir Path(dataset_raw/Annotations) yolo_dir Path(dataset_raw/labels) for img_path in sorted(images_dir.iterdir()): if img_path.suffix.lower() not in {.jpg, .jpeg, .png, .bmp}: continue xml_path xml_dir / (img_path.stem .xml) yolo_path yolo_dir / (img_path.stem .txt) if not xml_path.exists(): print(f缺XML: {img_path.name}) continue if not yolo_path.exists(): print(f缺YOLO: {img_path.name}) continue # 解析XML中的object数量 root ET.parse(xml_path).getroot() xml_count len(root.findall(object)) # 统计YOLO txt非空行数 with open(yolo_path, r, encodingutf-8) as f: yolo_count sum(1 for line in f if line.strip()) if xml_count ! yolo_count: print(f数量不一致: {img_path.name} XML{xml_count} YOLO{yolo_count})这个脚本跑完如果输出为空说明压缩包内部配对没问题如果有“缺XML”或“数量不一致”那你得决定以哪一套为准。我的习惯是XML为准转YOLO时重新生成txt后面第3章会给出完整转换脚本。这一步千万别省你永远不知道压缩包里的YOLO标签是哪次转换的产物万一是别人用错误脚本转的训练出来的模型会很“玄学”指标忽高忽低。另外要特别看一眼ImageSets/Main里的train.txt和val.txt。如果这个压缩包没带划分文件需要自己按比例切分。我一般按8:1:1切训练、验证、测试且保证切分时同一场景的不同角度图片尽量落在同一个集里避免验证集“作弊”。3. 把 VOC XML 转成 YOLO txt转换脚本与3个边界坑3.1 为什么不能直接拿别人的 classes.txt 套上去很多新手拿到这个数据集第一反应是看一眼labels/里txt的第一个数字然后去猜类别。这是最危险的路径。VOC格式里name是字符串YOLO格式里第一个数字是类别id两者的对应关系完全取决于生成txt时用的classes.txt顺序。如果原来的开发者把“999感冒灵颗粒”放在 index 0你却在训练配置里把 index 0 写成“999感冒灵胶囊”模型不会报错但训练出的结果会在推理时把两种药盒搞混而且你很难发现因为loss照常下降。正确的做法是找到压缩包里附带的classes.txt如果没有就从所有XML里提取name的去重集合按字母序或按出现频次固定下来再把这份映射写进转换脚本。这个映射一旦生成就不要手工改顺序否则所有已转换的txt全部作废。3.2 转换脚本下面这个脚本我一般直接放在数据集根目录跑输入是VOC的XML目录输出是YOLO的labels目录。# -*- coding: utf-8 -*- import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image xml_dir Path(dataset_raw/Annotations) img_dir Path(dataset_raw/JPEGImages) out_dir Path(dataset_raw/labels_converted) out_dir.mkdir(exist_okTrue) # 第一步扫描所有XML提取类别名并排序 class_names set() for xml_path in xml_dir.glob(*.xml): root ET.parse(xml_path).getroot() for obj in root.findall(object): class_names.add(obj.findtext(name).strip()) class_names sorted(class_names) print(类别映射:, class_names) with open(classes.txt, w, encodingutf-8) as f: f.write(\n.join(class_names) \n) # 第二步逐个图片转换坐标 for xml_path in xml_dir.glob(*.xml): root ET.parse(xml_path).getroot() filename root.findtext(filename) img_path img_dir / filename if not img_path.exists(): print(f图片缺失: {filename}, 跳过) continue # 用PIL读取真实宽高别信XML里的size with Image.open(img_path) as im: img_w, img_h im.size print(f处理 {filename}: 实际尺寸 {img_w}x{img_h}) yolo_lines [] for obj in root.findall(object): name obj.findtext(name).strip() # 跳过难例或按需求保留 if obj.findtext(difficult) 1: continue if name not in class_names: print(f未定义类别: {name}, 跳过) continue class_id class_names.index(name) bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) # 归一化并转成中心点宽高 xmin max(0.0, min(xmin, img_w - 1)) xmax max(0.0, min(xmax, img_w - 1)) ymin max(0.0, min(ymin, img_h - 1)) ymax max(0.0, min(ymax, img_h - 1)) if xmax - xmin 1 or ymax - ymin 1: print(f警告: {filename} 中有过小框, 已跳过) continue center_x ((xmin xmax) / 2.0) / img_w center_y ((ymin ymax) / 2.0) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h # 防止浮点归一化后边界刚好等于1导致训练报错 center_x min(center_x, 1.0 - 1e-6) center_y min(center_y, 1.0 - 1e-6) yolo_lines.append(f{class_id} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}) out_path out_dir / (xml_path.stem .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(yolo_lines)) print(f已转换: {xml_path.stem}.xml ({len(yolo_lines)} 个目标))这段代码的逻辑很直白先扫描全部XML生成类别表再逐图读取真实宽高、遍历每个object做坐标归一化。几个值得说明的参数和细节为什么用PIL读真实尺寸而不是XML里的size因为很多人对图片做过缩放后忘了同步修改XML。如果XML里写的是1920x1080实际图片是960x540那你转换出来的YOLO坐标全部偏大一倍训练时YOLO读到的框是越界的会被训练逻辑强制裁剪导致损失震荡。为什么要对坐标做min/max限制标注框超出图片边界太常见了尤其是手工标注时鼠标一滑就出去了。YOLO的训练代码对越界框的处理在不同版本里不一样统一在转换时收敛到图片边界内后面训练能少折腾很多。为什么跳过difficult1的样本这类对象通常是严重遮挡、模糊或者小到人眼都难辨别的YOLO默认训练不处理它们。反正你是在做药盒检测被手挡住一半的盒子注定不是主要目标留着还干扰损失计算。3.3 三个坑difficult、越界坐标、归一化的除法第一个坑是difficult误处理。有的VOC数据集里难例被标成了occluded1或truncated1而不是difficult1。在决定“跳过哪些框”之前建议把每个object的所有子标签都打印出来看一眼。我就是因为偷懒没看把一批被遮挡的盒子和正常盒子混在一起训练模型学出来的框经常框住半个手。第二个坑是坐标是浮点还是整数。VOC的标准定义里xmin这类值是整数像素但有些标注工具导出的是浮点。如果你用int()强转会丢精度框大一号小一号差别不大反而要在脚本里兼容字符串转float再取整避免字符串前后空格导致的解析错误。第三个坑是归一化时的除零。图片本身不会宽高为0但有些图片被错误地存成0字节文件PIL读不进去脚本直接抛异常。解决办法是在打开图片那一步加异常捕获失败时把文件名打印出来把图片从数据集里剔除而不是让整个转换中断。4. 训练前先体检372张数据的类别分布与标签质量4.1 用统计脚本找出缺标注、错类别和不平衡转换完成后直接丢进训练大概率能跑但效果好不好就看你有没有做数据体检。372张图片说多不多说少不少里面有几张标签为空是常有的事。空标签图片要么删掉要么保留作为背景图如果不处理模型会把无目标的图片也当作负样本少量还好多了会影响收敛。from pathlib import Path from collections import Counter labels_dir Path(dataset_raw/labels_converted) class_names Path(classes.txt).read_text(encodingutf-8).splitlines() stats Counter() empty_images [] box_sizes [] for txt_path in labels_dir.glob(*.txt): lines [line.strip() for line in txt_path.read_text(encodingutf-8).splitlines() if line.strip()] if not lines: empty_images.append(txt_path.name) continue for line in lines: parts line.split() class_id int(parts[0]) w float(parts[3]) h float(parts[4]) if class_id len(class_names): print(f类别id越界: {txt_path.name} 中的 {class_id} 超出 {class_names}) continue stats[class_names[class_id]] 1 box_sizes.append((w, h)) print(空标签图片数量:, len(empty_images)) print(各类别目标数量:, dict(stats)) if box_sizes: avg_w sum(w for w, h in box_sizes) / len(box_sizes) avg_h sum(h for w, h in box_sizes) / len(box_sizes) tiny sum(1 for w, h in box_sizes if w * h 0.01) print(f平均框宽比: {avg_w:.3f}, 平均框高比: {avg_h:.3f}) print(f小目标(面积1%图片)数量: {tiny})这段脚本从转换生成的YOLO标签读取统计核心看三个信息类别分布是否失衡、有无空标签图片、目标尺寸是否偏小。参数说明阈值0.01表示框面积不足图片总面积的1%对药盒场景来说如果大量目标小于这个值训练时YOLO固定网格可能照顾不到。框中w和h都是归一化后的值乘上图片宽高就能换算成像素。统计完我一般还会抽查20张图片把标注框画出来肉眼看一遍。这一步没有代码能替代用的就是OpenCV画矩形截图拼墙看。我见过最离谱的情况是统计里一个类别有200多个框画出来发现其中一半框的是收银台而不是药盒——标注员把“感冒灵”旁边的二维码也框进去了。这种错误不人工看很难发现。4.2 小数据集怎么扩影像翻转与Mosaic的落地参数372张原始图即使全部有效也不够训练一个稳定的药盒检测器。在做任何数据扩充之前先把训练集、验证集、测试集固定下来再只对训练集做增强。YOLO框架内置的Mosaic、随机仿射、HSV扰动都直接吃配置文件参数不需要写额外代码。我用Ultralytics YOLO训练这类小数据集时data.yaml会这样写path: D:/datasets/cold_med train: images/train val: images/val names: 0: ganmaoling_particle_box 1: ganmaoling_capsule_box 2: other_cold_med这里的names顺序必须和classes.txt一致不能自己看着顺眼就重排。训练命令里我会打开Mosaic和翻转但要限制增强强度防止小数据集被增强成“变形金刚”。yolo train taskdetect \ modelyolov8n.pt \ datacold_med.yaml \ epochs150 \ imgsz640 \ batch16 \ mosaic0.8 \ fliplr0.5 \ flipud0.1 \ hsv_h0.015 \ hsv_s0.4 \ hsv_v0.3 \ patience30 \ projectruns/train_cold_med参数说明mosaic0.8表示80%的批次样本会用Mosaic拼接四张图它能在小数据集上显著提升模型对药盒在货架复杂背景下的鲁棒性但开太高会让小目标变形严重hsv_h0.015控制色相偏移幅度药盒包装颜色是重要特征药店灯光通常偏暖色相偏移过大会把红黄包装学成别的颜色batch16在372张图片规模下已经足够显存不够就降到8优先保证每个epoch能完整看到所有样本。4.3 训练参数怎么设epochs、lr0与weight decay的取舍小数据集最怕过拟合典型现象是训练loss降到0.02、验证集mAP却卡在0.5不动。我一般把epochs设在150左右配合patience30早停lr0从0.01开始配合余弦退火到0.001。377张图150轮等价于普通大数据的50轮效果因为每轮重复看图很多次太高的学习率会在后期反复震荡。关于预训练权重我建议用yolov8n.pt而不是yolov8s.pt。理由很实际372张这种规模模型容量越大越容易把药盒背景纹理也背下来小模型反而有更强的泛化先验。如果你的最终部署设备算力足够先把nano跑通验证效果再换small重训一轮不迟。如果你用的是YOLOv5系列注意hyp.yaml里的anchor设置。药盒的形状比例相对固定长宽比集中在1:1.5到1:2.5之间默认COCO anchor里有很多针对人的瘦长框对药盒不太友好。常见做法是关掉autoanchor的自动重算显式把anchor改成更贴近药盒的一组数值。这个属于进阶调优第一轮训练可以先不管。5. 常见问题排查药盒小目标、漏检和过拟合的3类“翻车”5.1 现象检测框全部偏向图片中央药盒在角落就漏检我在这类小数据集上翻过几次车第一次就是这症状。训练完在验证集上跑发现凡是在图片中间的药盒都能框出来一到货架边角就丢。原因有两个一是372张训练图里目标多数位于图片中心附近模型学到的位置先验就是中间二是Mosaic增强默认会从四张图里各取一块拼图拼接时原图边界被裁掉角落的药盒变成残缺目标被当作背景样本。解决思路分两步。先调整数据划分不要把货架全景图全部放进训练集留出若干张角落有药盒的图作为验证集然后在训练时把mosaic0.8降到mosaic0.5给原始完整图片更多曝光机会。如果条件允许对训练集每张图做一次随机平移扩充人为把目标挪到边缘。5.2 现象训练loss下降但验证mAP不涨这个小数据集过拟合的经典信号。前面统计脚本看到类别不平衡时就要警惕。假如ganmaoling_particle_box有200个框other_cold_med只有20个框模型为了降低整体损失会倾向于把所有预测都压向大类别小类别直接放弃。验证时即使框对了也因置信度阈值被过滤掉。解决方法是类别加权损失或者在训练时做“小类别复制增强”。常见做法是把含小类别的图片复制三份每份做不同的色域和仿射扰动再混入训练集。注意不要直接用repeat_dataset之类的粗暴复制那会导致模型把小类别的背景纹理也记住验证集一换场景就原形毕露。另外检查一下验证集的标注质量。我遇到过mAP不涨是因为验证集里有一批XML的xmax比图片真实宽度还大转换脚本做了裁剪但裁剪后的框和原标注差了几个像素人眼看不出mAP计算却很敏感。用第4章的统计脚本把验证集也过一遍确认没有越界框再谈调参。5.3 现象VOC和YOLO混用导致类别错位这是这个数据集最典型、也最隐蔽的坑。训练代码读的是labels_converted但调试时用LabelImg打开看的是Annotations两者类别顺序不一样你肉眼看没问题模型输出的类别id是错的。我曾经在一个药盒检测项目里把两个来源的数据集合并没检查各自的classes.txtA数据集的“感冒灵颗粒”是id 0B数据集是id 1合并后出现同一个类两个id训练完预测结果时好时坏。排查办法是把所有txt文件第一列数字统计出来对照classes.txt手动确认每个id对应的类别名。养成习惯任何一次数据集合并或格式转换后必须重新生成classes.txt并且用第4章的统计脚本校正。5.4 现象解压报错、文件乱码、缺图缺标注这类问题发生在拿到zip的第一步。如果你用unzip解压时看到cannot create directory先检查压缩包里有没有中文文件名没有乱码也可能会遇到“文件被加密”的提示。加密zip通常是发布方设置的正常做法是回到资源页找密码说明不要试图用破解工具也不要把时间浪费在暴力破解上。更常遇到的是zip本身残缺下载到一半中断了。这时用Python检查压缩包完整性import zipfile path 药品感冒药999感冒灵检测数据集372张VOCYOLO格式.zip with zipfile.ZipFile(path, r) as zf: bad zf.testzip() if bad is None: print(压缩包完整性校验通过) else: print(f损坏文件: {bad})testzip()会逐个解压缩内部文件并计算CRC校验返回第一个损坏文件的名字。如果返回不是None建议重新下载。别指望部分解压能凑合用VOC和YOLO标签文件只要缺一个训练时那一整张图就成了无声的噪声。重新下载后记得再跑一次校验我吃过亏校验通过前绝不进入下一步。6. 收尾技巧用冻结训练和伪标注把372张数据变成可用基线先说怎么验证当前模型是否真的达标。不要只盯着整体mAP把验证集按难度分成三组正常摆放的药盒、半遮挡的药盒、逆光或模糊的药盒分别跑F1和mAP。如果正常组mAP在0.85以上、困难组在0.55以下说明模型对场景敏感而不是对物体敏感这时优先补数据而不是调参数。我习惯的做法是“冻结训练伪标注”两步走。在372张上完成第一轮训练后用模型去预测一批未标注的药店实拍图或网上公开的商品图把置信度在0.5以上的框自动生成候选label再用标注工具人工修正错框和漏框。这一步能把数据量从372张扩到600-800张质量可控。先冻结backbone再只训练head防止模型在新样本上灾难性遗忘yolo train taskdetect \ modelruns/train_cold_med/weights/best.pt \ datacold_med_v2.yaml \ epochs60 \ imgsz640 \ batch16 \ freeze10 \ lr00.005参数说明freeze10表示冻结模型前10层参数只更新后面的检测头lr0降到0.005是为了在已有权重上做平滑微调避免新数据把旧特征冲掉。这一步跑完再解冻全部层用更低学习率 0.002 精调20轮最终得到的基线基本能扛住实拍场景的七成需求。372张确实不多但它足够帮你把整个目标检测链路走通从zip解压、VOC转YOLO、标签体检、训练调参到伪标注扩展。你真正要花时间的不是跑训练而是让每张图的标签干净且一致。我做药盒识别项目时吃过一次亏觉得标完就万事大吉结果一到货架实拍夜光场景就漏检后来养成了“先统计后画框再训练”的习惯每次换数据集都这么过一遍。希望帮到你。本文还有配套的精品资源点击获取