简介面向计算机视觉目标检测入门与实战的药品检测数据集围绕999感冒灵包装识别整理适合用于药品检测、零售货架识别等任务场景。压缩包共1119个文件以jpg图片、VOC格式xml标注和YOLO格式txt标注为主包含372张图片整体大小仅20.88MB便于快速下载与实验。数据集中仅设置999ganmaoling一个目标类别共计573个标注框所有标注均由labelImg工具完成类别定义明确标注文件与图片一一对应组织规整可直接用于YOLO系列、Faster R-CNN等主流目标检测框架的训练和验证。对需要规范双格式数据集或希望省去标注环节的开发者来说这份数据能节省大量数据准备时间免去格式转换与标签整理的麻烦可作为药品识别项目初期的模型训练数据也适合作为目标检测入门练习的陪练样例。目前已有248人学习/下载整体内容聚焦、体积轻量便于直接上手机器学习流程。1. 拿到 372 张 999 感冒灵标注图先别急着开训先把数据集的“脾气”摸清楚目标检测数据集是这个领域最容易被低估的坑手里有图片容易有标注容易但图片和标注能不能直接被 YOLO 类模型消费是另一回事。标题里这份药品感冒药999感冒灵检测数据集一共 372 张属于典型的少样本数据集压缩包里通常同时带着 VOC 的 xml 和 YOLO 的 txt 两种格式。接下来我会顺着这个 zip 讲一套可复制的落地路径拆包体检、看懂两种格式、整理成 YOLOv8 训练目录并跑通再列出药品检测场景里最容易翻车的几个隐蔽问题。适合刚入门目标检测的算法同学也适合正在做药房货架识别、药品包装质检的工程团队参考。2. 拆开 zip 先校验文件完备性VOC 的 XML 与 YOLO 的 txt 为什么能同时存在很多新手拿到这类数据集会问VOC 和 YOLO 到底用哪个答案是两个都有用。VOC 格式把每个目标写进一份 XML坐标是像素单位的左上角和右下角YOLO 格式把每个目标写成 txt 里的一行坐标是归一化后的中心点和宽高。两份文件描述的是同一批标注但落到不同训练工具里消费方式完全不同。成熟的做法是保留 xml 作为原始存档训练时只喂 YOLO 的 txt。这一章先把两种格式的物理结构拆开不然后面坐标错乱时你根本不知道去哪里查。2.1 用 linux 的 unzip 做文件体检jpg/xml/txt 三件套缺一不可第一次拿到 zip我不会急着打开 xml而是先做文件级清点。服务器上基本都是 Linux 环境用 unzip 加 find 就能把家底摸清楚mkdir -p 999_data unzip -q 【目标检测数据集】药品感冒药999感冒灵检测数据集372张VOCYOLO格式.zip -d 999_data cd 999_data find . -name *.jpg | wc -l find . -name *.xml | wc -l find . -name *.txt | wc -l参数说明unzip 的-q是安静模式解压时不刷屏-d指定解压目录避免所有文件摊在当前目录。三个 find 分别统计图片、VOC 标注、YOLO 标注的数量。标题写的是 372 张如果三个数字都接近 372说明压缩包基本完整如果 jpg 是 372 而 txt 只有 369后面训练时 dataloader 一定会报“找不到标签”。接下来核对文件名是否对齐把扩展名去掉再对图片名列表和标注名列表做差集ls *.jpg | sed s/\.jpg$// | sort img_names.txt ls *.xml | sed s/\.xml$// | sort xml_names.txt # 有图但没有 xml 的文件 comm -23 img_names.txt xml_names.txt # 有 xml 但没有图的文件 comm -13 img_names.txt xml_names.txt参数说明sed s/\.jpg$//把 jpg 后缀去掉sort 排序后才能交给 comm 做逐行比较。comm -23只显示第一个文件里有、第二个文件里没有的行正好对应“有图无标注”comm -13反过来显示“有标注无图”。对 YOLO 训练来说真正需要的是 jpg 和 txtxml 是原始备份但缺了它以后重新生成 txt 或换格式就少了一条路。所以我清点时总是三份文件一起检查而不是只数 jpg。顺带提醒一句解压后如果文件名带中文、空格或括号直接复制文件名到 shell 里很容易触发转义问题。标题里的压缩包名就有中文方括号。我的习惯是先解压再把这批文件重命名成纯英文编号比如img_0001.jpg这种风格后面写训练脚本、批量处理都能少踩很多坑。2.2 看懂 VOC 的 XML像素坐标、类别名和容易藏脏数据的地方VOC 的 XML 最早来自 Pascal VOC 数据集后来成了“人眼能直接看懂”的标注标准。一个object节点代表图里的一个目标里面包含类别名name和检测框bndbox。下面用 Python 的 xml 库把单张标注读出来import xml.etree.ElementTree as ET tree ET.parse(0001.xml) root tree.getroot() size root.find(size) print(图片尺寸:, size.findtext(width), size.findtext(height)) for obj in root.iter(object): name obj.findtext(name) box obj.find(bndbox) xmin int(box.findtext(xmin)) ymin int(box.findtext(ymin)) xmax int(box.findtext(xmax)) ymax int(box.findtext(ymax)) print(name, (xmin, ymin), (xmax, ymax))逻辑说明root.iter(object)会遍历所有目标节点比findall更宽容。size节点记录原始图宽高是后面转 YOLO 时的分母很多 xml 转 txt 脚本报“宽高为 0”就是因为读漏了这个节点。参数说明findtext在标签缺失时返回 None打印出来能直接看到不要用[0].text那种写法遇到空节点抛异常很烦。这类药品数据集的真实麻烦在于类别名不干净。同一盒 999 感冒灵在 xml 里可能被标成999、ganmaoling、999感冒灵三种名字转成 YOLO 后类别数就会多出两三倍。检查技巧很简单把全部 xml 的 name 收进一个集合看一眼就能发现不统一的类别名。import glob names set() for f in glob.glob(*.xml): root ET.parse(f).getroot() for obj in root.iter(object): names.add(obj.findtext(name)) print(names)这段脚本遍历所有 xml把类别名收集到集合里。如果打印出来是{999, ganmaoling, 999感冒灵}后续就必须做类别纠偏否则 txt 里类别索引会错乱。我拿到别人的数据集时绝不会跳过这一步因为它直接决定 data.yaml 里的 names 怎么写。很多人训练时发现类别数翻倍十有八九是这个环节出了问题。2.3 把 YOLO 的 txt 还原成像素框坐标换算、越界检查和最实用的可视化YOLO 的 txt 每一行是五个数字类别索引、归一化中心 x、归一化中心 y、归一化宽、归一化高。没有任何一项是像素值。归一化指的是除以图片宽或高所以还原公式就是反过来乘回去。直接写代码验证import cv2 img cv2.imread(0001.jpg) h, w img.shape[:2] with open(0001.txt, r) as f: for line in f: line line.strip() if not line: continue cls_id, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(int(cls_id)), (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(check_0001.jpg, img)逻辑说明先取图片高宽再解析五元组cx、cy是中心点bw、bh是宽高比例。还原成左上角和右下角后直接画框保存。最容易写错的是bw / 2这一项如果写成bw * w / 2会重复缩放cls_id从 float 转 int 时也要显式转换防止后续画图时把浮点类索引传给 OpenCV 导致参数类型报错。顺手做一次越界检查把 txt 里坐标不在 [0,1] 区间的行找出来import glob for f in glob.glob(*.txt): with open(f) as fh: for idx, line in enumerate(fh, 1): parts line.split() if len(parts) ! 5: print(f, idx, 字段数不对) continue cls_id, cx, cy, bw, bh map(float, parts) if cx 0 or cx 1 or cy 0 or cy 1 or bw 1 or bh 1: print(f, idx, 坐标越界, cls_id, cx, cy, bw, bh)参数说明enumerate(fh, 1)从第 1 行开始编号方便定位。大于 1 的 bw 表示框比图还宽通常是标注边缘框后没有裁切这种框在训练早期不会报错但会让模型学到偏宽的形状。我一般会让脚本记录而不是直接删等看完所有越界行的分布后再统一处理比在循环里直接改数据更安全。VOC 和 YOLO 的换算关系用一张表收着后面所有转换脚本都对照它| 对比项 | VOC XML | YOLO txt | | 坐标单位 | 像素 | 归一化比例 | | 检测框表示 | xmin, ymin, xmax, ymax | cx, cy, w, h | | 类别表示 | name 字符串 | 从 0 开始的整数 | | 单个目标 | 一个 object 节点 | 一行五个数字 | | 空图片 | 没有 object 节点 | 空 txt 或没有 txt |看懂这张表就明白为什么很多工具要写转换脚本而不是手工维护两份文件。只要原始 xml 是干净的txt 随时可以重新生成反过来只有 txt 丢了 xml再想找回原始坐标就非常麻烦。所以我在任何项目里都要求原始标注物完整存档这个习惯帮我躲过不少后续返工的坑。3. 用这份数据集跑通 YOLOv8目录结构、data.yaml 与最值得调的三个参数zip 里的文件通常是平铺的不能直接扔给 YOLO 训练。YOLO 类工具要求图片和标签分别放在 images 和 labels 两棵平行目录下并且 train 和 val 分开。下面这节把整理、配置和训练命令一次说清。3.1 划分 train/val使用复制而不是移动保留 xml 原始存档先建一个独立的训练目录不要改动原始 zip 解压出来的内容。import os import random import shutil random.seed(42) src 999_data dst 999_yolo for split in (train, val): os.makedirs(os.path.join(dst, images, split), exist_okTrue) os.makedirs(os.path.join(dst, labels, split), exist_okTrue) # 只保留 jpg 和 txt 都存在的样本 names [ f[:-4] for f in os.listdir(src) if f.endswith(.jpg) and os.path.exists(os.path.join(src, f[:-4] .txt)) ] print(可用于训练的样本数:, len(names)) random.shuffle(names) val_n max(1, round(len(names) * 0.2)) for i, name in enumerate(names): img_src os.path.join(src, name .jpg) txt_src os.path.join(src, name .txt) if i val_n: shutil.copy(img_src, os.path.join(dst, images, val, name .jpg)) shutil.copy(txt_src, os.path.join(dst, labels, val, name .txt)) else: shutil.copy(img_src, os.path.join(dst, images, train, name .jpg)) shutil.copy(txt_src, os.path.join(dst, labels, train, name .txt))逻辑说明列表推导式筛掉 jpg 和 txt 对不上的图片缺失标签的直接不进训练目录。按 8:2 随机划分训练集和验证集val_n取max(1, ...)防止样本极少时验证集为空。random.seed(42)保证每次重跑结果一致。使用shutil.copy而不是os.rename是为了让原始 zip 解压目录保持完整后面从 xml 重新生成 txt 时照片还在。如果你不想给磁盘留两份数据也可以在确认不会再改标签后改成 move但我建议保留。实际划分时更大的坑是类别分布。372 张图里如果“999 感冒灵盒装”占 300 张“冲剂”只占 72 张纯随机划分几乎必然导致小类在验证集里只剩几张。少样本项目里我一般会做最简单的分层抽样先读取每张图片 txt 的第一列把图片按类别分组再从每组内按比例抽 20% 到 val。这样能保证验证集里每个类别都有一定数量后面的单类 AP 才有参考价值。3.2 编写 data.yaml绝对路径、类别顺序和 nc 三者必须自洽YOLOv8 用 data.yaml 告诉训练器两个关键信息数据放哪里类别叫什么。一份最小配置如下# 训练前改成你本机的绝对路径 path: /home/alg/999_det/999_yolo train: images/train val: images/val nc: 2 names: 0: ganziling_box 1: ganziling_pack参数说明path是数据集根目录后面 train、val 都是相对路径nc是类别数必须等于 names 列表的长度。很多新人把path写成相对路径训练命令一旦换了工作目录立刻找不到图片。还有把 names 顺序写反的原 txt 里 0 代表盒装配置里 0 却写了冲剂模型就会一路错下去不自知。写完后我会立刻跑一段核对脚本检查 txt 里实际出现的类别编号和配置是否一致python - PY import yaml, glob cfg yaml.safe_load(open(data.yaml)) print(names:, cfg[names]) print(nc:, cfg[nc]) ids set() for f in glob.glob(999_yolo/labels/*/*.txt): for line in open(f): ids.add(int(line.split()[0])) print(txt 中出现的类别索引:, sorted(ids)) PY参数说明glob.glob(999_yolo/labels/*/*.txt)同时覆盖 train 和 val 两个子目录。打印出的 ids 如果是[0, 1]配置自洽如果是[0, 2]说明某个 txt 写了越界类 id训练时不报错但网络输出头永远不知道第 2 类是什么。遇到这种情况把对应文件打印出来挨个查通常又是 2.2 节说的类别名不统一导致的。这段小脚本每次换数据集我都会先跑一遍。3.3 迁移学习训练预训练权重、batch、imgsz 和 loss 曲线怎么看小样本数据集最大的救星是迁移学习。加载预训练权重后前几层直接复用在大规模数据上学到的边缘和纹理特征模型只需要学会“感冒灵包装长什么样”这种更高层的语义特征。我一般用这份命令起步yolo detect train \ modelyolov8n.pt \ datadata.yaml \ epochs150 \ imgsz640 \ batch16 \ lr00.001 \ patience30 \ cacheTrue \ device0参数说明modelyolov8n.pt是 n 型预训练权重体积小适合 372 张这种数据量epochs150看着多配合patience30的早停机制通常五六十轮就停了imgsz640是输入边长显存够用我会直接改 960对药品包装上的小字识别有可见提升batch16在普通单卡上能跑显存不足先降到 8并把 lr0 同步减半cacheTrue把解码后的图片缓存到内存372 张图占用不大能省不少数据读取时间。训练日志里同时打印box_loss、cls_loss、dfl_loss三列它们合起来才是完整的 yolo 损失函数。很多人只盯 box_loss其实分类任务里应该重点看 cls_loss。药品检测里如果 cls_loss 一直在降而 box_loss 起伏很大多半是同类目标尺寸跨度太大近景盒子和远处小盒差了十几倍。看到这种情况我不会急着调 loss 权重而是先把预测图拉出来看确认是不是缺小目标样本。常用超参数我用一张小表记录不必每次都翻文档| 参数 | 372 张的起步值 | 调整方向 | | imgsz | 640 或 960 | 小目标多就提高显存受限降到 512 | | batch | 16 | 显存不够用 8并同步降低 lr0 | | epochs | 150 | 配合早停多给不亏 | | lr0 | 0.001 | 数据越少学习率越低防止过拟合 | | cache | True | 小数据集开缓存省时间 |注意yolo命令来自 ultralytics 的 CLI不同小版本对数据增强的默认值略有差异。我的习惯是先只跑 5 个 epoch 确认流程没报错再挂长训免得睡醒发现训练在第二轮就中断。4. 药品检测数据集的五个避坑记录现象、原因、解决都是实际踩过的372 张的体量里最大成本是数据校验不是训练。下面这五个问题我在类似的检测数据集上都见过按现象、原因、解决的顺序写方便你对照排查。4.1 翻车一训练一切正常预测时框全偏根子在坐标解释现象验证集指标正常用同一个 best.pt 去预测新图片目标框整体往左上角偏框尺寸也不对。这不是模型问题是标注数据的问题。原因很多转换脚本把 VOC 的 xmin、ymin、xmax、ymax 做了“归一化”却仍然按 xmin、ymin、xmax、ymax 的顺序写进 txt而没有换算成 YOLO 约定的 cx、cy、w、h。按 YOLO 格式去读中心点自然错位画出来的框就全偏了。解决训练前把 txt 还原成像素框画在原图上人工看 5 张就知道对不对。我会直接读一个 txt 的第一行打印 cx、cy、w、h然后判断 0.5 这个数量级更接近中心点还是左上角。坐标系的错乱用眼睛看比任何日志都快不要让模型这个黑匣子替你背锅。我的经验是一张 640 宽的图上第一个框 cx 小于 0.1基本可以断定它实际存的是 xmin。4.2 翻车二类别索引跳号names 列表和 txt 里的整数对不上现象训练不报错loss 也正常下降但推理时模型总把 A 类感冒灵识别成 B 类过两个 epoch 又可能换回来像抽风一样。原因xml 里的类别名不统一某个转换脚本用了sorted(set(names))自动生成类别映射导致 txt 里的 class id 和我手写的 data.yaml 顺序对不上。常见的情况是“999”“ganmaoling”“999感冒灵”被当作三个类实际上它们是一类。解决类别映射必须硬编码不能动态生成。我给项目加了一个 class_map.json把类别与编号的对应关系写死import json name2id { 999感冒灵盒装: 0, 999感冒灵颗粒: 1, } with open(class_map.json, w, encodingutf-8) as f: json.dump(name2id, f, ensure_asciiFalse, indent2)参数说明ensure_asciiFalse让 json 里不写中文转义符方便人工核对。之后任何 xml 转 txt 的脚本都统一读取这份映射不允许自己推断。另一个补救办法是反向建立 id2name 对照表把现有 txt 的类 id 和 xml 的原始 name 对应起来但那样要逐文件排查非常麻烦。所以我在拿到数据的第一天就做类别清单之后所有标签处理流程都从这张表取数。4.3 翻车三小目标漏检远距离小盒在特征图上只剩一个点现象验证集 mAP 不低但药房货架的真实图片里上层远处的小盒包装几乎全漏漏掉的恰好是业务最关心的那部分。原因372 张训练图里大部分是近景正面照目标高度能占到图片的三分之一。输入缩放到 640 之后远处小盒宽可能只剩 20 像素再经过 YOLO 的多次下采样网络最后用于检测的特征层里小盒可能只占据一个格点。这是小目标检测最常见的问题不是结构不行是训练数据里“足够小的目标”太少。解决三个措施并行。第一把 imgsz 提到 960 或 1280让小目标在输入里占更多像素第二用当前模型给现场照片跑一次低置信度预测把所有候选框哪怕 conf 只有 0.1交给人工校对然后把难样本回灌到训练集第三把数据增强里的 hsv、旋转参数加一点幅度增强对暗光和拍摄角度的鲁棒性。但这没有玄学可言最终瓶颈还是数据分布如果 372 张里完全没有远视角再多增强也补不出新视角。我后来是专门补拍了两个远距离角度漏检率才真正降下来。4.4 翻车四标签文件缺失或为空dataloader 在训练中途崩现象训练到某个 epoch 突然报 FileNotFoundError进程退出重新启动跑到差不多同样的位置又崩。原因某些图片没有对应 txt某些 txt 是 0 字节。dataloader 默认按图片文件名的前缀到 labels 目录找同名 txt找不到就报错。空 txt 表示“这张图没有目标”是合法的背景样本但如果标签生成脚本把空文件过滤掉原本作为背景的图片就变成“有图无标签”。解决对每个 split 把缺失的标签文件补成空 txtimport os for split in (train, val): img_dir os.path.join(999_yolo, images, split) lab_dir os.path.join(999_yolo, labels, split) missing 0 for img_name in os.listdir(img_dir): stem os.path.splitext(img_name)[0] lab_file os.path.join(lab_dir, stem .txt) if not os.path.exists(lab_file): open(lab_file, w).close() missing 1 print(split, 补空标签数:, missing)逻辑说明遍历 images 下所有图片只要是 labels 下没有同名 txt 的就创建一个空文件。补空文件不等于删样本模型需要真实的背景图来降低误检尤其是药房货架上大量没有目标的角落照片。最后我会再统计一次空 txt 的占比如果超过 20%就要回头查原始 xml 是不是在转换环节大面积丢失标签而不是简单补空文件蒙混过关。所谓后悔药就是在排查阶段保留原始 zip随时能用 xml 重新生成一遍 txt。4.5 翻车五验证集和训练集同源mAP 高分是“背题”来的现象验证集 mAP50 到 0.95信心满满拿去另一个门店测试精确率掉到 0.6 以下。这类数据集大多来自同一拍摄批次同一个药盒在连续几张照片里反复出现随机划分把同一条目标同时分进 train 和 val验证集等于在背题。原因372 张图经常由几次连续拍摄组成连续帧高度相似。全量随机划分之后模型在训练时已经见过目标的正样本验证时再看到同一目标的另一个角度等于开卷考试。解决按场景划分而不是按图片划分。如果 zip 里的文件名带拍摄批次或目录编号优先按批次分如果完全是平铺命名就用感知哈希做近似重复聚合。先把每张图缩到 8x8 灰度计算两两平均差异距离小于阈值的归为一组整组进同一个集合。这样验证集衡量的是模型面对没见过的近似场景的能力而不是对同一串照片的背题能力。遇到这种情况我不会去改随机种子而是先做去重分组再决定参数。这也是少样本验收里最容易被忽略的一步。5. 把 372 张的余热用到极致伪标签回灌、单类 AP 和验收习惯5.1 用伪标签把现场照片低成本变成增广数据少样本数据集中最贵的是标注人工不是训练时间。训练出一个能用的 best.pt 后我会直接把模型拿到新门店的照片上跑一次低置信度预测拿到候选框之后人工校对比从头画框快一半以上。yolo detect predict modelruns/detect/train/weights/best.pt \ source./new_shop_photos/ \ conf0.1 \ save_txtTrue \ save_confTrue \ project./pseudo_labels参数说明conf0.1是为了多召回候选框低置信度框宁可多给漏检的框人工再补save_txtTrue让结果直接变成 YOLO 格式 txtsave_confTrue把置信度一起写进去人工筛选时优先看低分框。回灌到训练集之前要按 class_map.json 统一类别编号并手动删掉非目标框。这一步做完原本只有 372 张的数据集能快速扩到 500 到 700 张且分布更接近现场。5.2 验收不要只看 mAP50单类 AP 和漏检率曲线才是药品场景的生死线372 张图里如果某一类占大头总 mAP 很容易被拉高。我会用下面的命令单独做一次验证然后去结果目录翻混淆矩阵yolo detect val modelruns/detect/train/weights/best.pt datadata.yaml在runs/detect/val目录下confusion_matrix.png和results.csv是重点。如果第二类的 recall 低于 0.5首先回看该类别样本数是不是少于 30 张。样本不够时直接补拍比调参更有效。做测试时我习惯把置信度从 0.5 一路扫到 0.1记录漏检率变化找到业务能接受的“准召平衡点”再用这个阈值上线。mAP50 适合排行榜对比不适合药房货架的漏检要求。最后讲一个习惯小数据集最容易翻车的动作是一上来就开大模型、同时调五六个增强参数。我这么干过一次损失曲线直接没法看。后来收敛成固定流程——先冻结主干只训练检测头跑 30 轮确认标签和流程没问题再放开主干做完整微调。少样本条件下把数据格式校验和数据集分布管理做扎实比多调十个超参数更有价值。希望帮到你。本文还有配套的精品资源点击获取
