news 2026/10/8 7:17:10

药品感冒药999感冒灵检测数据集实战:VOC转YOLO与训练避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
药品感冒药999感冒灵检测数据集实战:VOC转YOLO与训练避坑指南

简介:这是一份面向计算机视觉学习者、算法工程师和药品零售场景开发者的目标检测数据集,主题为“999感冒灵”外包装识别。数据集共提供372张jpg原图,以及一一对应的VOC格式xml标注文件和YOLO格式txt标注文件,由labelImg工具标注完成;标注类别为999ganmaoling,标注框总数573个,每个类别框数明确。压缩包内文件总数为1119个,其中jpg、xml、txt三类文件数量基本对应,便于按图像-标签一一对应的方式完成训练集和验证集划分;资源包整体约20.88MB,适合快速下载后直接用于YOLO系列、Faster R-CNN、MMDetection等常见检测框架的训练、迁移学习与效果验证。目前已有249人学习下载,可作为药品外包装检测、零售货架识别、目标检测课程作业等场景的入门或补充数据。由于数据已标明图片数、标注框数和类别名,使用者能够方便评估数据规模、判断样本均衡性,并进一步做数据增强、难例挖掘或格式转换,减少前期数据整理成本。

1. 药品感冒药999感冒灵检测数据集:372张图能做什么、不能做什么

「药品感冒药999感冒灵检测数据集」这个名字乍看普通,却是目标检测里最典型的“小而专”样本:372张图片、VOC与YOLO两套标注格式,场景几乎全是药店货架、桌面、手持包装盒这类近景拍摄。它的价值不在规模,而在干净——类别少、背景相对固定、光照和角度还算可控,正好用来验证迁移学习在小样本上的真实下限。对刚入门目标检测的开发者,它是练习数据标注规范、跑通训练全流程的练手样本;对做药品识别或药店库存盘点的从业者,它是低成本验证方案可行性、再决定是否扩采数据的起点。这一篇我就按自己拿到这种小数据集后的完整套路来讲:先体检数据,再统一格式,然后训练调参,最后教你避开小数据集最常翻车的几个坑。

2. 拆开药品感冒药999感冒灵检测数据集:目录结构、标注内容与体检方法

2.1 VOC与YOLO两套标注并存的目录长什么样

解压zip之后,先把目录结构摸清楚。一个同时提供VOC和YOLO格式的数据集,常见做法是Annotations(XML标注)、JPEGImages(原图)、labels(YOLO的txt标注)三个目录平行存在,或者按images/train与labels/train这种YOLO惯例拆分。区别只在于组织方式,标注内容等价。

路径(常见布局)格式内容作用
JPEGImages/.jpg原始图片模型输入
Annotations/.xmlVOC标注:对象名、bndbox坐标、图片尺寸人类可读、便于画框检查
labels/.txtYOLO格式:类别ID + 归一化中心坐标 + 宽高直接喂给YOLO训练

拿到手先别急着训练,我一般会先做两件事:确认每个XML在labels里有对应txt,确认每个txt里没有空行和越界坐标。这种小数据集通常是人工或半自动标注的,最怕的是标注者用不同工具补标过几批,导致格式不统一,尤其是类别名大小写混用、某一两张图漏标。

2.2 372张图的样本结构与“小数据集”的真实含义

372张图,对目标检测来说非常小。以YOLOv8默认的COCO预训练权重为起点,做迁移学习,勉强够用;但要想让模型对“不同光照下的同款药盒”都稳定,这个量级必然吃紧。我的判断标准是:单类别检测,每类至少100~300个正样本框,且每个框在尺寸、角度、遮挡状态上要有差异。999感冒灵的包装盒外观相对统一,372张图里如果大多数是正面平拍,模型的泛化性会很差;如果混入了货架斜拍、多盒堆叠、部分遮挡,反而更有训练价值。

所以拿到数据的第一件事,不是配环境,而是统计。统计每个类别到底有多少框、每张图平均几个框、图片尺寸分布如何、有没有灰图或损坏文件。这些决定了后面要不要做数据增强、用多大的输入分辨率、验证集怎么划分。

2.3 训练前必做的数据体检:一张图对应几个框、框是否越界

这里给你一个可以直接跑的统计脚本,把JPEGImages和Annotations丢进去,它会输出图片数量、XML数量、类别分布、图片尺寸范围和异常文件,省得一张张翻文件夹。

import os import xml.etree.ElementTree as ET from collections import Counter from PIL import Image img_dir = "JPEGImages" ann_dir = "Annotations" imgs = [f for f in os.listdir(img_dir) if f.endswith(".jpg")] xmls = [f for f in os.listdir(ann_dir) if f.endswith(".xml")] print("图片数:", len(imgs), "标注数:", len(xmls)) name_counter = Counter() # 类别名统计 size_set = set() # 图片尺寸集合 missing_img = [] # 有xml但缺jpg的 for xml_name in xmls: tree = ET.parse(os.path.join(ann_dir, xml_name)) root = tree.getroot() # 检查对应图片是否存在 img_name = root.find("filename").text if img_name not in imgs: missing_img.append(img_name) width = int(root.find("size/width").text) height = int(root.find("size/height").text) size_set.add((width, height)) for obj in root.iter("object"): name = obj.find("name").text name_counter[name] += 1 print("类别统计:", name_counter) print("出现过的图片尺寸:", size_set) print("缺失图片的xml:", missing_img[:10])

这段脚本的逻辑很简单:遍历所有XML,记录类别名和图片尺寸,同时检查filename字段引用的图片是否真的存在于JPEGImages目录。缺失图片、类别名不一致、尺寸混乱,这三类问题都会在后续转换或训练时暴雷。参数上要注意root.find("size/width")这一层路径,不同标注工具生成的XML结构略有差异,有些把size节点放在object之前,有些把difficult、truncated等字段加在object里,脚本都能兼容,但如果遇到字段缺失,会抛AttributeError,到时再把对应XML单独打出来看就行。

3. 把VOC转成YOLO标注:一个脚本和四条边界规则

3.1 为什么必须转:YOLO只认txt,不认XML

VOC标注是把“框的左上角和右下角坐标”直接写进XML,而YOLO训练要求每张图对应一个txt,每一行是“类别ID、归一化中心x、归一化中心y、归一化宽、归一化高”。两者描述的是同一个框,但坐标系不同。如果你的zip里只有Annotations和JPEGImages,训练前就必须自己做转换;如果labels目录已经存在,你也最好抽样验证一下转换逻辑是否正确,因为很多数据集发布者用的转换脚本不一定处理了越界坐标。

转换的核心公式就两行:

x_center = (xmin + xmax) / 2 / img_width y_center = (ymin + ymax) / 2 / img_height box_width = (xmax - xmin) / img_width box_height = (ymax - ymin) / img_height

这四个值全部归一到0~1之间。注意,归一化的分母是图片宽高,不是标注框的宽高,新手在这里容易写反。

3.2 一个能直接用的VOC转YOLO脚本

下面这段是数据转换脚本,兼容单个XML和整个Annotations目录批量处理。

import os import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, class_map, out_dir): """把单个VOC XML转成YOLO txt,class_map是类别名到ID的映射""" tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): # difficult=1的对象建议跳过,避免干扰训练 if obj.find("difficult") is not None and int(obj.find("difficult").text) == 1: continue name = obj.find("name").text if name not in class_map: print(f"跳过未映射类别: {name} 在 {xml_path.name}") continue cls_id = class_map[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) # 边界裁剪:防止坐标越界 xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: print(f"非法框: {xml_path.name} {name}") continue x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: out_name = Path(xml_path).stem + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) # 示例调用:把Annotations下所有xml转换到labels目录 class_map = {"ganmaoling": 0} # 类别名->ID,按你的实际类别改 os.makedirs("labels", exist_ok=True) for xml_file in Path("Annotations").glob("*.xml"): voc_to_yolo(xml_file, class_map, "labels")

逻辑说明:先解析XML拿到图片宽高,再遍历object节点提取每个目标的类别和bndbox四角坐标。中间做了两步防御,一是跳过difficult标记的目标,二是把框坐标裁剪到图片边界内,避免标注时手滑标出图的负坐标。最后按YOLO五列格式写入txt,类别ID由class_map映射。

