药品目标检测小样本实战:VOC转YOLO与YOLOv8训练全流程
2026/9/11 19:57:05 网站建设 项目流程

简介:针对感冒药品999感冒灵识别场景,这份目标检测标注数据集可服务于智慧药房、药品陈列合规检查及零售终端自动识别等方向,适合目标检测初学者、算法工程师和相关课题研究者用于模型训练、算法验证及毕业设计等任务。压缩包共1119个文件,内含372张jpg原图、373个xml标注和374个txt标注,整体大小约20.88MB;图像同时提供Pascal VOC与YOLO两种格式,类别名称仅设“999ganmaoling”,累计573个目标框。所有标注由labelImg完成,坐标信息和类别字段均已整理,训练时可直接接入YOLO、Faster R-CNN等常见检测框架,无需额外做格式转换;资源内文件组织清晰,便于快速完成数据集加载和数据划分。目前已有248人浏览学习,对于需要药品类检测数据、练习VOC/YOLO格式转换或开展模型对比实验的读者,是一份可以直接上手的数据素材。

1. 372张标注图,能不能撑起一个药品目标检测项目

拿到「药品感冒药999感冒灵检测数据集372张VOC+YOLO格式」这类资源,第一反应通常有两种:372张是不是太少了,能不能训出能用的模型;或者VOC和YOLO两种格式都给了,到底该用哪一套。先说结论:372张对通用目标检测来说是小样本,但对药品检测这种类别少、外观相对固定的任务,配合迁移学习和数据增强,完全能跑出一个在限定场景下可用的模型。

这类数据集的典型价值在于它已经把最费人力的部分做完了:拍摄采集、标注、格式整理。你拿到手之后核心工作不再是标框,而是搞清楚标注内容长什么样、坐标是否归一化、类别有没有歧义,然后直接进入训练和迭代。适合的人群也明确:做药店货架识别、药盒计数、过期药品筛查、智能售药柜的工程师,以及需要一份可以直接喂给YOLO系模型做基线测试的算法同学。

2. VOC和YOLO双格式的组织结构:先搞清楚XML和txt各自存了什么

2.1 VOC格式的三层目录结构与XML标注字段

VOC(Visual Object Classes)格式是目标检测领域的老牌标注格式,它的目录组织有固定习惯。一个标准VOC数据集通常包含三个核心目录:JPEGImages放原始图片,Annotations放与图片同名的.xml标注文件,ImageSets/Main放训练集和验证集的txt列表。部分发行版还会带labels目录,但那是从VOC转出来的中间产物,不是VOC原生结构。

展开一个典型的XML标注文件,核心字段如下:

<annotation> <folder>JPEGImages</folder> <filename>IMG_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>cold_medicine</name> <bndbox> <xmin>312</xmin> <ymin>245</ymin> <xmax>1080</xmax> <ymax>865</ymax> </bndbox> </object> </annotation>

读取XML时的关键判断点有三个:<name>里的类别名是不是你想要的,<bndbox>里的坐标是绝对像素值还是相对值,以及一个图片里有没有多个<object>节点。VOC标准里坐标本来就是绝对像素值,不用额外换算。检查的时候重点看类别名的语义一致性,有时候发行方会把"999感冒灵"标成ganmaolingcold_medicine或者ling,训练前不统一,后面类别数量就会对不上。

2.2 YOLO格式的归一化坐标与txt标签行

YOLO格式把每个标注对象写成一行文本,格式为class_id x_center y_center width height,且这四个坐标值全部除以图片宽和高做归一化,取值范围是0到1。一个txt文件对应一张同名图片,如果图片没有目标,txt文件就是空的。

以VOC标注中xmin=312, ymin=245, xmax=1080, ymax=865、图片尺寸1920x1080为例,转换后的YOLO行是:

0 0.362500 0.513889 0.400000 0.574074

x_center的算法是(312 + 1080) / 2 / 1920y_center(245 + 865) / 2 / 1080,宽度是(1080 - 312) / 1920,高度是(865 - 245) / 1080。训练时YOLO会在内部用这五个数做解码,不需要你再做任何面积换算。这一行里最容易出问题的是第一个数字,它对应的是data.yamlnames列表的下标。如果names['999_cold_medicine'],那这一行第一个数字只能是0;如果还有第二类,才轮到1。

2.3 格式转换脚本:VOC转YOLO的可靠写法

虽然发行版宣称同时提供VOC和YOLO格式,但实际使用时经常出现标注文件和图片文件名对不上、部分标注缺失的情况。自己写一个转换脚本是最稳妥的,顺便还能校验一遍数据。下面这段是常见做法,用Python标准库加xml.etree.ElementTree完成解析和写入:

import os import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) labels = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in class_names: continue cls_id = class_names.index(name) 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) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h labels.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") txt_path = Path(out_dir) / (Path(xml_path).stem + '.txt') txt_path.write_text('\n'.join(labels), encoding='utf-8') class_names = ['999_cold_medicine'] xml_root = 'Annotations' out_root = 'yolo_labels' os.makedirs(out_root, exist_ok=True) for xml_file in Path(xml_root).glob('*.xml'): voc_to_yolo(str(xml_file), out_root, class_names)

这段脚本的逻辑是先用ElementTree解析XML,取出图片宽度和高度,再遍历每个object节点,逐一计算归一化坐标。写文件时用Path.stem保证同名映射,不依赖额外依赖库。参数说明:class_names列表的顺序定义了类别编号,这个顺序必须和后续data.yaml里的names完全一致;xml_root是存放XML标注的VOC目录,out_root是输出的YOLO标签目录。

3. 用YOLOv8把感冒药数据集跑通最小训练流程

3.1 目录整理与data.yaml配置

拿到数据集后第一步不是直接训练,而是重新整理目录。强烈建议手动把图片和标签按YOLO官方推荐的布局重建,不要直接沿用压缩包里的散装结构。标准布局如下:

datasets/medicine/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

划分比例按小数据集的经验用8:2或7:3都行,但必须保证每张图片在images/train里存在,在labels/train里就存在对应文件名的.txt,反之亦然。检查脚本一行能搞定:

