ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

药品目标检测实战:999感冒灵数据集与YOLO训练全流程解析

药品目标检测实战:999感冒灵数据集与YOLO训练全流程解析 简介面向计算机视觉、目标检测算法工程师及科研初学者这份数据集专注于药品领域的“999感冒灵”检测任务可弥补公开数据集中特定商品样本稀少的短板。压缩包共1119个文件包含372张jpg原图、373个VOC格式xml标注文件和374个YOLO格式txt标注文件整体约20.88MBxml与txt分别对应Pascal VOC和YOLO两种主流标注规范无需额外转换即可接入常见训练流程。数据由labelImg标注完成统一使用“999ganmaoling”类别共573个标注框类别单一且框数充足适合快速验证单类目标检测模型也能扩展到药品零售、智能货架等真实场景。当前已有248人学习浏览说明该数据在同类任务中具备一定参考价值整体格式规整、标注风格统一便于二次利用。解压后可直接划分训练集与验证集用于YOLOv5、YOLOv8、Faster R-CNN等主流检测框架的训练与效果对比帮助省去大量整理与转换标注的时间。1. 药品感冒药 999 感冒灵检测数据集372 张图如何支撑一次 YOLO 训练如果你手头有药店货架、医药电商的商品图正愁没数据跑目标检测这套药品感冒药 999 感冒灵检测数据集可以直接把训练流程拉通。372 张图片、1 个类别、573 个标注框体量不大但胜在双格式完整Pascal VOC 的 xml 和 YOLO 的 txt 配套齐全省掉了最枯燥的数据标注环节。对刚接触目标检测的新手来说这是练手材料对手里有真实检测需求、想先用小规模数据验证方案可行性的工程师来说这是体检样本。它不负责解决所有问题但能让你在几小时内从零走到第一次评估。2. 拆开双格式标注VOC 与 YOLO 之间到底差在哪2.1 先看目录结构jpg、xml、txt 三件套的对应关系这套数据集的命名风格一眼能看穿999ganmaoling_xyxr_编号.jpg这类文件名每张 jpg 旁边都配着同名 xml 和同名 txt。xml 是 Pascal VOC 标准格式txt 是 YOLO 训练直接读取的标签格式。两者描述同一批目标框但存储口径不一样xml 用像素绝对值txt 用归一化比例。后缀内容谁在消费.jpg原始图像训练时由 dataloader 读取.xml类别名、像素坐标、图片尺寸LabelImg 打开可继续编辑.txt类别 id 与归一化框坐标YOLO 系列训练时读取拿到手第一件事我习惯先做一次三目录对账别急着开训。手工标注和打包传输常会丢文件训练时最常见的报错就是某张图找不到对应标签。跑一遍这种脚本能省掉后面一半的排错时间。import os jpg_set {f[:-4] for f in os.listdir(images) if f.endswith(.jpg)} xml_set {f[:-4] for f in os.listdir(Annotations) if f.endswith(.xml)} txt_set {f[:-4] for f in os.listdir(labels) if f.endswith(.txt)} only_jpg jpg_set - xml_set - txt_set only_xml xml_set - jpg_set only_txt txt_set - jpg_set print(缺 xml 或 txt 的 jpg:, only_jpg) print(缺 jpg 的 xml:, only_xml) print(缺 jpg 的 txt:, only_txt)这段脚本的输入是三个目录名只要改成你解压后的实际路径就行。集合运算会找出三边不齐的文件名输出为空说明文件对应关系正常。注意这里用的是文件名前缀去匹配前提是 xml 和 txt 与 jpg 同名这套数据满足这个命名规则。2.2 xml 里的关键节点name 与 bndbox打开任意一张 xml看到的都是 Pascal VOC 标准模板。以其中某个文件为例核心内容长这样annotation filename999ganmaoling_xyxr_55.jpg/filename size width640/width height480/height depth3/depth /size object name999ganmaoling/name bndbox xmin100/xmin ymin80/ymin xmax520/xmax ymax360/ymax /bndbox /object /annotation这里有个新手容易忽略的点size节点里的 width 和 height 是原图尺寸很多手工转换脚本拿它做归一化分母但如果 xml 里的尺寸和 jpg 实际像素宽高不一致最终框的位置会整体偏移。判断方法很简单用 OpenCV 或 PIL 读一遍图片尺寸跟 xml 里的值比对一下就行。bndbox里是像素坐标xmin/ymin 是目标左上角xmax/ymax 是右下角。坐标原点是图像左上角y 轴向下增长这个坐标系跟多数 UI 编程习惯一致但转 YOLO 格式时稍不留神就会把 y 方向搞反。一个 object 节点对应一个目标这张图如果同时出现两个感冒灵盒子就会有第二个 object。2.3 txt 里的归一化坐标四个数的顺序与计算口径YOLO 系列读取的标签文件是 txt每行五个数class_id x_center y_center width height。前四个数全部除以图片宽高做归一化理论取值区间是 0 到 1。从 xml 转 txt 的换算公式是一组初中生都能看懂的加减乘除x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height w (xmax - xmin) / width h (ymax - ymin) / height转出来的一条 txt 记录大概是这样的0 0.484375 0.458333 0.656250 0.583333第一个 0 代表类别 id后面四个是归一化之后的中心点 x、中心点 y、框宽、框高。我写过一个可以直接套用的转换函数def voc_to_yolo(xmin, ymin, xmax, ymax, img_w, img_h): x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) w max(0.0, min(1.0, w)) h max(0.0, min(1.0, h)) return x_center, y_center, w, h参数说明前四个参数从 xml 的 bndbox 读取img_w 和 img_h 建议以实际 jpg 尺寸为准不要完全信任 xml 里的 size 节点。最后做 clip 是为了防止框越界导致训练时标签解析报错但 clip 只是兜底真正要做的是把越界样本找出来修掉。注意txt 里是比例值不是像素值。有人图省事把 x_center 写成 xmin/width结果预测框整体左移半个身位这类问题肉眼在可视化里很难发现后面第 4 章细说。2.4 多目标场景一个文件多行别只读第一行一张图里有多个目标时xml 里会写多个 object 节点txt 里会对应多行数据。新手踩坑的点集中在两处一是读取 txt 时只取第一行二是不小心用逗号分割而不是空格分割导致每行的坐标解析错位。洗数据时我习惯跑一遍对象数核对确保 xml 里的 object 个数和 txt 行数一致。import os import xml.etree.ElementTree as ET def count_objects_in_xml(xml_path): tree ET.parse(xml_path) return len(tree.findall(.//object)) def count_lines_in_txt(txt_path): with open(txt_path) as f: return len([line for line in f if line.strip()]) base_names [f[:-4] for f in os.listdir(images) if f.endswith(.jpg)] for name in base_names: xml_cnt count_objects_in_xml(fAnnotations/{name}.xml) txt_cnt count_lines_in_txt(flabels/{name}.txt) if xml_cnt ! txt_cnt: print(fmismatch: {name}, xml{xml_cnt}, txt{txt_cnt})脚本逻辑不复杂遍历所有 jpg 文件名分别取对应 xml 和 txt比对里面记录的目标数。输出为空说明两套标注的对象数量对齐了。任何一条 mismatch 都要在训练之前处理否则后面看 loss 曲线全是玄学。3. 训练前的数据准备划分、统计与 data.yaml 配置3.1 划分训练集和验证集372 张怎么拆才算合理数据量 372 张划分比例直接影响最终评估的可信度。验证集太少指标波动大验证集太多训练样本不够。我常用的方案是 8:2也就是大约 297 张训练、75 张验证。先随机打乱再按比例切片。import os import random images [f for f in os.listdir(images) if f.endswith(.jpg)] random.seed(42) random.shuffle(images) train_ratio 0.8 length len(images) train_cnt int(length * train_ratio) train_images images[:train_cnt] val_images images[train_cnt:] train_paths [os.path.join(images, name) for name in train_images] val_paths [os.path.join(images, name) for name in val_images] with open(train.txt, w) as f: f.write(\n.join(train_paths)) with open(val.txt, w) as f: f.write(\n.join(val_paths)) print(ftrain: {len(train_paths)}, val: {len(val_paths)})逻辑说明先把所有 jpg 文件名列出来用random.seed(42)固定随机种子这样每次运行都得到完全一致的划分复现实验时不会因为验证集不同导致指标对不上。train.txt 和 val.txt 里存的是图片路径YOLO 的工具链会按行读取去找对应图片和标签。参数说明train_ratio 0.8是针对 372 张图的经验值如果以后扩充到几千张可以把比例调成 9:1验证集只需要覆盖足够多样的场景即可。seed 设为 42 不是约定俗成只要记录下来自己用了多少以后重新划分时保持一致就行。3.2 统计每个类别的框数573 是不是真均衡训练前的数据体检最重要的一项是按类别统计框数量。这套数据集只有一个类别573 个框看似全部归到 999ganmaoling但统计脚本还是得跑一遍有两个现实原因一是确认 txt 里的类别 id 没有混入其他数字标注时鼠标误点或脚本 bug 都可能混入 id1、id2二是以后增加新类别时统计脚本可以直接复用。from collections import Counter import os label_dir labels counter Counter() for txt_name in os.listdir(label_dir): if not txt_name.endswith(.txt): continue with open(os.path.join(label_dir, txt_name)) as f: for line in f: line line.strip() if not line: continue cls_id int(line.split()[0]) counter[cls_id] 1 for cls_id, count in sorted(counter.items()): print(fclass {cls_id}: {count} boxes)输出里应该只有 class 0 这一行数量在 573 左右。如果出现了其他 id说明标签文件里有脏数据需要回到对应 xml 里查类别名统一修正。单类数据集的问题很多时候不在类别数量上而在类别 id 的分布上混进一个空格或者隐藏字符都会导致解析出错。3.3 生成 data.yaml路径、类别数与名称一次配齐YOLOv5 和 YOLOv8 的训练入口都依赖一个 data.yaml 配置文件。格式不复杂但路径写错是高频报错点。我习惯把train和val直接指向上一小节生成的 txt 文件而不是指向图片目录这样图片路径的拼接更可控。train: ./train.txt val: ./val.txt nc: 1 names: - 999ganmaoling配置说明train和val这里用了相对路径但在别人机器上复现时绝对路径更省事。nc是类别数量必须和names列表长度一致这里 nc 是 1列表只有一个元素匹配得上。类别名必须严格等于 xml 里 object/name 节点的值多一个空格都不行。注意如果换机器跑data.yaml 里的路径一定要改成实际路径否则训练会在数据加载阶段卡住报错信息又不会明确告诉你哪一行路径错了。4. 标注数据避坑指南五个最容易翻车的位置4.1 坐标越界xmin 为 0 时模型为什么画不对框现象训练能跑但推理时部分预测框明显偏移甚至跑到图片边缘外面去。原因xml 的 bndbox 可能存在越界值比如 xmin 小于 0、xmax 大于图片宽度。这类框在 YOLO 数据加载阶段会被裁剪或丢弃模型在对应区域就学不到正确的回归信号。很多情况是第一轮标注时鼠标拖拽过头标完没放大检查就直接导出了。解决统一跑一遍越界检查用 OpenCV 读取真实图片尺寸逐个比对 bndbox 是否越界。import os import xml.etree.ElementTree as ET import cv2 for xml_name in os.listdir(Annotations): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(Annotations, xml_name)) root tree.getroot() img_name root.find(filename).text img cv2.imread(os.path.join(images, img_name)) img_h, img_w img.shape[:2] for obj in root.findall(object): 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) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: print(f越界: {img_name}, bbox({xmin}, {ymin}, {xmax}, {ymax}))这段脚本不依赖 xml 里的 size 节点直接读真实图片尺寸查出来的越界情况最接近实际。输出为空最好有输出就优先人工检查那几张图片通常问题出在少数图上。4.2 类别名不一致name 里多一个空格引发连锁反应现象训练完成后的 confusion matrix 里类别名错乱或者出现 Unknown 类别。原因xml 里写的是999ganmaolingdata.yaml 里写的是999ganmaoling中间如果混入了一个空格或全角字符字符串匹配失败评估阶段的类别名就对不上。这个问题在人工创建 yaml 配置时特别容易出现。解决跑一个脚本把全部 xml 里的唯一 name 值列出来和 data.yaml 的 names 做对比有差异立刻看原始字符串。import os import xml.etree.ElementTree as ET import yaml with open(data.yaml) as f: cfg yaml.safe_load(f) names_in_yaml cfg[names] xml_names set() for xml_name in os.listdir(Annotations): if not xml_name.endswith(.xml): continue root ET.parse(os.path.join(Annotations, xml_name)).getroot() for obj in root.findall(.//object): xml_names.add(obj.find(name).text) print(yaml names:, names_in_yaml) print(xml unique names:, xml_names)注意这里我故意没有对 xml 里的 name 做 strip就是为了暴露原始数据的真实情况。靠 strip 兜底只能掩盖问题正确做法是把 xml 里的原始值修正干净。4.3 类别 id 对不上txt 里的 0 不一定是 999ganmaoling现象训练时 loss 降得正常但 mAP 统计图里的类别名完全对不上。原因YOLO txt 标签第一列是类别 id它对应 data.yaml 的 names 列表里的顺序。如果这份 txt 是早期脚本生成的而后续 names 列表顺序被调整过就会出现错位。解决检查所有 txt 里出现过的类别 id 最大值保证它小于 nc。同时挑一张图读它的 txt 第一行 id 和同图 xml 的 name做一次人工比对。import os max_id -1 txt_count 0 for txt_name in os.listdir(labels): if not txt_name.endswith(.txt): continue with open(os.path.join(labels, txt_name)) as f: for line in f: line line.strip() if not line: continue left int(line.split()[0]) if left max_id: max_id left txt_count 1 print(fmax class id: {max_id}) print(ftotal boxes in txt: {txt_count})如果 max_id 大于等于 nc说明一定有 txt 的类别 id 越界。最直接的修法是把这个越界 id 全部改成 0前提是你确认所有目标都属于同一个类别。这类问题不常见但一旦踩中模型训练完后发现的代价是最高的。4.4 文件名里的中文、空格与重名现象训练脚本报 FileNotFoundError但文件明明就在目录里。原因部分目标检测框架的 dataloader 对中文路径支持不好Windows 下的中文路径更是一告一个准。这套数据的原始文件名是纯英文加下划线很规范但如果你以后自己扩充数据网上下载的素材常常带中文、空格甚至括号侥幸开训后总会在某个 epoch 中途翻车。解决统一把所有图片文件重命名成 ASCII 字符集格式限定为英文字母、数字、下划线组合。写一个批量改名脚本把图片和对应的 xml、txt 一起改掉保持三者前缀一致。4.5 372 张全是有目标的图过拟合比想象中来得早现象训练集 mAP 很快逼近 1.0验证集 mAP 卡在 0.8 左右而且不再上升。原因这套数据集每张图都包含目标缺少负样本。模型学到的更多是这张图里有没有感冒灵而不是感冒灵在哪个位置。372 张图、单类别特征空间本来就窄过拟合几乎是必然的只是早晚问题。解决训练时开 mosaic、flip、hsv 等数据增强部署时在置信度阈值上留出余量后续补充一批没有任何目标的背景图和难例。数据增强的具体操作最后一章给方案。5. 验证这批数据的有效性可视化检查与训练后评估5.1 先把标注框画出来看一遍拿到数据后我一般不直接开训而是先把 xml 里的框画回原图生成带框预览图快速过一遍标注质量。坐标写错、框框错目标、漏标目标这几种问题在预览图上都是一眼可见的比训练完再去查指标高效得多。import cv2 import xml.etree.ElementTree as ET img cv2.imread(999ganmaoling_xyxr_55.jpg) root ET.parse(999ganmaoling_xyxr_55.xml).getroot() for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(ymin - 6, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check_999ganmaoling_55.jpg, img)这段依赖 OpenCV安装后直接跑文件名换成实际路径即可。我会批量生成全部预览图再挑十几张放大看框边缘和目标的贴合度重点检查有没有框到背景、有没有漏掉侧面包装。5.2 训练后看两个数mAP0.5 和过拟合曲线372 张单类数据用 yolov5n 或 yolov8n 这样的小模型一两百个 epoch 内就能收敛。评估时优先看 mAP0.5这类小数据集目标数少mAP0.5:0.95 的波动很大参考意义有限。另一个关键指标是训练集和验证集的 loss 曲线差距持续拉大就是过拟合信号。5.3 数据量不够时的三条出路增强、迁移学习、负样本想继续压榨这套数据的潜力我的习惯是依次做三件事第一训练时开 mosaic、flip、hsv 增强让模型看到更多样性第二换用 COCO 预训练权重做迁移学习底层的通用特征不用重新学第三补拍一些没有感冒灵的药店货架图片作为负样本让模型学会抑制背景误检。从那以后我每次拿到新数据集都强制走一遍可视化检查和类别统计再去碰训练脚本。这套流程不复杂但能避开九成以上的玄学问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表