简介面向目标检测与计算机视觉实践者的药品识别数据集聚焦感冒药999感冒灵的检测场景适合用于模型训练、算法效果对比或课堂教学演示。压缩包共1119个文件包含372张jpg原图、373个XML文件与374个txt文件其中标注文件各372份整体大小约20.88MB图片与标注一一对应且不包含分割路径便于用户按需自行划分训练集与验证集。标注类别统一为“999ganmaoling”共573个边界框全部由labelImg工具绘制同时提供VOC与YOLO两种格式既可在Pascal VOC流程中直接评测也能导入YOLO系列框架快速迭代。由于类别单一、框数适中该数据集还可作为细粒度药品识别的小样本基准用于验证数据增强、迁移学习等策略。图像内容清晰标注风格统一便于直接训练与评估目前已有248人学习下载适合需要规范标注数据启动检测项目的开发者参考。1. 372张的药品检测数据集到底能拿来做什么先泼一盆冷水372张图片对目标检测来说是典型的“小样本”体量想靠它直接训出一个在药店货架上无往不利的模型不现实。但这个数据集的定位本来也不是“量产”而是给药品识别方向做验证性训练和流程打底。999感冒灵是固定包装、固定商标的OTC药品外观变化小类别也只有一种这种“单类别、强外观特征”的场景恰恰是小样本最容易出效果的用YOLOv8n这类轻量模型配合预训练权重AP50做到0.85甚至更高是完全可能的。这套数据集交付的是VOC和YOLO两种标注格式等于同时给了一套“标注数据”和一套“能直接喂给YOLO系列的训练原料”。适合三类人刚入坑目标检测、想跑通“数据→训练→评估”全流程的新手接药品陈列、药盒识别类项目需要快速验证可行性的工程师以及想研究小样本场景下数据增强和迁移学习效果的算法岗。先说清楚结论这个数据集能做但做完之后还得自己补数据、调阈值、做域适配下文把这些步骤全部拆开讲。2. 拆开压缩包先理清格式VOC坐标与YOLO坐标怎么互相换算拿到zip压缩包第一反应是解压。Windows下直接用资源管理器右键解压就行Linux下记住这个命令unzip 药品感冒药999感冒灵检测数据集372张VOCYOLO格式.zip -d drug_det。-d参数指定解压目录避免把所有文件摊在当前目录里弄得一团乱。解压完成后不要急着打开训练脚本先做一次“资源盘点”——很多翻车事故都是因为没搞清楚压缩包里到底是什么目录结构。2.1 先花五分钟认清目录结构和文件名映射规则理想情况下数据集里应该有图片、VOC格式的XML标注文件、YOLO格式的TXT标注文件各一份。常见做法是images目录放jpg标注可能直接在annotations目录下分成voc和yolo两个子目录也可能散在同一层。我拿到手的第一件事是看文件命名训练代码通常默认图片和标注“同名不同后缀”比如IMG_0001.jpg对应IMG_0001.xml和IMG_0001.txt。如果发现文件名对不上后面所有环节都会崩。用下面这行命令检查三类文件数量是否一致ls images | wc -l ls voc_annotations | wc -l ls yolo_annotations | wc -l 2/dev/null || ls *.xml | wc -l输出三个数字如果不相等就得用脚本做差集分析看看是哪些文件名缺失了对应标注。这个步骤相当于质检缺失文件在训练时会被YOLO静默跳过导致实际参与训练的图片少于372张而你不一定察觉得到。2.2 两种标注格式的坐标系换算核心差异就四行公式VOC格式来自Pascal VOC项目标注文件是XML框的坐标用绝对像素值表示xmin、ymin是矩形左上角坐标xmax、ymax是右下角坐标。YOLO格式则完全不同TXT文件每一行是class_id cx cy w h其中cx和cy是中心点坐标w和h是框的宽高四个数值全部除以图片宽高做了归一化取值范围在0到1之间。从VOC转YOLO的核心换算公式如下注意x_center和y_center用的是坐标之和的一半不是坐标差的一半x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h这里有个极易踩的坑xmax - xmin和ymax - ymin算出的是框的宽高直接除以图片宽高即可归一化不需要任何像素到厘米之类的换算。但很多刚上手的人会误把xmax直接当宽、ymax直接当高结果标注框全部变形。2.3 批量转换脚本VOC转YOLO直接可以跑VOC转YOLO的脚本网上版本很多但多数没有处理“类别名不在类别表里”的情况。下面这个版本我做了容错处理转换时遇到未知类别直接跳过并打印提示避免一个错误类别让整个转换任务中断import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_path, classes, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: print(f[跳过] 未注册类别: {name} in {xml_path}) continue cls_id classes.index(name) bndbox obj.find(bndbox) x1 float(bndbox.find(xmin).text) y1 float(bndbox.find(ymin).text) x2 float(bndbox.find(xmax).text) y2 float(bndbox.find(ymax).text) 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_path, w) as f: f.write(\n.join(lines)) # 使用示例遍历voc目录下所有xml classes [999_ganmaoling] # 按实际类别名修改 voc_dir voc_annotations yolo_dir yolo_annotations os.makedirs(yolo_dir, exist_okTrue) for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue img_file xml_file.replace(.xml, .jpg) img_path os.path.join(images, img_file) if not os.path.exists(img_path): print(f[警告] 找不到图片: {img_path}) continue # 一般用cv2.imread读取图片获取宽高这里用简化写法 voc_to_yolo(os.path.join(voc_dir, xml_file), os.path.join(yolo_dir, xml_file.replace(.xml, .txt)), classes, img_w640, img_h640)代码里img_w640, img_h640是示意值实际必须从图片本身读取否则归一化坐标全错。正确做法是用cv2.imread获取真实宽高。这段脚本的逻辑是遍历每个XML提取每个目标的类别和bbox坐标转成归一化的中心点宽高表示写入TXT文件。classes列表的索引就是YOLO格式里的class_id所以顺序不能乱一旦训练中途改了类别顺序之前的标注就全部报废。2.4 转完格式之后立刻做可视化校验转换脚本跑完不代表数据没问题。最靠谱的验证方式是把标注框画回图片上肉眼检查偏移。下面这段代码用OpenCV把YOLO格式的TXT标注绘制回原图框位置如果和药盒边缘有明显偏差说明转换精度或者图片宽高读取出了问题import cv2 def draw_yolo_boxes(img_path, txt_path, classes): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f.readlines(): parts line.strip().split() if len(parts) ! 5: continue cls_id, cx, cy, bw, bh parts cx, cy, bw, bh float(cx), float(cy), float(bw), float(bh) 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, classes[int(cls_id)], (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img img draw_yolo_boxes(images/IMG_0001.jpg, yolo_annotations/IMG_0001.txt, [999_ganmaoling]) cv2.imwrite(check_0001.jpg, img)注意代码里的关键细节cx - bw / 2计算的是左上角x坐标很多新手写成cx - bw框会往左上偏移一半。绘制时用原图的宽高反算像素坐标不是用标注里已有的数值直接画。多抽查几十张图如果全部正常再进行下一步划分训练集。3. 用YOLOv8训练自己的数据集划分、配置与最小训练命令数据集格式统一之后下一步就是划分数据集并启动训练。这一步看似简单但“怎么分”直接决定训练结果可信度。372张图全部拿去训练绝对不行没有验证集的训练就是闭眼开车。3.1 372张怎么划分训练、验证、测试的比例与要点我的习惯划分比例是8:1:1也就是训练集300张、验证集36张、测试集36张。对小样本来说验证集和测试集不能太少否则评估指标方差太大一次训练AP50从0.8跳到0.6都属正常。划分时注意两点一是random.seed固定随机种子保证每次划分结果一致方便复现二是不要手动挑选图片容易引入主观偏差。很多人问我为什么还要单独分测试集验证集不够吗不够。验证集在训练过程中会被用来做早停和学习率调整模型间接“见过”它的分布。测试集是最后才碰的用来模拟真实场景评估泛化能力。在小样本场景下三者缺一不可虽然每一份都小但流程必须完整。3.2 数据划分脚本直接复制改路径就能用import os import random import shutil random.seed(42) # 固定种子保证可复现 image_dir images label_dir yolo_annotations train_img_dir dataset/images/train val_img_dir dataset/images/val test_img_dir dataset/images/test train_lbl_dir dataset/labels/train val_lbl_dir dataset/labels/val test_lbl_dir dataset/labels/test for d in [train_img_dir, val_img_dir, test_img_dir, train_lbl_dir, val_lbl_dir, test_lbl_dir]: os.makedirs(d, exist_okTrue) all_images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(all_images) n len(all_images) n_val int(n * 0.1) n_test int(n * 0.1) n_train n - n_val - n_test splits { train: all_images[:n_train], val: all_images[n_train:n_train n_val], test: all_images[n_train n_val:] } for split_name, images in splits.items(): for img_name in images: base img_name.replace(.jpg, ) src_img os.path.join(image_dir, img_name) src_lbl os.path.join(label_dir, base .txt) dst_img os.path.join(fdataset/images/{split_name}, img_name) dst_lbl os.path.join(fdataset/labels/{split_name}, base .txt) if os.path.exists(src_lbl): shutil.copy(src_img, dst_img) shutil.copy(src_lbl, dst_lbl) else: print(f[跳过] 缺少标签: {src_lbl}) print(f训练集: {n_train}, 验证集: {n_val}, 测试集: {n_test})这段脚本做的事把图片随机打乱按比例切分然后同时复制图片和同名TXT标注到YOLO要求的目录结构下。YOLO系列的data配置默认寻找images/train和labels/train这样的目录对目录结构不对是训练启动报错最常见的原因。脚本末尾的打印当前划分数量用来核对总和是否为372张。3.3 写data.yaml文件与最小训练命令YOLOv8的数据集描述文件是YAML格式它告诉训练器类别数量和类别名。注意YAML中类别列表的顺序必须和标注文件里的class_id完全对应否则模型训练得再久也是错的# dataset/data.yaml path: dataset train: images/train val: images/val test: images/test nc: 1 names: [999_ganmaoling]然后是最小训练命令。新手直接用YOLOv8n它是纳米版训练速度快显存占用低适合小数据集和快速迭代。有条件的可以试YOLOv8s精度略高但速度慢约一倍yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ epochs100 \ batch16 \ imgsz640 \ patience20 \ projectruns/detect \ nameexp_999参数说明modelyolov8n.pt表示加载COCO预训练权重而不是从零开始训练这对小样本至关重要patience20是早停参数验证集指标连续20个epoch不提升就停止训练省时间防过拟合imgsz640训练分辨率如果原始图片中许多药盒很小可以试试896或1024但显存占用会显著上升。3.4 训练结束后看什么结果文件与“假收敛”识别训练完成后在runs/detect/exp_999目录下会生成weights/best.pt、weights/last.pt和results.png。新手容易犯的错误是直接拿last.pt去推理这个权重是最后一个epoch的可能已经过拟合必须用best.pt它是验证集上指标最优的检查点。results.png里有两张关键曲线train/box_loss和val/box_loss。如果训练损失持续下降但验证损失中途反弹就是过拟合的典型信号。372张图训练集太小这种情况几乎必然出现只是时间早晚的问题。这也是为什么第二章强调先做可视化校验——数据本身不干净损失曲线分析得再细也没有意义。4. 避坑与排查药品盒小样本训练的5个翻车现场小样本数据集训练坑比参数多。这章我把实操中最容易翻车的5个问题按“现象→原因→解决”写出来每一条都是真实发生过的血泪经验。4.1 验证损失先降后升训练损失一直降过拟合来敲门现象训练到第40个epoch左右train/box_loss还在下降val/box_loss却开始掉头向上AP50在验证集上波动加大典型过拟合。原因372张图训练集太小模型把训练图里的背景纹理、光照甚至噪点都背了下来泛化能力反而变差。这是小样本训练的第一杀手不是代码写错了。解决最直接的办法是缩短训练轮数或者调低patience让早停更敏感我一般设10~15。其次是增强数据增强把YOLOv8默认的增强参数hsv_h、hsv_s、degrees适度调大增加样本多样性。如果仍然过拟合考虑用迁移学习冻结骨干网络前10层再训练减少需要学习的参数量yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ freeze10 \ epochs60 \ batch16 \ imgsz640 \ patience15freeze10表示冻结模型前10层通常是骨干网络的浅层特征提取部分浅层特征如边缘、颜色、纹理是通用的不需要在小数据集上重新学只需要更新深层针对特定任务的参数。这样参数更新量小过拟合压力显著降低。4.2 标签全部转换成功训练却提示“找不到标签”现象数据集转换、划分都完成运行训练命令后抛错提示类似No labels found in dataset/images/train但目录里明明有TXT文件。原因YOLO要求标签必须放在labels目录下而不是annotations。很多人在划分脚本里把标注复制到了labels但目录结构是dataset/train/labels而不是dataset/labels/train路径层级错了一位。另外data.yaml里train字段指向的是images/train这个相对路径YOLO默认你同时存在labels/train目录不会检查是否存在。解决严格按照第三章给出的目录结构dataset/images/train和dataset/labels/train平级data.yaml中的path指向dataset这一层。检查后修正路径或者干脆把TXT文件用软链接指过去。4.3 验证集评估猛如虎测试集上AI失灵数据划分不随机现象验证集AP50高达0.9测试集性能却只有0.6差距悬殊。原因数据集中可能存在大量同一个药盒在不同角度、不同光照下的连续帧照片。如果划分时没有做序列去重训练集和验证集里会出现“同一场景的近亲照片”模型记住了场景而不是药盒验证分数虚高。解决划分之前先做哈希去重。计算每张图片的感知哈希把相似度超过阈值的图片归为一组整组划分到同一个子集中从机制上杜绝数据泄漏import os import cv2 import numpy as np def dhash(image, hash_size8): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (hash_size 1, hash_size)) diff resized[:, 1:] resized[:, :-1] return sum([2 ** i for i, v in enumerate(diff.flatten()) if v]) def hamming_dist(h1, h2): return bin(h1 ^ h2).count(1) image_dir images hashes {} for img_name in os.listdir(image_dir): img_path os.path.join(image_dir, img_name) img cv2.imread(img_path) if img is None: continue h dhash(img) hashes[img_name] h # 找出两两相似度高的图片对 similar_pairs [] names list(hashes.keys()) for i in range(len(names)): for j in range(i 1, len(names)): if hamming_dist(hashes[names[i]], hashes[names[j]]) 5: similar_pairs.append((names[i], names[j])) print(f找到 {len(similar_pairs)} 组疑似重复图片)这段代码用感知哈希dHash计算每张图的指纹汉明距离小于5就判定为近似重复。输出结果后把重复组手动归并到同一个数据子集再重新划分。这个过程很繁琐但这是小样本评估可信度最重要的保障。4.4 药盒小目标漏检白色药盒在浅色背景下直接消失现象训练完成后在测试集上跑推理远处的、货架高层的药盒经常漏检尤其是白色药盒放在白色货架隔板上时。原因这是两个因素叠加一是小目标本身检测难度大药盒可能只占画面的2%二是数据增强中hsv_h随机改变色相把白色药盒的颜色变得偏蓝偏绿模型反而学不到“白色包装盒”的本质特征。解决对小目标场景推理时将imgsz从训练时的640提高到960相当于把输入分辨率放大小目标在特征图上的像素数量增加漏检率通常能下降一半。同时生成增强数据时关闭过度的颜色扰动将hsv_h调低到0.005yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ imgsz960 \ hsv_h0.005 \ hsv_s0.5 \ epochs80 \ patience15hsv_h是色调扰动幅度白色物体对色调极其敏感扰动过大等于把样本特征改没了。imgsz训练和推理保持统一分辨率否则模型在特征金字塔上的感受野不匹配效果会打折扣。4.5 推理时置信度调不对框框乱跳或者全不框现象训练结果看起来不错但实际用model.predict跑推理时要么满屏都是置信度0.2的假阳性框要么一个框都没有。原因YOLO模型输出的原始置信度不代表真实概率训练集小、类别少时模型对“背景”和“未见过角度”的置信度普遍偏低。默认阈值conf0.25可能不适合当前场景需要实际统计来确定。解决批跑验证集统计置信度分布和对应的精确率召回率选取F1分数最高的阈值。用下面的脚本找到最适合当前数据集的置信度门限from ultralytics import YOLO import numpy as np model YOLO(runs/detect/exp_999/weights/best.pt) results model.predict(sourcedataset/images/val, conf0.05, saveFalse) conf_scores [] for r in results: if r.boxes is not None and len(r.boxes) 0: conf_scores.extend(r.boxes.conf.cpu().numpy().tolist()) scores np.array(conf_scores) for t in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]: keep scores[scores t] print(fconf{t}: 保留 {len(keep)}/{len(scores)} 个框, 平均精度 {keep.mean() if len(keep) else 0:.3f})这个脚本把置信度阈值从0.05开始收集所有候选框然后统计不同阈值下保留的框数量和置信度均值。结合可视化抽帧观察找出“既不乱框也不漏框”的平衡点。小样本模型通常需要把阈值调到0.35到0.5之间比默认的0.25要高。5. 小样本模型的最后一公里部署验证、数据扩充与置信度校准模型训练好比在实验里跑通距离真正用起来还差两件事把模型放到真实环境里验证以及继续补数据让模型变稳。小样本只是起点不是终点。先说部署验证。训练得到的best.pt是PyTorch权重在生产环境中最常见是转成ONNX部署到服务端CPU推理甚至边缘设备。转换命令一行就能完成yolo export modelruns/detect/exp_999/weights/best.pt formatonnx opset12 imgsz640转完之后用ONNX Runtime做批量推理验证重点检查转换前后输出一致性。转换过程偶有算子在ONNX Runtime不支持导致输出偏差尤其是在某些版本的YOLOv8中Sigmoid和Concat算子在不同opset下行为可能不同。验证方法是读同一张图分别用yolo predict和ONNX Runtime推理比较检测框的重叠度。IOU低于0.9就要核对转换参数。数据扩充是让小模型变得更实用的唯一出路。不要只加数据量要有针对性地补“模型当前最不擅长”的场景药盒斜放超过30度、部分遮挡、逆光、背景杂乱。采集数据用手机拍即可拍完用LabelImg手动打标导出为YOLO格式labelimg images annotations classes.txtclasses.txt里写入和训练一致的类别名999_ganmaoling打标时软件会自动读入。补图时注意和原始数据集的分布要互补比如原数据集多为正对药盒拍摄新数据就专门拍俯视和侧视原数据集在白色背景居多新数据就拍深色木桌或货架。数据扩充后重新划分数据集、重新训练迭代两三轮后模型稳定性会有一个质的提升。最后提一个每天都要做的习惯每次训练完把results.png、confusion_matrix.png和F1_curve.png存到一个按日期命名的实验记录目录里。小样本训练的随机性大同一个数据集两次训练AP50波动0.05以上不奇怪没有实验记录很难判断某个参数改动的真实效果。我自己吃过这个亏一次调参觉得是增强参数起了作用后来复盘才发现只是随机种子不同导致的波动。从372张的VOCYOLO数据到能跑通全流程、找到适合自己场景的置信度门限这篇文章里的每一步都是为了让你不把时间浪费在排查格式问题、假收敛和阈值调试这些坑里。记住一个原则小样本模型的结论永远只是“方向上可行”实用化靠的是后续数据迭代和部署验证。希望帮到你。本文还有配套的精品资源点击获取