for f in images/train/*.jpg; do name=$(basename "$f" .jpg); [ -f "labels/train/$name.txt" ] || echo "missing: $name"; done

data.yaml是训练入口,写法上有几个容易忽略的细节,尤其是路径用绝对路径还是相对路径。我的经验是直接用当前工作目录下的相对路径最省事:

path: datasets/medicine train: images/train val: images/val nc: 1 names: 0: 999_cold_medicine

path字段指向数据集根目录,trainval是相对根目录的图片目录路径,模型会自动在相同目录名下的labels目录里寻找标签。nc是类别总数,names列表的顺序和长度必须与标签文件里的class_id最大值匹配。类别名建议统一用英文小写加下划线,避免中文路径在部分环境下的编码问题。

3.2 训练命令与YOLOv8关键参数

环境装好ultralytics库之后,最小训练命令是:

yolo detect train data=datasets/medicine/data.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16

model=yolov8n.pt表示用COCO预训练权重初始化,n是nano版本,对372张的小数据集是最合适的起点。epochs开150是因为小数据集收敛慢,配合早停机制可以自动截断。imgsz=640是训练时把图片缩放到640x640,这个值和原始标注的绝对坐标无关,因为YOLO格式已经是归一化坐标,缩放图片不影响标签正确性。batch=16根据显存调整,6GB显存用16一般没问题,显存小降到8。

训练中断后想续训,用同一命令加resume=True即可,Ultralytics会自动读取runs/detect/train/weights/last.pt。如果觉得训练太慢,显存又足够大,把model=yolov8s.pt换通常规做法,但小数据集上s和n的精度差距在1到2个点以内,不值得为这点提升增加训练时长。

参数调整优先级如下,影响从大到小排列:

参数作用
modelyolov8n.pt预训练权重决定特征提取基础能力
epochs150小样本需要更多迭代充分拟合
imgsz640输入分辨率,过低丢失药盒小字信息
batch16批量大小,显存允许时尽量大
patience50早停阈值,防止无效训练
optimizerauto自动选择优化器,小数据集上省心
cos_lrTrue余弦退火,收敛更平滑

3.3 验证与测试:直接看模型输出

训练结束后,用验证集评估模型:

yolo detect val model=runs/detect/train/weights/best.pt data=datasets/medicine/data.yaml

val命令会输出mAP50、mAP50-95、precision、recall四个核心指标,同时生成混淆矩阵.png验证结果.pngruns/detect/train/目录下。小数据集上mAP50低于0.85是常态,原因通常是标注框不贴合或类别样本内部差异大,先把混淆矩阵调出来看,比盲目堆epoch更有价值。对单类目标,重点看的是class那一行的P和R,mAP50只是综合参考。

推理一张新图片检查实际效果:

yolo predict model=runs/detect/train/weights/best.pt source=test_images/001.jpg save=True

4. 372张的小样本扩容:数据增强与标注校验

4.1 YOLOv8内置增强参数怎么调

YOLOv8自带一组内置数据增强,在训练命令里直接传参控制。参数默认值对通用场景设置过,小样本数据集必须调整,否则容易过拟合。最值得动的是hsv_hhsv_shsv_vmosaicmixup这几个。

yolo detect train data=datasets/medicine/data.yaml model=yolov8n.pt epochs=200 imgsz=640 batch=16 hsv_h=0.015 hsv_s=0.6 hsv_v=0.4 mosaic=0.8 mixup=0.2 patience=60

色彩相关的hsv_h表示色调扰动幅度,默认0.015,感冒灵药盒的红色包装对色调敏感,调太大会把红色偏移成橙色,反而混淆模型对包装颜色的判断。hsv_s饱和度扰动0.6可以提升对不同光照的鲁棒性,药店LED灯、自然光、白炽灯差别很大,这组参数值得保持或微调。mosaic会把四张图拼成一张训练,默认1.0对小目标检测效果很好,但372张小数据集上全部用mosaic会导致模型难以学到单图尺度下的目标语义,常见的调法是0.8到1.0之间。mixup是两张图按比例混合,0.2是个保守值,高了对药盒边缘纹理破坏过大。

4.2 离线增强:不改变标注信息的拼接与复制粘贴

内置增强在每次epoch中随机变化,而离线增强是提前在磁盘上生成更多图片。对小数据集来说,离线增强的收益更稳定,因为训练时不需要每次重新计算,而且可以人为控制哪些变换合理。推荐两种方案。

第一种是复制粘贴增强,逻辑是从其他图片里裁下药盒目标,随机贴到一张背景图上,同时复制对应的归一化坐标:

import cv2 import numpy as np import random def load_yolo_label(txt_path, class_names): objs = [] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) == 5: objs.append([float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])]) return objs def paste_object(bg_img, obj_img, obj_label, box): x_min, y_min, x_max, y_max = box obj_crop = obj_img[y_min:y_max, x_min:x_max] h, w = bg_img.shape[:2] scale = random.uniform(0.7, 1.3) new_w = int(obj_crop.shape[1] * scale) new_h = int(obj_crop.shape[0] * scale) obj_resized = cv2.resize(obj_crop, (new_w, new_h)) paste_x = random.randint(0, max(1, w - new_w)) paste_y = random.randint(0, max(1, h - new_h)) bg_img[paste_y:paste_y + new_h, paste_x:paste_x + new_w] = obj_resized x_center = (paste_x + new_w / 2) / w y_center = (paste_y + new_h / 2) / h box_w = new_w / w box_h = new_h / h new_label = [obj_label[0], x_center, y_center, box_w, box_h] return bg_img, new_label bg = cv2.imread('background.jpg') obj_img = cv2.imread('sample_001.jpg') objs = load_yolo_label('labels/sample_001.txt', ['999_cold_medicine']) # 取第一个目标,并按其原标注框裁切 xmin = int((objs[0][1] - objs[0][3] / 2) * 1920) ymin = int((objs[0][2] - objs[0][4] / 2) * 1080) xmax = int((objs[0][1] + objs[0][3] / 2) * 1920) ymax = int((objs[0][2] + objs[0][4] / 2) * 1080) bg_aug, new_label = paste_object(bg, obj_img, objs[0], (xmin, ymin, xmax, ymax)) cv2.imwrite('aug_001.jpg', bg_aug) with open('aug_001.txt', 'w') as f: f.write(' '.join(map(str, new_label)))

这段代码的核心逻辑是先把原图里药盒目标按标注框切出来,随机缩放后贴到背景图随机位置,然后按新位置重新计算归一化坐标。这里有个参数细节:scale控制在0.7到1.3,不要超过这个范围,否则药盒尺寸与真实场景差异过大会干扰尺度学习。分母用背景图的实际宽高,必须与训练时的imgsz对应,如果训练时缩放到640,背景图的宽高比例也应接近640x640。

第二种是简单的整体拼接,把多张图横向或纵向排布成大图,每张子图的标注坐标重新偏移后合并。适合要提升模型在小尺寸下识别能力时使用,生成逻辑更复杂但效果直观,能模拟货架上多个药盒并排的场景。注意拼接后图片尺寸也不要偏离640太多,否则训练时缩放损失严重。

4.3 标注质量问题排查

小数据集上精度上不去的头号原因不是算法,是标注错误。拿到数据先做三板斧:类别名唯一性检查、坐标越界检查、空白标签检查。

# 检查所有标签文件中类别id的最大值,不能超过names长度减1 cat labels/train/*.txt labels/val/*.txt | awk '{print $1}' | sort -n | uniq -c # 检查坐标是否越界,第二到第五个值必须在0到1之间 for f in labels/train/*.txt labels/val/*.txt; do awk '{if ($2<0 || $2>1 || $3<0 || $3>1 || $4<0 || $4>1 || $5<0 || $5>1) print FILENAME": "$0}' "$f" done

两条命令没输出就是正常,有输出就要逐行修标注。还有一类问题是图片里有药盒但标签文件为空,这类样本相当于隐形的负样本,混在训练集里会让模型不断产生漏检。

5. 小样本药品检测的三个必调参数与结果验证技巧

小数据集训练完,绝大多数人只会对着mAP50看两眼就结束,但真正影响落地的是三个容易被忽略的点:类别不均衡导致误检、验证集划分不随机导致指标虚高、导出模型时输入尺寸与训练尺寸不一致导致精度损失。

第一个必调参数是scale。YOLOv8默认有尺度增强,范围随训练自动变化,但372张的数据集里药盒尺寸跨度可能不大,过度随机缩放会让模型对真实尺寸产生误导。显式固定实际训练尺度,把scale=0.3传给训练命令,让模型专注真实尺度分布。第二个是label_smoothing,药品包装上的文字和图案差异大,设0.05能给类别边界软化,有品牌LOGO干扰时提升尤其明显:

yolo detect train data=datasets/medicine/data.yaml model=yolov8n.pt epochs=200 imgsz=640 scale=0.3 label_smoothing=0.05

第三个是验证时的conf阈值。训练完的默认置信度阈值是0.25,但对单类小样本模型,误检往往集中在0.3到0.6置信度区间。建议把阈值调到0.35再跑验证集,看同样的推理结果下P和R的变化曲线,找到召回和精确率的平衡点。这个值会影响后续部署时直接输出的检测框数量。

验证方法上用随机种子的固定划分:

python -c " from sklearn.model_selection import train_test_split from pathlib import Path imgs = sorted(Path('images').glob('*.jpg')) train, val = train_test_split(imgs, test_size=0.2, random_state=42) print(len(train), len(val)) "

固定random_state=42的目的是让每次训练前划分的可复现性成立,便于对比不同超参数的实际效果,而不是每次都因为划分不同产生噪声。最后导出ONNX时,记得在导出命令里显式写imgsz=640,导出的模型输入尺寸必须与训练时严格一致:

yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 opset=12

部署端用ONNX Runtime加载时留意输入张量的维度,NCHW中的H和W等于640,不要用其它尺寸直接喂数据,否则预处理阶段的缩放比例和坐标还原会全部错位。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询