参数上你需要改动的只有class_map。药品数据集的类别名可能是999、ganmaoling、感冒灵这样五花八门的写法,务必先跑一遍第2章的统计脚本确认类别名的真实写法,再填进映射。另外,输出小数精度保留6位足够,文件路径用Path对象拼接比字符串加号更稳妥,Windows和Linux下都能跑。

3.3 转换后的四个边界坑:类别名不一致、空txt、重复框、图片尺寸不匹配

第一坑,类别名大小写不一致。同一个感冒灵,有的XML写ganmaoling,有的写Ganmaoling,直接导致转换时后者被当成未映射类别跳过,标注框莫名丢失。解决方法是统计脚本里先对类别名做Counter,发现大小写混用就用一个规范化字典统一映射。

第二坑,转换后有些txt是空的。常见原因是该XML里所有对象的类别都没进class_map,或者全部被difficult过滤。空txt在训练时会报label格式错误或直接忽略,你根本不知道那张图没参与训练。转换完要跑一行检查:打印所有字节数为0的txt,再回去看对应XML。

第三坑,重复标注框。同一个人在标注时,可能把同一个药盒框了两次,位置几乎重叠。YOLO训练对这种重复框不报错,但会重复计算损失,让模型对那个位置的响应过强。最简单的方法是转换时按IoU去重,两个框IoU大于0.9就只保留置信度高的那个。

第四坑,图片尺寸与XML里的size不一致。有的数据集图片被统一压缩过,但XML里的width/height没跟着改,归一化后的坐标全是错的。验证方法很直接:用PIL读图片实际尺寸,在程序里加一个断言,不一致时打印警告并优先使用图片真实尺寸做归一化分母。

4. 用YOLOv8训练这个小数据集:最小配置、损失函数与参数调法

4.1 数据YAML怎么写:路径、类别名、train/val划分

把数据准备好之后,YOLO训练前要写一个数据描述文件。这个YAML是Ultralytics框架的入口,路径写错或者类别名对不上,会直接在训练开始时报错。

# ganmaoling.yaml path: ./datasets/ganmaoling # 数据集根目录,绝对或相对当前工作目录 train: images/train # 训练图片目录 val: images/val # 验证图片目录 names: 0: ganmaoling # 类ID从0开始,与txt标注一致

数据集的图片要按train和val分好,而不是让YOLO自动划分。372张图这样的小数据集,自动划分容易造成验证集里混入与训练集高度相似的图片,导致评估虚高。我一般按9:1或8:2人工划分,同时保证同一盒药的不同角度照片只出现在train或只出现在val,否则模型在验证集上的成绩只能算记忆测试。

训练命令是:

yolo detect train data=ganmaoling.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640 patience=20

model用的是yolov8n.pt,nano版本,对这个任务足够轻。如果你的显卡只有4GB显存,batch降到8或4,imgsz降到480或320,不要硬扛。输出目录默认在runs/detect/train,训练结束后weights目录下会有best.pt和last.pt,验证和推理都用best.pt。

4.2 小数据集下损失函数的表现怎么看

YOLOv8训练时终端会实时打印三个损失:box_loss、cls_loss、dfl_loss。新手经常只看总loss往下掉就觉得没问题,其实拆分看才有效果。box_loss衡量预测框与真实框的位置差距,cls_loss衡量分类是否正确,dfl_loss是Distribution Focal Loss,负责让框的边界回归更精准。

小数据集上最常见的现象是:cls_loss快速降到很低,但box_loss和dfl_loss震荡甚至上升。原因是372张图里外观差异太小,模型很快记住了“这个类别是什么”,但框的位置边界始终学不扎实。遇到这种情况,不要盲目加epochs,先把imgsz提到640,让模型看到更多像素级细节;再检查标注框是不是普遍过大或过小,如果全是紧贴盒身的框,dfl_loss可能因为边界锐利而波动剧烈,这是正常现象,重点看验证集mAP是否同步上升。

4.3 372张图的训练策略:迁移学习、数据增强和早停

我训练这种小数据集的标准动作是三步。第一步,基于COCO预训练权重yolov8n.pt开始训练,而不是从零随机初始化,迁移学习能省掉大量“学边缘、学纹理”的epoch。第二步,打开数据增强,ultralytics默认的augmentation对它来说偏保守,你可以在训练命令里手动调大:

yolo detect train data=ganmaoling.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640 \ hsv_h=0.02 hsv_s=0.8 hsv_v=0.6 flipud=0.2 translate=0.1 scale=0.5

hsv_h是色调扰动,药品包装盒的颜色是特征之一,不要调太大,否则药盒颜色被改得不像药盒;scale=0.5允许图片缩放范围更大,模拟不同拍摄距离。第三步,设patience=20,连续20个epoch验证集mAP不涨就早停,小数据集过拟合极快,最后二十个epoch基本都在反复横跳,早停能帮你留下成绩最好的权重,而不是最后一个epoch的权重。

这三个参数我几乎每次都要调:batch、imgsz、patience。很多人只看epochs,其实在小数据集上patience比epochs重要得多,372张图训练100个epoch大概几分钟,但第40个epoch可能就已经是最优点了,后面全是过拟合。

5. 药品检测数据集训练的五个坑:过拟合、标注泄漏与格式陷阱

5.1 训练loss很好看,验证集mAP却很低

现象:终端打印的loss一路下降,cls_loss趋近于0,但epoch结束后验证集mAP@50只有0.2左右。

原因:过拟合。372张图片对模型来说太少,网络直接把训练图的背景纹理、光照条件记住了,压根没有学到“感冒灵包装盒”这个类别本身。尤其是数据集中如果同一张图多次出现(重复样本),模型学到的就只剩记忆。

解决:先查重复图片,用图片的MD5值去重;然后降低模型复杂度,从yolov8m换到yolov8n;再拉大数据增强强度,重点加scale和fliplr;最后减少训练轮数,把patience从20降到10,让训练提前终止在泛化最好的位置。

5.2 类别名大小写不一致,转换时静默丢框

现象:训练前统计txt里总框数,发现比XML里所有object节点数少了一截,但没有任何报错。

原因:第3.2节脚本里对未映射类别是打印一条提示后continue,如果类名Ganmaoling和ganmaoling并存,后者会被跳过。数据集如果是多人标注合并而成,这种问题几乎必现。

解决:转换前先跑第2.3节统计脚本,把Counter打印出的所有类别名列出来,逐一核对大小写和空格。规范做法是在脚本里加一行name = name.strip().lower(),统一转小写再做映射,一劳永逸。

5.3 标注框在图片边缘,越界坐标把训练搞崩

现象:训练刚开始就报ValueError: All bounding boxes should have positive height and width,或者某个epoch中途loss变成NaN。

原因:标注工具允许框拖出图片边界,xmin或ymin可能是负值,xmax或ymax大于图片宽高。YOLO在计算IoU时,负值和越界坐标会产生非法宽高。

解决:转换脚本里的边界裁剪不能省,xmin = max(0, min(xmin, img_w - 1))这一行就是后悔药。针对已有txt的情况,写一段脚本遍历labels,把越界值全部裁剪并重新归一化。裁剪后如果出现xmax <= xmin的退化框,直接删掉这一行。

5.4 图片文件损坏,训练中途读图失败

现象:训练跑到第七八个epoch,突然抛PIL.UnidentifiedImageError或者Image file is truncated,程序终止。

原因:数据在打包zip、拷贝、解压过程中图片文件损坏,或者某张jpg本来就是截断的。这类问题在372张的小数据集中随机出现一张,就能让你的训练中断,而且每次中断的epoch可能都不同,看起来非常像玄学。

解决:训练前对JPEGImages做一次全量完整性检查。用PIL逐个Image.open(img).load(),读取失败的直接移动到corrupted目录。不要删原文件,移动出去方便后面人工确认是否可修复。

from PIL import Image from pathlib import Path for img_path in Path("JPEGImages").glob("*.jpg"): try: img = Image.open(img_path) img.load() except Exception as e: print(f"损坏图片: {img_path} -> {e}") img_path.rename(Path("corrupted") / img_path.name)

