药品目标检测小样本实战:VOC转YOLO与YOLOv8训练全流程
简介针对感冒药品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张VOCYOLO格式」这类资源第一反应通常有两种372张是不是太少了能不能训出能用的模型或者VOC和YOLO两种格式都给了到底该用哪一套。先说结论372张对通用目标检测来说是小样本但对药品检测这种类别少、外观相对固定的任务配合迁移学习和数据增强完全能跑出一个在限定场景下可用的模型。这类数据集的典型价值在于它已经把最费人力的部分做完了拍摄采集、标注、格式整理。你拿到手之后核心工作不再是标框而是搞清楚标注内容长什么样、坐标是否归一化、类别有没有歧义然后直接进入训练和迭代。适合的人群也明确做药店货架识别、药盒计数、过期药品筛查、智能售药柜的工程师以及需要一份可以直接喂给YOLO系模型做基线测试的算法同学。2. VOC和YOLO双格式的组织结构先搞清楚XML和txt各自存了什么2.1 VOC格式的三层目录结构与XML标注字段VOCVisual Object Classes格式是目标检测领域的老牌标注格式它的目录组织有固定习惯。一个标准VOC数据集通常包含三个核心目录JPEGImages放原始图片Annotations放与图片同名的.xml标注文件ImageSets/Main放训练集和验证集的txt列表。部分发行版还会带labels目录但那是从VOC转出来的中间产物不是VOC原生结构。展开一个典型的XML标注文件核心字段如下annotation folderJPEGImages/folder filenameIMG_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namecold_medicine/name bndbox xmin312/xmin ymin245/ymin xmax1080/xmax ymax865/ymax /bndbox /object /annotation读取XML时的关键判断点有三个name里的类别名是不是你想要的bndbox里的坐标是绝对像素值还是相对值以及一个图片里有没有多个object节点。VOC标准里坐标本来就是绝对像素值不用额外换算。检查的时候重点看类别名的语义一致性有时候发行方会把999感冒灵标成ganmaoling、cold_medicine或者ling训练前不统一后面类别数量就会对不上。2.2 YOLO格式的归一化坐标与txt标签行YOLO格式把每个标注对象写成一行文本格式为class_id x_center y_center width height且这四个坐标值全部除以图片宽和高做归一化取值范围是0到1。一个txt文件对应一张同名图片如果图片没有目标txt文件就是空的。以VOC标注中xmin312, ymin245, xmax1080, ymax865、图片尺寸1920x1080为例转换后的YOLO行是0 0.362500 0.513889 0.400000 0.574074x_center的算法是(312 1080) / 2 / 1920y_center是(245 865) / 2 / 1080宽度是(1080 - 312) / 1920高度是(865 - 245) / 1080。训练时YOLO会在内部用这五个数做解码不需要你再做任何面积换算。这一行里最容易出问题的是第一个数字它对应的是data.yaml里names列表的下标。如果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), encodingutf-8) class_names [999_cold_medicine] xml_root Annotations out_root yolo_labels os.makedirs(out_root, exist_okTrue) 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; donedata.yaml是训练入口写法上有几个容易忽略的细节尤其是路径用绝对路径还是相对路径。我的经验是直接用当前工作目录下的相对路径最省事path: datasets/medicine train: images/train val: images/val nc: 1 names: 0: 999_cold_medicinepath字段指向数据集根目录train和val是相对根目录的图片目录路径模型会自动在相同目录名下的labels目录里寻找标签。nc是类别总数names列表的顺序和长度必须与标签文件里的class_id最大值匹配。类别名建议统一用英文小写加下划线避免中文路径在部分环境下的编码问题。3.2 训练命令与YOLOv8关键参数环境装好ultralytics库之后最小训练命令是yolo detect train datadatasets/medicine/data.yaml modelyolov8n.pt epochs150 imgsz640 batch16modelyolov8n.pt表示用COCO预训练权重初始化n是nano版本对372张的小数据集是最合适的起点。epochs开150是因为小数据集收敛慢配合早停机制可以自动截断。imgsz640是训练时把图片缩放到640x640这个值和原始标注的绝对坐标无关因为YOLO格式已经是归一化坐标缩放图片不影响标签正确性。batch16根据显存调整6GB显存用16一般没问题显存小降到8。训练中断后想续训用同一命令加resumeTrue即可Ultralytics会自动读取runs/detect/train/weights/last.pt。如果觉得训练太慢显存又足够大把modelyolov8s.pt换通常规做法但小数据集上s和n的精度差距在1到2个点以内不值得为这点提升增加训练时长。参数调整优先级如下影响从大到小排列参数值作用modelyolov8n.pt预训练权重决定特征提取基础能力epochs150小样本需要更多迭代充分拟合imgsz640输入分辨率过低丢失药盒小字信息batch16批量大小显存允许时尽量大patience50早停阈值防止无效训练optimizerauto自动选择优化器小数据集上省心cos_lrTrue余弦退火收敛更平滑3.3 验证与测试直接看模型输出训练结束后用验证集评估模型yolo detect val modelruns/detect/train/weights/best.pt datadatasets/medicine/data.yamlval命令会输出mAP50、mAP50-95、precision、recall四个核心指标同时生成混淆矩阵.png和验证结果.png在runs/detect/train/目录下。小数据集上mAP50低于0.85是常态原因通常是标注框不贴合或类别样本内部差异大先把混淆矩阵调出来看比盲目堆epoch更有价值。对单类目标重点看的是class那一行的P和RmAP50只是综合参考。推理一张新图片检查实际效果yolo predict modelruns/detect/train/weights/best.pt sourcetest_images/001.jpg saveTrue4. 372张的小样本扩容数据增强与标注校验4.1 YOLOv8内置增强参数怎么调YOLOv8自带一组内置数据增强在训练命令里直接传参控制。参数默认值对通用场景设置过小样本数据集必须调整否则容易过拟合。最值得动的是hsv_h、hsv_s、hsv_v和mosaic、mixup这几个。yolo detect train datadatasets/medicine/data.yaml modelyolov8n.pt epochs200 imgsz640 batch16 hsv_h0.015 hsv_s0.6 hsv_v0.4 mosaic0.8 mixup0.2 patience60色彩相关的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 ($20 || $21 || $30 || $31 || $40 || $41 || $50 || $51) print FILENAME: $0} $f done两条命令没输出就是正常有输出就要逐行修标注。还有一类问题是图片里有药盒但标签文件为空这类样本相当于隐形的负样本混在训练集里会让模型不断产生漏检。5. 小样本药品检测的三个必调参数与结果验证技巧小数据集训练完绝大多数人只会对着mAP50看两眼就结束但真正影响落地的是三个容易被忽略的点类别不均衡导致误检、验证集划分不随机导致指标虚高、导出模型时输入尺寸与训练尺寸不一致导致精度损失。第一个必调参数是scale。YOLOv8默认有尺度增强范围随训练自动变化但372张的数据集里药盒尺寸跨度可能不大过度随机缩放会让模型对真实尺寸产生误导。显式固定实际训练尺度把scale0.3传给训练命令让模型专注真实尺度分布。第二个是label_smoothing药品包装上的文字和图案差异大设0.05能给类别边界软化有品牌LOGO干扰时提升尤其明显yolo detect train datadatasets/medicine/data.yaml modelyolov8n.pt epochs200 imgsz640 scale0.3 label_smoothing0.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_size0.2, random_state42) print(len(train), len(val)) 固定random_state42的目的是让每次训练前划分的可复现性成立便于对比不同超参数的实际效果而不是每次都因为划分不同产生噪声。最后导出ONNX时记得在导出命令里显式写imgsz640导出的模型输入尺寸必须与训练时严格一致yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640 opset12部署端用ONNX Runtime加载时留意输入张量的维度NCHW中的H和W等于640不要用其它尺寸直接喂数据否则预处理阶段的缩放比例和坐标还原会全部错位。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →