简介:面向药品识别与目标检测场景,一款999感冒灵检测数据集可为计算机视觉学习者、算法工程师提供可直接投入训练的标注数据。资源围绕单一目标类别“999ganmaoling”构建,共372张jpg原图,每张图片都同时包含VOC格式xml与YOLO格式txt标注,总计573个真实标注框,便于进行单类别检测模型的训练、验证与迁移学习。包体共1119个文件,压缩后约20.88MB,其中jpg图像372张、xml标注373个、txt标注374个,整体体积适中,下载和搬运都很方便。数据集已用labelImg完成标注,同时提供两种主流格式,免去格式转换环节,可直接接入Pascal VOC或YOLO系列训练流程,也可按需做数据划分或数据增强。已有248人浏览学习,适合想快速上手目标检测实战、补充药品类样本或做数据增强实验的开发者使用。
1. 372张的999感冒灵检测数据集,怎么做才能训练出能用的目标检测模型
感冒药盒子这类外观稳定、拍摄背景却从药店货架换到柜台和批发箱的检测目标,372张图像看着不大,恰恰是练目标检测流程的合适样本量。这套999感冒灵检测数据集,图片内容集中在药品包装,同时交付VOC和YOLO两种标注格式,等于把最费时的标注环节前置,拿到之后可以直接走训练流程。它适合三类人:想快速跑通一个货架商品检测链路的新手,在VOC与YOLO两个框架之间反复横跳的老手,以及先拿小样本验证YOLO训练流程、随后再去扩数据的产品团队。真正决定成败的不是372这个数字,而是两套格式的坐标怎么对齐、372张怎么划分和增强、以及训练时那一堆指标到底该信谁。
2. 拿到压缩包先别急着跑:VOC与YOLO两套格式的坐标体系
解压后第一件事不是训练,而是确认标注格式长什么样。只要做一次完整流程就会明白:目标检测数据集最常见的拦路虎不是模型,而是标签和样本不匹配。下面按一个VOC+YOLO双格式数据集最常见的目录结构来看。
2.1 解压后先做三件事:数图片、列目录、看标注类别
先解压,再把目录结构列出来数一遍。先用这些命令:
unzip 999感冒灵检测数据集372张VOC+YOLO格式.zip find . -maxdepth 2 -type d | sortfind . -name "*.xml" | wc -l find . -name "*.txt" | wc -l第一段命令是看压缩包内部组织:有的压缩包把VOC和YOLO分两个根目录放,有的是混在一起再靠后缀区分。第二段命令统计标注文件数量,用于确认XML和txt文件数量是否接近图片数量。如果txt数量比图片少几十个,说明部分空样本没有标注文件,这在YOLO格式里是正常现象,空图片本来就不需要生成txt。
下一步需要统计类别名。写一行Python把所有XML里的<name>字段打出来,确认类别是999、999ganmaoling还是带空格的变体。LabelImg、CVAT这些目标检测常用标注工具导出的XML,类别名就藏在<name>节点里,不先看清楚,后面YAML文件里的names列表很容易写错。
2.2 VOC的XML标注:绝对像素坐标和object节点
VOC格式的标注文件是XML,和图片同名,放在Annotations目录下。一个典型的XML片段长这样:
<annotation> <filename>IMG_0001.jpg</filename> <size> <width>640</width> <height>480</height> </size> <object> <name>999ganmaoling</name> <bndbox> <xmin>120</xmin> <ymin>96</ymin> <xmax>312</xmax> <ymax>268</ymax> </bndbox> </object> </annotation>这段XML表示在640x480的图上框住一个药盒,左上角是(120,96),右下角是(312,268)。VOC坐标是绝对像素值,给定一张图,框的位置不会因为运行设备、部署分辨率变化而改变,这也是它适合作为中间交换格式的原因。标注工具导出VOC格式时,通常会带上<size>节点,但要注意:这个节点有时并不等于图片真实尺寸,后面转换时很容易在这里踩坑。
2.3 YOLO的txt标注:归一化坐标,一行五个数
YOLO格式的标注文件是txt,和图片同名,后缀不同。每一行代表一个目标,五个数值按顺序是:类别ID、中心点x、中心点y、框宽、框高。后四个值全部归一化到0到1之间,用坐标值除以图片实际宽度或高度得到。
拿上面XML里的框来算一笔账:
- 中心点x = (120+312)/2/640 = 0.3375
- 中心点y = (96+268)/2/480 = 0.3792
- 框宽 = (312-120)/640 = 0.3000
- 框高 = (268-96)/480 = 0.3583
对应的txt行:
0 0.3375 0.3792 0.3000 0.3583这就是同一张图在两套格式下的真实关系。VOC转YOLO没有玄学,就是除以图片宽高;反过来YOLO转VOC则是乘以宽高再还原边角坐标。核心只有一点:分母必须是图片真实尺寸,不能用XML里写的<size>替代。
2.4 VOC转YOLO的Python脚本:一边转一边把边界问题拦下来
很多数据集虽然标了VOC+YOLO两种格式,但偶尔会碰到只有XML没有txt的情况,需要自己转换。下面这段脚本是常见做法,可以直接保存成voc2yolo.py:
import os import glob import xml.etree.ElementTree as ET from PIL import Image def voc2yolo(xml_path, img_w, img_h, class_names): """把一个VOC XML文件转换成YOLO txt的一行行标注。""" tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: # 类别名对不上就跳过,避免把未知类别硬塞进训练集 print(f"跳过未知类别: {name}") continue class_id = class_names.index(name) b = obj.find("bndbox") xmin = float(b.find("xmin").text) ymin = float(b.find("ymin").text) xmax = float(b.find("xmax").text) ymax = float(b.find("ymax").text) # 核心换算:绝对坐标 -> 归一化中心点 + 宽高 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h bw = (xmax - xmin) / img_w bh = (ymax - ymin) / img_h # 越界框截断到0-1,避免训练时出现负坐标或超界坐标 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) lines.append(f"{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") return lines if __name__ == "__main__": # 改成解压后实际统计到的类别名 class_names = ["999ganmaoling"] xmls = glob.glob("Annotations/*.xml") for xml_path in xmls: # 这里直接读图片真实尺寸,别迷信XML里的size标签 img_path = xml_path.replace("Annotations", "JPEGImages").replace(".xml", ".jpg") if not os.path.exists(img_path): print(f"缺图,跳过: {img_path}") continue img = Image.open(img_path) w, h = img.size base_name = os.path.splitext(os.path.basename(img_path))[0] lines = voc2yolo(xml_path, w, h, class_names) if not lines: print(f"没有有效标注: {xml_path}") continue out_txt = f"labels/{base_name}.txt" with open(out_txt, "w") as f: f.write("\n".join(lines) + "\n")这段代码里,class_names的顺序就是训练YAML里names的顺序,改顺序会导致类别ID错位,所以转换前先确认这个列表。img_w和img_h直接用PIL读图拿真实尺寸,不读XML里<size>节点,这是避免坐标整体偏移最有效的一招。最后那组min(max(...))截断是把越界框拉回0到1区间,很多标注边缘目标时会标出负坐标,不截断的话YOLO训练容易直接崩。
3. 372张图怎么组织才不浪费:目录划分、数据自检和增强参数
数据集只有372张,每一张都不能浪费。如果划分随意,比如连续编号的前100张全是同一时间段拍的,验证集就会失真。这一章把标准目录、划分脚本和自检脚本一次讲清楚。
3.1 建目录并固定划分随机种子:训练验证集划分的先决条件
YOLOv8训练时默认按images/train和images/val两个目录找图,标签目录自动找并排的labels目录。先建标准目录:
mkdir -p datasets/ganmaoling/images/train datasets/ganmaoling/images/val mkdir -p datasets/ganmaoling/labels/train datasets/ganmaoling/labels/val然后写一个划分脚本。假设解压后的图片全在images/all,txt标注在labels/all:
import os import random import shutil all_files = [f for f in os.listdir("images/all") if f.endswith((".jpg", ".png"))] all_files.sort() random.seed(42) # 固定随机种子,保证下次跑还能得到同一份划分 random.shuffle(all_files) split = int(len(all_files) * 0.8) train_files, val_files = all_files[:split], all_files[split:] def move(files, img_dir, lbl_dir): for f in files: base = os.path.splitext(f)[0] # 图片和txt标注同名,复制时同步搬运,避免训练时找不到label shutil.copy(os.path.join("images/all", f), os.path.join(img_dir, f)) shutil.copy(os.path.join("labels/all", base + ".txt"), os.path.join(lbl_dir, base + ".txt")) move(train_files, "datasets/ganmaoling/images/train", "datasets/ganmaoling/labels/train") move(val_files, "datasets/ganmaoling/images/val", "datasets/ganmaoling/labels/val")这段脚本先把文件名读出来、排序,再用固定随机种子打乱,最后按8:2切分。372张按这个比例训练集约298张,验证集约74张。关键点是图片和txt同步复制,训练集和验证集之间不能有同名图片混合。如果数据集采集时存在多场景,最好先按场景目录分组,再从每个场景各自抽20%进验证集,这样验证分布更贴近真实环境。
3.2 训练前的数据自检:把坏图、空标注、越界坐标一次查完
数据量小的时候,最容易出现一个脏文件拖垮整个训练。训练前跑一遍自检脚本,能在几分钟内发现问题,省下训练到一半才发现tensor崩溃的时间:
import os from PIL import Image image_dir = "datasets/ganmaoling/images/train" label_dir = "datasets/ganmaoling/labels/train" for f in sorted(os.listdir(label_dir)): if not f.endswith(".txt"): continue base = f[:-4] img_path = os.path.join(image_dir, base + ".jpg") # 检查1:图片是否存在 if not os.path.exists(img_path): print(f"缺图: {base}") continue # 检查2:图片能不能正常解码 try: with Image.open(img_path) as im: im.load() except Exception: print(f"坏图: {img_path}") # 检查3:空标注 with open(os.path.join(label_dir, f)) as fh: lines = [ln.strip() for ln in fh if ln.strip()] if not lines: print(f"空标注: {base}.txt") # 检查4:格式和坐标范围 for ln in lines: parts = ln.split() if len(parts) != 5: print(f"格式错误: {base}.txt -> {ln}") continue cid = int(parts[0]) if cid != 0: # 只有0一个类别时,出现其他ID直接报错 print(f"类别越界: {base}.txt -> {ln}") _, cx, cy, bw, bh = map(float, parts) if max(cx, cy, bw, bh) > 1.0: print(f"坐标越界: {base}.txt -> {ln}")自检覆盖四类高频问题:图片不存在、图片损坏、标注文件为空、坐标或类别ID越界。其中图片损坏和坐标越界最容易引起训练中BN统计异常,后续出现NaN时往回排查,十有八九是这里出问题。注意脚本里找图片只认.jpg后缀,如果原数据里有PNG图,要把base + ".jpg"改成同时尝试.png,或者用glob匹配,否则会误报缺图。
3.3 小数据集的在线增强:Mosaic、翻转和HSV各自该开多大
372张图硬扛100轮训练必然过拟合,在线增强是性价比最高的应对方式。YOLOv8把增强参数直接暴露成训练参数,不必离线生成增强图就能在每轮训练时实时变换:
yolo detect train ... \ mosaic=1.0 mixup=0.2 \ scale=0.5 translate=0.2 \ fliplr=0.5 flipud=0.0 \ hsv_h=0.015 hsv_s=0.7 hsv_v=0.4mosaic=1.0表示每张训练图都有一半概率由四张图拼接而成,对于想在同一个货架上同时检出多个药盒的场景特别有利。mixup=0.2是两张图按比例融合,能让模型在目标重叠时依然保持定位稳定,但开太大容易让药盒纹理糊成一团。flipud=0.0值得一提:药盒文字是竖向排列还是横向排列取决于拍摄角度,如果数据里不存在倒置摆放的样本,就别用上下翻转,否则会凭空造出大量不符合真实分布的假样本。HSV三个参数控制颜色扰动,药店灯光偏暖偏冷很正常,给色相一点偏移空间能提升抗干扰能力,但hsv_h超过0.02会让蓝色包装直接变调。
在线增强不改变硬盘上的文件,只在每次训练迭代时从原图临时变换,这样存储成本低,也能在每轮训练动态看到不同的组合。如果后面发现验证集和训练集之间光照差异太大,优先调HSV,而不是急着加离线增强副本。
4. 用YOLOv8训练自己的999感冒灵检测:从预训练权重到参数调整
数据准备好之后,接下来走一条最小训练链路。前提是你已经有Python环境,机器上有没有GPU都能跑,只是速度和batch大小不同。
4.1 环境准备与预训练权重:不需要从零训练
YOLOv8用ultralytics这一个包就能覆盖训练、验证和推理。先建虚拟环境并安装:
python -m venv .venv source .venv/bin/activate pip install -U ultralytics环境装好之后,第一轮训练不需要手动找预训练模型,model=yolov8n.pt这个参数会让程序自动下载官方权重。对372张的小数据集,从yolov8n起步是理智选择:yolov8s或更大的模型容量高,小样本下更容易把训练集细节背下来,验证集分数反而不升。预训练权重带来的好处是模型已经见过大量自然图像特征,药盒的边角、纹理和文字都算常见特征,微调几百张就能收敛。
如果机器没有GPU且内存紧张,训练时把batch降到8,imgsz降到480,训练时间会增加但流程不变。不要在CPU机器上盲目跑大模型,等会训练到一半内存不够,返回去再改参数才是真浪费时间。
4.2 训练命令与必调参数:epochs、imgsz、batch和lr0
最简训练命令如下:
yolo detect train \ model=yolov8n.pt \ data=datasets/ganmaoling/dataset.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.005 \ patience=20 \ seed=42 \ project=runs/detect name=999g这里的dataset.yaml是上一章建好的数据集配置文件,内容类似:
path: datasets/ganmaoling train: images/train val: images/val names: 0: 999ganmaoling几个参数按小样本场景分别说。epochs=100看似多,实际YOLOv8有早停机制,patience=20表示验证集指标连续20轮不涨就自动停,对372张数据集来说通常40到60轮就到顶了。imgsz=640是输入分辨率,药盒在图片里占的比例变化很大,640能兼顾小目标和显存开销。batch=16在显存允许时不用刻意调大,预训练微调阶段batch大小对最终精度影响有限。lr0=0.005比官方默认的0.01小一半,原因是小数据集上较大的学习率容易让预训练权重在前几轮就被冲坏,尤其当标注框本身不太完美时,大学习率会放大噪声。训练输出在runs/detect/999g目录下,best.pt是验证集指标最好的权重,last.pt是最后一轮权重。
4.3 训练中途NaN、BN崩溃或断电:resume恢复和results.csv排查
训练到一半出现NaN或者train loss飙升是常见翻车现场。原因往往有两种:一是数据里有坏图或坐标越界的标注,BN统计被异常张量污染;二是学习率相对数据量太大,梯度在个别batch上爆炸。前者用上一章的自检脚本查出来删掉,后者把lr0降到0.001再重训。
如果只是断点续训,不需要从头再来:
yolo detect train \ model=runs/detect/999g/weights/last.pt \ data=datasets/ganmaoling/dataset.yaml \ resume=True这里resume=True会从last.pt继续,把训练状态恢复到中断时的epoch,而不是重新跑。注意不要用best.pt去resume,因为best.pt是验证指标最好的状态,但它所在的epoch不一定和训练state匹配,续训会丢掉一部分优化器状态。判断训练是否真的出了问题,可以看runs/detect/999g/results.csv:
tail -20 runs/detect/999g/results.csv这个文件每行一个epoch,包含train loss、val loss、mAP等指标。如果train loss降但val loss连续20轮不动,那就是过拟合信号,训练会在patience触发时自动停,不必手动中断。
5. 372张级别避坑记录:格式转换、类别名和过拟合
这一章是血泪经验汇总。每个问题都按现象、原因、解决的顺序写,处理完这四条,基本能排除大部分常见坑。
5.1 类别名不一致导致标注被吞
现象:训练时日志显示的实例数量明显少于预期,或者train loss正常但验证mAP始终为0。
原因:XML里标注的<name>字段和YAML里的names列表不一致。比如XML里写的是999感冒灵,YAML里写的是ganmaoling,转换脚本看到未知类别名就直接跳过,等于一张图里的目标全被丢弃。更隐蔽的情况是类别名中间有全角空格,肉眼看不出来。
解决:先遍历所有XML统计真实类别名,再写YAML:
import xml.etree.ElementTree as ET import glob names = set() for xml in glob.glob("Annotations/*.xml"): root = ET.parse(xml).getroot() for obj in root.findall("object"): names.add(obj.find("name").text) print(names)统计结果里出现几个名字,YAML的names就写几个,类别顺序一旦确定就不要中途改。转换后还要抽查几个txt文件,确认里面的类别ID和预期一致。
5.2 坐标转换后所有框都偏:别信XML的size,要读原图
现象:转换完用可视化工具查看,检测框位置整体偏移,有的框超出图片边界,有的框比目标大一圈。
原因:转换脚本用了XML里<size>节点记录的分辨率,这个值和图片真实像素不一致。常见于数据集被resize过但XML没同步更新,或者标注工具自动填充了错误参数。宽度差几个像素时看不太出来,一旦宽高差到几十像素,所有框都会整体跑偏,验证集mAP直接被拖下去。
解决:转换脚本里用PIL或OpenCV直接读图片真实尺寸作为除数。转换完成之后,随手写一行可视化代码,把标注框画回原图上,肉眼抽查十张。框和药盒边缘偏差超过两个像素,就要回头检查图片加载路径和标注文件的对应关系。
5.3 train loss走低但val mAP上不去:小样本过拟合
现象:训练到第40轮,train loss一路降到0.5以下,但验证集mAP卡在60%左右一直不涨,甚至后续轮次还掉几个点。
原因:372张对YOLOv8n来说依然偏少,模型在反复看训练集的过程中记住了药盒的具体摆放和背景纹理,而不是泛化出"在任意货架角度都能找到药盒"的能力。train loss低只代表模型对训练集拟合好,验证指标才能反映真实泛化水平。
解决:先确认早停参数patience=20已经生效,不要硬跑满100轮。然后增强力度往上调,尤其mosaic和mixup这两个参数对小样本最有效。再不行就换更小的模型,从yolov8n换到yolov8n其实已经是nano,不要再降;如果用的是yolov8s,先退回n版本。最后一条路是补充数据,比调参更直接有效。
5.4 验证指标还行但真图漏检:阈值和验证分布
现象:验证集mAP有75%,但把训练好的模型放到一张自然光下的药店货架图上,漏检了一半药盒。
原因:验证集划分太"干净",随机打乱时把同一场景下拍摄角度几乎一致的图片同时放进了训练集和验证集,模型在验证集上高分,一遇到真实场景的货架透视角度、反光和遮挡就熄火。另一层原因是推理阈值设太高,验证时用的是置信度0.5,真实检测时还把阈值保留在0.5以上,很多低置信度的真目标被过滤掉。
解决:推理阶段先用conf=0.25跑一批真实图片,看看漏检目标落在哪个置信度区间。如果漏检目标大多是0.3到0.5之间,就把阈值降到0.25,同时用iou=0.45做NMS,避免同一个药盒出多个框。验证集划分改成按拍摄场景分组后抽取,才能让mAP有参考意义。
6. 验证模型并反哺数据集:372张只是一个起点
模型训练完成后,最后一段路是拿真实场景图片验证并筛选错误样本。先把best.pt放到一批没参与训练的真实图片上跑一遍:
yolo detect predict \ model=runs/detect/999g/weights/best.pt \ source=real_test/ \ conf=0.25 iou=0.45 \ save=True输出图片会标注框、置信度和类别名。先挑那些低置信度的漏检目标看,再挑误检目标看。如果漏检主要发生在药盒重叠、盒面反光或包装有塑料袋遮挡时,不要急着调参,把这些图片找出来补标,加进训练集重训一轮。常见做法是每轮增加50到100张新增样本,连续三轮增量训练之后,模型对这个场景的稳定性通常会有明显改善。
我自己在这个方向吃过亏:第一次迭代只看验证集mAP,感觉很稳,结果部署到真实柜台照片上漏检率接近三成。后来养成的习惯是每个项目都保留一批固定的真实场景测试图,不参与训练也不参与验证,每轮训练后跑一遍,通过错误图片决定下一轮补数据方向。对372张这种规模的数据集来说,增量数据永远比花时间调增强参数更值得投入。希望帮到你。
本文还有配套的精品资源,点击获取