这段脚本的PIL部分没有多余参数,关键是load()方法必须在open之后手动调用,否则拖延到训练时才真正读数据,问题就后置了。

5.5 划分验证集时泄漏:同一盒药的两个角度分别进了train和val

现象:验证集mAP高达0.95,部署到现场拍新的药盒,检测效果一塌糊涂。

原因:数据划分时用的是随机划分,没有考虑样本来源。同一盒感冒灵在不同角度、不同光线下的多张照片,如果一张进train一张进val,模型在验证时等于直接看到了训练过的同一个物体,评估成绩虚高。

解决:按“物体实例”而非“图片”划分。看文件名是否包含药品编号或拍摄批号,比如ganmaoling_01_a.jpg和ganmaoling_01_b.jpg属于同一个药盒,就应整组划入同一侧。实在没有编号信息,就用聚类思路:把所有图片按拍摄批次分组,批次为单位划分train/val,保证批次不交叉。

6. 模型落地的验证套路:先过混淆矩阵,再过手机实拍

最后一个阶段是验证模型是不是真的能用。我自己的验收套路分三层:第一层看训练输出里的混淆矩阵和PR曲线,确认这个类别没有系统性误检;第二层拿训练时没见过的现场照片做批量推理,看漏检率和误检率;第三层是拍一段货架视频跑实时推理,看帧率和小角度偏移下的稳定性。

混淆矩阵关注两处:一是背景被误判为感冒灵的比例高不高,二是感冒灵被漏判的比例高不高。药品包装盒之间颜色相近,如果矩阵里背景行有明显响应,就需要在推理时提高置信度阈值,默认0.25太宽容,建议调到0.4甚至0.5。视频推理用yolo predict model=best.pt source=test_video.mp4 conf=0.4,如果检测框在连续帧间跳来跳去,优先检查imgsz是不是太小,640下小包装盒的边缘特征不够,换成960或1280通常能稳定很多,代价是帧率下降。

我的习惯是训练结束后把best.pt和那一批corrupted图片名单一起存档,既留模型也留数据问题记录,方便下次扩采数据时避开同样的坑。372张图训练出的模型,承担不了“所有光照下都检测准确”的预期,但它足够让你看清这个方向上线的成本和效果差距。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 7:16:35

德国行李托运回国靠谱公司怎么选

我身边不少从德国回来的朋友&#xff0c;都吐槽过行李托运的糟心事&#xff1a;扔了半箱家当可惜&#xff0c;自己扛着转机太累&#xff0c;找个托运公司还容易踩坑。这几年中德往来越来越频繁&#xff0c;留德学生毕业返程、中资企业外派人员任期结束、旅居华侨回国定居、赴德…

作者头像 李华
网站建设 2026/10/8 7:16:20

基于关键词的数据采集、数据分析案例!

目录 一、网民情感分析 二、区域统计分析 三、内容话题分析&#xff1a;什么在驱动讨论&#xff1f; 四、负面问题归因 五、高赞评论与话题链 六、洞察与行动建议 对广州文旅 / 景区 / 商务部门 对内容创作者 / 餐饮商家 给计划去广州的游客 本案例的数据范围&#xf…

作者头像 李华
网站建设 2026/10/8 7:15:40

为什么 C++ 没有像 Python 那样拥有海量开箱即用的库?

C与Python生态差距的本质在于设计哲学差异&#xff1a;C追求零开销抽象与极致性能&#xff0c;导致ABI不稳定、编译模型复杂、二进制分发困难&#xff0c;库需源码编译&#xff0c;使用门槛高&#xff1b;而Python以运行时灵活性和统一接口实现“即插即用”&#xff0c;配合高效…

作者头像 李华
网站建设 2026/10/8 7:15:17

MazeSec-113

信息搜集 端口扫描 ┌──(kali㉿kali)-[~] └─$ nmap -A -p- 192.168.21.7 Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-07 05:38 -0400 Nmap scan report for 192.168.21.7 Host is up (0.00065s latency). Not shown: 65533 closed tcp ports (reset) PORT STAT…

作者头像 李华