简介面向目标检测与药品识别需求这份感冒药品分类检测数据集提供九百六十张真实场景图片涵盖999感冒灵、板蓝根、布洛芬等常见药品类别适用于药品检测、货架管理、智能问药等场景的模型训练与算法验证。数据采用Pascal VOC与YOLO两种主流标注格式每张图片均对应一个xml文件与一个txt文件由labelImg工具手动标注矩形框共标注一千三百六十五个目标框类别定义清晰、边界准确可帮助入门者快速理解目标检测标注规范。资源包包含两千个文件主要有jpg图片、xml标注文件和txt标签文件压缩后约四十四MB文件命名规范、目录结构简洁解压后即可直接接入常见检测框架使用。该数据集迄今已被三百九十四人学习下载对于需要标准药品数据的开发者而言既能免除自行采集和标注的繁琐也能用作课程设计、毕业设计或算法对比的基准数据。各类别框数分布存在差异恰好可用于练习处理类别不平衡问题整体是一份实用且合格的公开数据材料。1. 为什么药品外观识别要先有一份双标注数据集家里备的感冒药一旦混装最可靠的办法不是记药名而是让模型在画面里先找到药盒再分类。这份感冒药品分类检测数据集提供 960 张真实 JPG 图像每张图同时带 labelImg 导出的 Pascal VOC XML 和 YOLO TXT 双格式标注覆盖 999ganmaoling、banlangen、buluofen 三个常见类别另含一个只有 1 个框的 drugs 杂类。它解决的是目标检测落地中最基础的资源问题当你需要快速验证 YOLO 训练流程、测试小样本类别不平衡场景或做数据增强实验时不用再花几天去画框。适合想跑通从标注到训练全流程的工程师也适合拿真实分布做算法对比的研究生。要注意的是这种双格式包并不万能直接拿去训练前必须理解两种坐标系和目录规则否则后面每一步都在填坑。2. 解包后先读目录VOC和YOLO标注的真实差异2.1 压缩包内文件构成与计数核对解压后不要急着打开图片先用命令统计一下后缀分布。很多网盘下载的数据集会告诉你“959张3类别”但解压出来可能是960张、4类这不是资源方少发而是打包时把一张重复图剔掉后又重新生成了一次标注。所以第一件事是清点文件数。unzip 感冒药品分类检测数据集959张3类别VOCYOLO格式.zip -d medicine_ds cd medicine_ds find . -type f | sed s/.*\.// | sort | uniq -c find . -name *.jpg | wc -l find . -name *.xml | wc -l find . -name *.txt | wc -l第一个命令统计当前目录下所有文件的后缀种类和数量后面三个分别核对 jpg、xml、txt 的数量。正常情况下三者数量一致都是960。这里有个容易忽略的点YOLO 的 txt 文件名必须和 jpg 完全同名只是扩展名不同否则 YOLO 训练时会报找不到 label。压缩包里没有包含分割路径的 txt因为它只做检测任务不需要实例分割的 polygon 坐标所以每条 txt 都是标准的五个数值给检测模型用之前也不需要额外清洗。如果发现图片和 txt 数量不一致优先检查是否存在大写扩展名如.JPG以及是否有隐藏的重复文件名。数据集的标注质量并不只看框准不准文件命名一致性是第一步这一步错位会在后续训练时变成各种各样的玄学报错。2.2 解析XML标注边界框、类别名与坐标规则labelImg 保存的标准 Pascal VOC XML 里size记录原始图像宽高object记录每个框的类别和 bndbox。画框时用的是矩形框所以 bndbox 内是 xmin、ymin、xmax、ymax 四个像素坐标坐标原点在图像左上角向右向下为正。这个坐标体系与数学里左下角为原点的情况不同转换时容易漏掉正负方向。import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) objects [] for obj in root.findall(object): name obj.find(name).text.strip() box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) objects.append({name: name, bbox: [xmin, ymin, xmax, ymax]}) return w, h, objects这段代码把 XML 中的图像尺寸和每个标注框读成 Python 对象。函数返回的w和h后面转 YOLO 时需要用到objects里 bbox 四元组是按xmin, ymin, xmax, ymax顺序存的不要被其他代码里常见的xywh影响。解析后可以打印出来看看类别名是否规范比如有没有带空格或中文引号这类隐藏字符会让类别映射直接断裂。我按所有 XML 里的name做了累加得到下面这个分布类别名可能对应药品标注框数999ganmaoling999感冒灵颗粒573banlangen板蓝根颗粒134buluofen布洛芬657drugs其他药品杂类1总计-1365这就是标题说“3类别”而实际有4类的原因drugs类只有1个框打包者可能把它当作杂质没算进主类别。但从格式角度它确实出现在 classes 列表里使用时必须决定去留而不是假装它不存在。2.3 YOLO txt标注格式与归一化转换YOLO 的 txt 每一行是class_id cx cy w h其中 cx、cy、w、h 都是相对图像宽高的比例范围通常在 0~1 之间。这是和 VOC 最明显的差异VOC 是绝对像素YOLO 是归一化坐标。解析它比 XML 简单但坑在类别索引。def parse_yolo_txt(txt_path): with open(txt_path) as f: lines [line.strip().split() for line in f if line.strip()] boxes [] for l in lines: cls_id int(l[0]) cx, cy, w, h map(float, l[1:5]) boxes.append((cls_id, cx, cy, w, h)) return boxes这段代码把 txt 的每一行解析成元组第一个元素是类别整数索引后面四个是中心点坐标和宽高。必须注意索引是从 0 开始的也就是999ganmaoling对应 0banlangen对应 1依此类推。如果你手里只有这些 txt 而没有classes.txt很容易把banlangen和buluofen的索引搞混。比较稳妥的做法是先通读所有 txt 文件看 class_id 的最大值是否等于类别数减 1如果出现大于 3 的 id说明标注文件里有脏数据需要先清洗再进训练管线。3. 从双标注到YOLOv8可训练的目录结构与数据集划分3.1 为什么不能直接拿压缩包去训练YOLOv8 的 detect 任务不会自动扫描当前目录下的所有 txt它只认固定的目录约定images/train放图片labels/train放同名 txtval 同理。压缩包里 jpg、xml、txt 混在一个目录直接写数据路径时会报「label not found」或者训练完发现所有框都没参与 loss。另一个原因是 YOLO 需要数据划分通常训练、验证按 8:2 或 9:1而不是把全部数据放 train。验证集的作用是观察每轮 mAP没有验证集的时候模型过拟合也不会知道。所以第一步是重新组织目录datasets/medicine/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/这里 images 和 labels 必须保持同名文件一一对应YOLO 会根据图片路径自动去labels/镜像目录找 txt。目录名不要用中文否则某些版本的 opencv 读图会失败报错信息还不容易看懂。3.2 一键划分并生成YOLO目录既然包里已经有 YOLO 格式 txt就不需要再转一次坐标直接复制并划分即可。下面的脚本按随机种子切分并把 jpg 和 txt 成对写入目标目录import os import random import shutil from glob import glob src_dir medicine_ds dst_dir datasets/medicine val_ratio 0.2 random.seed(42) imgs glob(os.path.join(src_dir, *.jpg)) random.shuffle(imgs) val_n int(len(imgs) * val_ratio) for split, imgs in [(train, imgs[val_n:]), (val, imgs[:val_n])]: for img_path in imgs: stem os.path.splitext(os.path.basename(img_path))[0] txt_path os.path.join(src_dir, stem .txt) if not os.path.exists(txt_path): print(f跳过缺失标签: {stem}) continue for ext, sub in [(.jpg, images), (.txt, labels)]: src os.path.join(src_dir, stem ext) dst os.path.join(dst_dir, sub, split, stem ext) os.makedirs(os.path.dirname(dst), exist_okTrue) shutil.copy(src, dst) print(train , len(imgs) - val_n, val , val_n)这段代码先把所有 jpg 路径打乱再取前val_n张作为验证集其余作为训练集。for split, imgs两层循环把图片和标签分别复制到对应 split 目录os.makedirs会自动创建不存在的目录。打印出的 train/val 数量用于和原始文件数核对比如 960 张图、0.2 比例时 train768val192。如果你的模型在验证集上表现不稳定可以把val_ratio调成 0.15给训练集多留一点样本。注意这段脚本不会处理 XML因为 txt 已经是最终产物。如果你拿到的包只有 XML则需要先用 2.2 节的解析函数做坐标转换再按同样方式写入 labels。这里没有做坐标转换原因是包内 txt 格式正确省去重复工作。3.3 类别映射与 data.yaml 配置YOLOv8 训练时需要一个 data.yaml把类别列表告诉框架。创建datasets/medicine/data.yamlpath: ../datasets/medicine train: images/train val: images/val nc: 4 names: 0: 999ganmaoling 1: banlangen 2: buluofen 3: drugspath是数据集根目录train和val都是相对path的路径。nc必须和names的长度一致这里 4 对应四个类别。如果我想去掉drugs类不能只把 yaml 里的 nc 改成 3还要把 txt 中 class_id3 的行删除并把原 class_id 2/3 重新映射否则类别会错位成buluofen变索引 2drugs变索引 3但 old txt 的1对应 banlangen3被 YOLO 越界忽略。这就是很多人改了 yaml 不删标签后 mAP 反而下降的真实原因。如果最终产品只需要三种感冒药推荐把drugs对应那一张图直接删掉再重新生成 txt。一个只有 1 个框的类在 960 张图里几乎不可能学好还会在计算 loss 时干扰梯度。4. 用YOLOv8训练感冒药检测模型参数设置与痛点4.1 最小可复现训练命令在确认目录结构和 data.yaml 无误后可以直接跑训练。以下命令适用于 ultralytics 的 YOLOv8 命令行接口yolo detect train \ datadatasets/medicine/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ optimizerauto \ seed42 \ projectruns/medicine \ nameexp这里每个参数都有明确作用data指向刚才写的 yamlmodelyolov8n.pt是 COCO 预训练权重n 是 yolov8 系里最小的网络对千张级数据集足够epochs100不一定会跑满因为 ultralytics 默认开了早停imgsz640是训练输入尺寸药品包装多为中近距离拍摄640 能覆盖大多数框batch16在 8G 显存上可以跑。seed42让随机增强可复现方便和别人对比。如果你的显存只有 4G把batch降到 8、imgsz降到 512。如果训练过程中 loss 变成 NaN优先把lr0从 0.01 降到 0.001。这个数据集小网络不需要收敛很久通常 60~80 个 epoch 就已经稳定。4.2 关键参数对收敛的影响参数调整要结合样本量不能照搬 COCO 的大参数。表格里是我在这类小数据集上常用的配置参数推荐值影响踩坑点imgsz416~640输入分辨率决定小目标保留多少细节药盒小就尽量用 640否则容易漏检batch16越大量子化越平滑显存不足时先减 batch不要直接减 imgszpatience50验证集指标不涨时早停的轮数设太小会在 loss 尚未下降时提前终止close_mosaic10最后 10 轮关闭 mosaic 增强不关会导致验证集分布和训练不一致pretrainedyolov8n.pt使用 COCO 预训练权重样本少时必须用预训练冷启动容易过拟合valTrue每轮结束后跑验证集小数据集下验证开销不大建议保持close_mosaic是 ultralytics 提供的参数作用是训练到末尾关掉马赛克增强让模型适应真实背景。常见错误是在 960 张图上直接套用默认值默认可能在最后几轮还在用 mosaic导致验证时框位置对齐混乱。另一个容易忽略的是pretrained如果换成yolov8s.pt训练时间会翻倍但精度并不会线性提升反而可能因为模型容量过大在验证集上过拟合。4.3 类别不平衡和背景误检的应对这个数据集的类别分布非常不均匀999ganmaoling 和 buluofen 占了绝大多数banlangen 只有 134 框drugs 只有 1 框。直接在原始分布上训练模型会倾向于把高置信度预测给前两类。常见做法是过滤掉 drugs 类或者给少数类增加重复采样。find medicine_ds -name *.jpg | while read f; do t${f%.jpg}.txt; [ ! -s $t ] echo ${f}; done | head -20这条命令找出所有没有非空 txt 的图片-s表示文件大小大于 0。如果这些空标注图大量存在需要确认它们是否均匀分布在 train 和 val 中。空图在 YOLO 训练里充当负样本能降低乱检背景的误检率但如果验证集里空图太多mAP 会被推高因为负样本不会带来 FPprecision 失真。我一般会把空图压到验证集的 5% 以内其余放训练集当背景负样本。另外建议在训练时不直接使用原始 imgsz640而是先跑一次测试看 bbox 面积。可以写一个快速统计import glob import numpy as np areas [] for txt in glob.glob(datasets/medicine/labels/*/*.txt): with open(txt) as f: for line in f: parts line.strip().split() if len(parts) 5: w float(parts[3]) h float(parts[4]) areas.append(w * h) areas np.array(areas) print(f框数: {len(areas)}, 面积中位数: {np.median(areas):.4f})这个脚本遍历所有 txt计算每行的归一化面积并输出中位数。如果中位数小于 0.02说明大量目标小于图像面积的 2%属于小目标检测范畴需要把 imgsz 从 640 提到 768或考虑切片推理。如果面积中位数大于 0.1反而可以降低 imgsz 来提速。这个统计只花几秒钟但能避开训练完再返工的结局。5. 训练后如何验证数据集质量与检测效果5.1 按框尺寸和位置回查标注训练不是终点第一版模型出来后的工作才是真正磨数据的环节。先跑一次验证yolo detect val \ datadatasets/medicine/data.yaml \ modelruns/medicine/exp/weights/best.pt \ splitval \ conf0.25 \ iou0.45conf0.25表示只保留置信度高于 0.25 的预测框iou0.45是 NMS 的 IoU 阈值。输出里的 mAP50 和 mAP50-95 都只能说明整体水平真正的异常藏在混淆矩阵里。YOLOv8 会把混淆矩阵保存到runs/medicine/exp/confusion_matrix.png重点看主对角线之外的高值块。如果999ganmaoling和buluofen互相误检多半是拍摄角度让包装颜色和商标看起来相似可以补充旋转和曝光增强。还有一个容易被忽略的验证维度是标注框是否贴边。很多自动标注工具会在图像边缘留下残缺框需要检查import glob for txt in glob.glob(datasets/medicine/labels/*/*.txt): with open(txt) as f: for line in f: _, cx, cy, w, h map(float, line.strip().split()) if cx - w / 2 0.02 or cy - h / 2 0.02 or cx w / 2 0.98 or cy h / 2 0.98: print(txt, 疑似贴边框)这里设计了cx-w/2 0.02等阈值框离图像边界 2% 以内就会被标出。正常的矩形框应该完整落在图像内部贴边框会引入一半背景作为目标特征影响模型学到完整轮廓。对于小药盒出现贴边框通常意味着标注时没有给边缘留余量。5.2 用滑窗和NMS补回小目标漏检当验证集 tp 里漏掉的多是小药盒时常规调参已经不能解决根本问题因为 640 分辨率下目标只有几十像素。我一般会先在验证集上抽样预测确认漏检是否集中在远距离小框。yolo predict \ modelruns/medicine/exp/weights/best.pt \ sourcedatasets/medicine/images/val \ imgsz640 \ conf0.1 \ save_txtconf0.1故意放低阈值目的是观察低置信度区域里是否藏着目标。如果低置信度框大量出现且位置分散说明模型有能力检测但置信度不够如果完全没有低置信度框说明特征没学出来。针对前者可以直接调低推理时的 conf 到 0.15同时用滑窗推理放大局部区域。对 1080p 原始图我会切成三块有重叠的窗口python -c from PIL import Image img Image.open(test.jpg) w, h img.size for i, box in enumerate([(0,0,w//2,h), (w//2,0,w,h), (w//4,h//4,3*w//4,3*h//4)]): img.crop(box).save(fwindow_{i}.jpg) 这段命令没有额外依赖用 PIL 把图切成左半、右半和中心放大三块分别送给模型做推理再把三份结果按原始坐标合并最后用 NMS 去掉重叠框。合并时注意窗口坐标偏移左半窗口的 x 坐标直接使用右半窗口的 x 坐标要加上w//2中心窗要加上w//4和h//4。NMS 的 IoU 阈值保持 0.45置信度阈值取 0.3基本能拿回大部分漏检的小药盒。本文还有配套的精品资源点击获取