简介:面向目标检测与计算机视觉实践者的药品识别数据集,聚焦感冒药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张VOC+YOLO格式.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_ok=True) 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_w=640, img_h=640)代码里img_w=640, img_h=640是示意值,实际必须从图片本身读取,否则归一化坐标全错。正确做法是用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_ok=True) 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(f'dataset/images/{split_name}', img_name) dst_lbl = os.path.join(f'dataset/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 \ data=dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ batch=16 \ imgsz=640 \ patience=20 \ project=runs/detect \ name=exp_999参数说明:model=yolov8n.pt表示加载COCO预训练权重而不是从零开始训练,这对小样本至关重要;patience=20是早停参数,验证集指标连续20个epoch不提升就停止训练,省时间防过拟合;imgsz=640训练分辨率,如果原始图片中许多药盒很小,可以试试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 \ data=dataset/data.yaml \ model=yolov8n.pt \ freeze=10 \ epochs=60 \ batch=16 \ imgsz=640 \ patience=15freeze=10表示冻结模型前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_size=8): 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.005:
yolo detect train \ data=dataset/data.yaml \ model=yolov8n.pt \ imgsz=960 \ hsv_h=0.005 \ hsv_s=0.5 \ epochs=80 \ patience=15hsv_h是色调扰动幅度,白色物体对色调极其敏感,扰动过大等于把样本特征改没了。imgsz训练和推理保持统一分辨率,否则模型在特征金字塔上的感受野不匹配,效果会打折扣。
4.5 推理时置信度调不对:框框乱跳或者全不框
现象:训练结果看起来不错,但实际用model.predict跑推理时,要么满屏都是置信度0.2的假阳性框,要么一个框都没有。
原因:YOLO模型输出的原始置信度不代表真实概率,训练集小、类别少时,模型对“背景”和“未见过角度”的置信度普遍偏低。默认阈值conf=0.25可能不适合当前场景,需要实际统计来确定。
解决:批跑验证集,统计置信度分布和对应的精确率召回率,选取F1分数最高的阈值。用下面的脚本找到最适合当前数据集的置信度门限:
from ultralytics import YOLO import numpy as np model = YOLO('runs/detect/exp_999/weights/best.pt') results = model.predict(source='dataset/images/val', conf=0.05, save=False) 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(f"conf={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 model=runs/detect/exp_999/weights/best.pt format=onnx opset=12 imgsz=640转完之后用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张的VOC+YOLO数据到能跑通全流程、找到适合自己场景的置信度门限,这篇文章里的每一步都是为了让你不把时间浪费在排查格式问题、假收敛和阈值调试这些坑里。记住一个原则:小样本模型的结论永远只是“方向上可行”,实用化靠的是后续数据迭代和部署验证。希望帮到你。
本文还有配套的精品资源,点击获取