简介:这是一个面向目标检测实战的药品包装检测数据集,聚焦板蓝根颗粒袋装与999感冒灵颗粒两类常见药品包装的识别定位,适合入门级目标检测项目练习、模型快速验证以及药品货架/包装识别场景测试。数据包共335个文件,以111张jpg原图、VOC格式xml标注文件和YOLO格式txt标注文件为主,压缩包整体仅3.71MB,使用labelImg人工标注,文件体量轻、便于快速下载与迭代。标注信息完整且类别区分清晰,总计134个目标框,其中板蓝根颗粒袋85个、999感冒灵49个,可用于训练和评估基于VOC/YOLO格式的目标检测模型。资源已有121人学习关注,适合正在学习YOLO、Fast R-CNN等目标检测算法,或需要少量实测数据进行流程测试的数据科学初学者、算法实习生与高校实验课程使用。
1. 药品板蓝根颗粒检测数据集只有110张,为什么还值得跑一遍检测流水线
药品外包装质检里,板蓝根颗粒的破损、异物和装量差异,本质上是一个目标检测问题。手头这套药品板蓝根颗粒检测数据集总共110张图,同时给了VOC和YOLO两种格式,量小得可怜,但做过检测流水线的人都清楚:小样本不是死刑,标注错位才是。110张足够你把数据体检、格式对齐、YOLOv8训练和错检回灌跑完一个闭环,用一轮迭代去判断质检方案可不可行,再决定要不要投入更大规模的采集。它适合正在做医药外观检测、以及准备用YOLO接手小批量质检场景的工程师。数据集的直接价值在格式齐、场景单一,不在数量大。
2. 拆开zip看门道:VOC与YOLO两套目录结构与标注文件的对应关系
拿到zip,第一步不是解压就训练,而是先确认两套格式的布局。很多平台的标注导出都是VOC和YOLO同时给,如果不把目录结构理清,后面写脚本会反复返工。这一章的结论一句话:VOC用像素坐标的xml,YOLO用归一化坐标的txt,两者描述同一个目标,坐标空间完全不同,对齐工作比训练本身更花时间。
| 维度 | VOC格式 | YOLO格式 |
|---|---|---|
| 标注文件 | xml | txt |
| 坐标含义 | 左上角xmin、ymin和右下角xmax、ymax,像素值 | 中心点x_center、y_center和宽高width、height,归一化 |
| 类别写法 | object节点下的name字符串 | 每行开头的整数编号,对应names顺序 |
| 一个图片对应 | 一个同名xml | 一个同名txt |
2.1 标准VOC布局:JPEGImages、Annotations与ImageSets/Main
我见过的大多数VOC风格数据集会按Pascal VOC的约定排列,解压后三个目录、一个文本组。JPEGImages放原图,Annotations放同名xml,ImageSets/Main放的是按空格分隔的图片名列表,用来划分训练、验证和测试。xml里重点看四个节点:filename、size下的width和height、object下的name和bndbox。bndbox给的是左上角xmin、ymin和右下角xmax、ymax,单位是像素,不是归一化数值。这个像素坐标是VOC和YOLO最大的分水岭,后续所有转换都从它出发。
用Python读这种xml不算复杂,ElementTree就够用。下面这个脚本遍历Annotations目录,把每个xml的类别和bndbox打印出来,同时核对filename字段和实际文件名是否一致:
import xml.etree.ElementTree as ET from pathlib import Path annot_dir = Path("Annotations") for xml_path in sorted(annot_dir.glob("*.xml")): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") size = root.find("size") img_w, img_h = int(size.findtext("width")), int(size.findtext("height")) boxes = [] for obj in root.findall("object"): name = obj.findtext("name") bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) boxes.append((name, xmin, ymin, xmax, ymax)) print(f"{xml_path.stem}: {img_w}x{img_h}, {len(boxes)} objects, filename={filename}") for box in boxes[:3]: print(" ", box)逻辑是先按文件名排序,逐个解析size和object节点,再打印前三个框确认坐标范围。参数说明:img_w和img_h在转YOLO格式时当分母,如果xml里的size和真实图片分辨率不一致,转化出来的坐标全错,这是最容易忽略的一环。建议打印结束后随机挑两张图,把坐标用OpenCV画上去肉眼核对一遍,比写一百行校验代码都管用。画框用rectangle,坐标直接用xmin那一串,不要先归一化再还原,多一次变换多一个出错点。
2.2 YOLO格式的labels目录:txt归一化坐标与类别编号
YOLO训练不读xml,只读txt。常规布局是images目录下放jpg,labels目录下放同名txt,文件名一致但扩展名不同。txt每行一个目标,五个字段:class_id、x_center、y_center、width、height。前四个都要归一化,中心点x除以图宽、中心点y除以图高、目标宽除以图宽、目标高除以图高。class_id从0开始编号,对应data.yaml里的names列表顺序。
为什么YOLO强制归一化?因为训练时输入会resize到640乘640,目标在640坐标系里的位置随原图分辨率变化,归一化之后才能在不同尺寸输入下保持同一个语义。从VOC转过来时,最容易出错的不是公式,而是把左上角坐标当成中心点来算。VOC转YOLO的标准公式:
- x_center = (xmin + xmax) / 2 / img_w
- y_center = (ymin + ymax) / 2 / img_h
- width = (xmax - xmin) / img_w
- height = (ymax - ymin) / img_h
注意宽高是目标真实宽高除以整图宽高,不是除以目标框宽高,也不是除以640。我把这个公式直接写成转换函数,输入xml路径和图片尺寸,输出txt内容:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_w, img_h): root = ET.parse(xml_path).getroot() lines = [] class_map = {"granule": 0} # 按实际类名调整,多类别时继续追加编号 for obj in root.findall("object"): name = obj.findtext("name") if name not in class_map: continue bndbox = obj.find("bndbox") xmin = int(float(bndbox.findtext("xmin"))) ymin = int(float(bndbox.findtext("ymin"))) xmax = int(float(bndbox.findtext("xmax"))) ymax = int(float(bndbox.findtext("ymax"))) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") return "\n".join(lines)参数说明:class_map这行是关键,txt里的第一个数字不是类别名称,而是类别在names列表里的索引。很多人把类别名直接写进txt,训练时类别数变成字符数加一,这种错误通常在loss正常但val的mAP为0时才暴露。另一个容易翻车的地方:xmin加xmax除以2,得到的是中心点;如果误写成xmax减xmin除以2,就得到宽度的一半,框全偏左上角。这里的img_w、img_h必须用原始图片的宽高,而不是xml里size字段之外的任何值。
2.3 双格式交叉校验:用Python脚本对齐图片、标注与ID
VOC和YOLO两套标注可能由不同工具或平台导出,经常出现xml有框但txt漏框、或者图片改名后两边不同步。直接训练,YOLO会跳过没有txt的图,val集合悄悄变小,精度数字看着还行,落地一推理全是漏检。我一般会先做一次交叉校验,把三个目录的stem集合拿来做差集:
from pathlib import Path jpg_dir = Path("JPEGImages") xml_dir = Path("Annotations") txt_dir = Path("labels") jpgs = {p.stem for p in jpg_dir.glob("*.jpg")} xmls = {p.stem for p in xml_dir.glob("*.xml")} txts = {p.stem for p in txt_dir.glob("*.txt")} print("缺xml的图:", jpgs - xmls) print("缺txt的图:", jpgs - txts) print("有txt没图的:", txts - jpgs) for stem in sorted(jpgs & txts): txt = txt_dir / f"{stem}.txt" for i, line in enumerate(txt.read_text(encoding="utf-8").splitlines()): parts = line.split() if len(parts) != 5: print(f"{stem} 第{i}行格式错误: {line}") continue _, xc, yc, w, h = map(float, parts) if xc - w/2 < -0.02 or xc + w/2 > 1.02 or yc - h/2 < -0.02 or yc + h/2 > 1.02: print(f"{stem} 第{i}行越界: {line}")逻辑是三步:收集三个目录的文件主名,做集合差集,再逐行检查txt格式和越界。参数说明:编码用utf-8而不是默认的gbk,Windows下txt经常以gbk写入,读出来报UnicodeDecodeError;行数不是5说明这行被回车截断或混入空行。越界容差0.02,低于这个不报,因为边缘物体的标注本来就容易探出边界。集合差集输出空是正常情况,一旦有输出就要回到标注工具里补。还有一个更直接的检查:抽三张图,用txt坐标画框,和原图叠在一起看一眼,颗粒是圆形,框应该贴合边缘,如果框明显偏移,多半是除以了错误的图宽高。
3. 训练前强制体检:110张小数据的3个必查项与可用检查脚本
小样本数据集最怕的不是数量少,而是坏数据占比高。110张图如果有一半标注框越界或漏标,模型会把背景学进特征里去。训练前体检不是玄学,是标准的工业流程。这一章给三个必查项:框质量、类别分布、数据划分,每个都有可复制的脚本。
3.1 框质量:接近零尺寸、越界与宽高比异常
标注工具缩放到小图时,很容易产生只有两三个像素的框。这种框在原始分辨率下肉眼看不出来,在640输入下更是一个噪声点,YOLO会为它输出一个近乎随机的小框。逐条检查越界和过小的问题,用一段小脚本读目录下全部txt:
from pathlib import Path txt_dir = Path("labels") imgsz = 640 bad = [] for txt in txt_dir.glob("*.txt"): for i, line in enumerate(txt.read_text(encoding="utf-8").splitlines()): parts = line.split() if len(parts) != 5: bad.append(f"{txt.name}:{i} 列数异常") continue xc, yc, w, h = map(float, parts[1:]) if w * imgsz < 5 or h * imgsz < 5: bad.append(f"{txt.name} 目标过小 w={w:.4f} h={h:.4f}") if xc - w/2 < -0.02 or xc + w/2 > 1.02: bad.append(f"{txt.name} 中心x越界 {xc:.4f}") if yc - h/2 < -0.02 or yc + h/2 > 1.02: bad.append(f"{txt.name} 中心y越界 {yc:.4f}") for item in bad: print(item)逻辑是把txt按行拆开,每行解析出5个字段,再换算成640尺度下的像素宽度。参数说明:w乘以imgsz是换算公式,imgsz取训练时的输入尺寸,缺省640;小于5像素的框直接列为过小,因为颗粒在640图上至少有十来个像素,5像素以下基本是误标。越界判定里的0.02是容差,某些数据集的边缘物体标注本来就会出边界,容差内先放行,等人工复核。这里不直接删框,只打印名单,删框是不可逆操作,先把问题暴露出来再逐个决定。
3.2 类别与每图目标数分布
板蓝根颗粒检测场景相对集中,但也要确认类别到底是一个还是多个。统计每张图的目标数和类别编号分布,能提前判断两个问题:一是是否有类别只在训练集里出现一两次,二是是否有大量图片一个框都没有。第二种最隐蔽,YOLO会把空图当成纯背景来压低误检,但空图占比过大,模型会倾向保守漏检。
统计脚本只需要一个Counter:
from collections import Counter from pathlib import Path txt_dir = Path("labels") cls_counter = Counter() per_img_counts = [] for txt in txt_dir.glob("*.txt"): lines = [l for l in txt.read_text(encoding="utf-8").splitlines() if l.strip()] cls_counter.update(int(line.split()[0]) for line in lines) per_img_counts.append(len(lines)) print("类别统计:", cls_counter) print("每图目标数分布:", Counter(per_img_counts)) print("空图张数:", sum(1 for c in per_img_counts if c == 0))逻辑是把每个txt的行数视作该图的目标数量,再统计类别编号和空图张数。参数说明:如果空图超过图片总数的15%,建议先补标一批有目标的图,而不是直接训练;如果某个类别编号只出现两三次,单独开一个检测头去学它,结果基本靠运气。我一般不用现成的可视化工具,直接看终端输出就够了,数字本身已经能揭示不平衡程度。还要注意类别编号必须从0开始连续排列,如果出现了class_id=5但中间缺了编号,data.yaml里的names顺序就有一半是错位。
3.3 划分策略:按组切分而不是随机打乱
110张图划分训练和验证,至少不能简单随机打乱后按比例切。同一批次拍摄的板蓝根颗粒图,光照和背景几乎一样,随机打乱会让验证集分布和训练集高度重叠,模型mAP虚高,一到现场就崩。我常用的做法是先按拍摄批次或文件名前缀分组,以组为单位切分,让val里的场景是train没见过的。
给出按文件名前缀分组划分的脚本:
from pathlib import Path import random, re jpgs = sorted(Path("JPEGImages").glob("*.jpg")) def group_key(stem): parts = re.split(r"[_-]", stem) return parts[0] if len(parts) > 1 else stem[:2] groups = {} for p in jpgs: groups.setdefault(group_key(p.stem), []).append(p.stem) random.seed(42) names = list(groups) random.shuffle(names) train, val = [], [] for g in names: if len(train) / len(jpgs) < 0.8: train.extend(groups[g]) else: val.extend(groups[g]) print("train:", len(train), "val:", len(val))逻辑是先把文件名按下划线或中划线拆出批次前缀,相同前缀归到同一组,再以组为单位切分到train和val。参数说明:0.8是train的目标占比,110张对应约88张train和22张val;seed固定成42,保证每次运行划分一致。如果文件名没有批次信息,脚本会自动退化成按前两个字符分组,此时相当于半随机,效果比纯随机好一点,但仍建议人工先看一眼哪些图是同一场景。划分完把train和val的图片清单各自存成txt,后续训练和换模型都复用同一份划分,否则不同实验之间没有可比性。另外,如果val里的目标总数不足20个,验证mAP方差大得没法看,可以把比例调成9:1,照样是小样本常规操作。
4. 用YOLOv8把110张图训练成板蓝根颗粒检测器:最小命令与关键参数
体检完就该训练了。选YOLOv8还是更晚的版本,看团队环境,我的经验是别盲目追新,先让一个稳定版本跑通流程。110张图属于极小样本,网络结构、增强策略、优化器三个地方都要调整,不是把图片丢进去等结果就行。
提示:训练前先跑一遍第3章的体检脚本,能省掉后面大半的排查时间。
4.1 安装与数据组织:YOLO官方目录约定
安装ultralytics包,一行pip。数据目录按YOLO约定的布局来摆,train和val下各放images和labels,图片在images下,txt在labels下,两个目录的文件主名必须完全一致:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml里写路径、类别数和类别名:
path: /home/user/dataset train: images/train val: images/val nc: 1 names: 0: granule参数说明:path用绝对路径,训练脚本和数据集不在同一目录时,相对路径容易解析到当前工作目录,折腾一次就不想再有第二次。nc必须和txt里的最大class_id加一相等,names顺序要和txt里的编号一一对应。类名写成granule只是示意,实际以标注里的类名为准,YOLO不要求英文名符合语义。安装命令:
pip install ultralytics装完可以用python -c "import ultralytics; print(ultralytics.version)"验证。数据集目录不要放在中文路径下,某些版本的第三方库对中文路径处理有历史遗留问题,容易在读取图片时崩掉。
4.2 训练命令与5个必调参数
训练命令本身很短,难在参数怎么设。110张图的数据量,参数设不好,训练一小时白费。去掉大模型和复杂trick,实际起作用的就这几个:
yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ optimizer=AdamW \ lr0=0.001 \ patience=30 \ mosaic=0.5 \ seed=42逐个解释。model=yolov8n.pt用的是预训练权重,不要用yolov8l或者更大的模型,110张图加大模型等于把过拟合写死在参数里。epochs=150配合patience=30,意思是150轮封顶,但连续30轮val mAP不涨就提前停,小样本普遍在80到120轮之间收敛。batch=16是显存和稳定性的折中,batch太小梯度噪声大,AdamW都救不回来;batch太大又受显存限制。imgsz=640是默认输入尺寸,如果板蓝根颗粒在图里小于32像素,建议改成768,代价是训练和推理都变慢。optimizer=AdamW配上lr0=0.001,是我在小样本检测上最稳的组合,SGD配0.01对这个小数据量容易在前期震荡。mosaic=0.5把默认的马赛克增强概率减半,原因下一小节细说。seed=42让整个训练过程可复现,换机器重跑结果一致,排查问题方便得多。
有人习惯在命令后面加plots=True,把P-R曲线和混淆矩阵存到runs/detect/train目录下,这不影响训练结果,但能让收敛趋势看得更清楚。训练结束后自动生成weights/best.pt和weights/last.pt,best是按val mAP评出来的,实际部署用best,不要手滑用了last。
4.3 数据增强救小样本:YOLOv8默认增强与人工增强的取舍
YOLOv8默认把马赛克增强开到接近1.0,即几乎每个训练batch都合成四宫格,同时对每张图做HSV扰动和随机翻转。对1000张以上的数据集,这套组合非常强。但110张的小样本,马赛克会把颗粒裁剪得只剩半颗,模型学出来的特征偏向边缘而非整体形状,在验证集上看到的现象是mAP漂亮,换成整袋颗粒就漏检。常见做法是把mosaic降到0.5,mixup降到0,保留fliplr=0.5和HSV小幅扰动,再补一点scale和translate让模型对尺度变化不敏感。
把这些直接写进训练命令,别放到data.yaml里:
yolo detect train \ data=data.yaml model=yolov8n.pt \ epochs=150 imgsz=640 batch=16 mosaic=0.5 mixup=0 \ hsv_h=0.015 hsv_s=0.5 hsv_v=0.4 \ scale=0.3 translate=0.1 fliplr=0.5参数说明:scale=0.3允许目标在0.7到1.3倍尺度之间随机缩放,颗粒是小物体,稍微放大一点对漏检的改善比平移明显。translate=0.1控制目标在图中最多平移10%,避免颗粒总是贴在图片中心。hsv三个值分别控制色调、饱和度、亮度的扰动幅度,板蓝根颗粒是棕色系,色相扰动太大容易让颗粒变成灰色或绿色,我把hsv_h压到0.015,保留一点变化但不失真。mixup=0直接关掉样本混合,对几十张的小样本,mixup会稀释颗粒的纹理特征,收益不如损失多。这组参数是血泪经验换来的,先用全默认跑一遍看loss曲线,哪个增强明显让val loss反弹,就关掉哪个。
5. 小样本训练避坑:5条从标注、格式到收敛的真实踩坑记录
小样本训练翻车的地方,不在模型结构,而在数据链路。我把带人做检测项目时最高频的几类问题整理成5条,每条按现象、原因、解决展开,你能直接对上自己的报错或诡异曲线。
5.1 现象:loss正常下降,val的mAP一直是0
原因几乎总是类别编号和names列表对不上。txt里写了class_id=3,data.yaml里nc=2,模型把类别索引当超过边界处理,推理时输出全被过滤。另一种情况是data.yaml的names顺序和标注工具导出顺序不一致,比如工具里颗粒类排第0,你写配置文件时把别的类放第0,loss照样能收敛,但val的mAP就是0。解决:训练前用3.2节的Counter脚本打印txt里的类别编号,再打开data.yaml核对names顺序。还有个自测办法:训练完用best.pt对任意一张val图推理,打印结果框的类别号和置信度,如果类别号全为0而图里明明有别的类,就是编号问题。
5.2 现象:loss先降后涨,val mAP震荡得像心跳
原因在小数据集上通常是三件事叠加:batch太小、mosaic全开、学习率偏高。batch=8时每个batch只有8张图的统计量,梯度方向不稳定;mosaic合成图让目标上下文频繁切换;lr0=0.01的SGD在小样本上相当于在loss曲面里反复横跳。解决:直接改成batch=16起步,mosaic降到0.5,optimizer换成AdamW并把lr0压到0.001。改完如果还在震荡,再把epochs从150提到300,早停阈值放宽到40轮,让模型在最低点附近多游一会儿。这套组合在多个小样本检测任务里都有效,我一般先按它跑一遍再微调。如果显存实在不够,batch=8也可以用,但要记得把lr0降到0.0005,让梯度更新步子小一点,震荡幅度会低一些。
5.3 现象:VOC转YOLO后画框全偏到左上角
原因:公式写错。最常见的是把x_center算成(xmax - xmin) / 2 / img_w,算出来的是目标宽度的一半,不是中心点;还有人把width算成(xmax - xmin) / 2,认为宽高也要除以2,结果框只有实际一半大小。解决:把公式抄对之后,用三个文件做人工验证:挑一张颗粒靠右下角的图,转完txt后和xml画在同一画布上对比;或者把txt坐标还原成像素坐标画框,代码就几行:
import cv2 img = cv2.imread("JPEGImages/xxx.jpg") with open("labels/xxx.txt", encoding="utf-8") as f: for line in f: _, xc, yc, w, h = line.split() xc, yc, w, h = float(xc), float(yc), float(w), float(h) x1 = int((xc - w / 2) * img.shape[1]) y1 = int((yc - h / 2) * img.shape[0]) x2 = int((xc + w / 2) * img.shape[1]) y2 = int((yc + h / 2) * img.shape[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("check.jpg", img)逻辑是把归一化的中心点和宽高还原成左上、右下像素坐标,再画绿框。参数说明:img.shape[1]是宽,img.shape[0]是高,两者不能写反;还原后框应该和xml映射的框重合,任何一厘米的偏移都说明转换脚本里有bug。注意cv2.rectangle要求坐标是整数,先转成int再画,否则OpenCV会报类型错误。
5.4 现象:模型把包装袋纹理或桌面背景当颗粒框出来
原因:110张图里多数是近景颗粒图,背景几乎不变,模型偷懒学了一个背景分类器而不是形状分类器。val里恰好有桌面背景图,模型就输出一堆假阳性框。解决:不是继续调参数,而是先做两件事。一是给训练数据加8到10张没有颗粒的负样本图,例如空包装袋、桌面、白底,txt写成空文件,让模型知道这类背景不该出框。二是在增强里把scale开到0.3,让颗粒在图上被放大缩小,弱化和背景的关联。负样本图不要乱加,加太多模型会变得保守,训练完先看val的假阳率再做增删,一般负样本占总量5%到8%就够。
5.5 现象:zip解压后文件名乱码或txt内容乱码
原因:zip打包工具在Windows下默认用GBK压缩文件名,Python的zipfile默认按UTF-8解压,文件名读取失败;txt标注文件如果被Excel编辑过,再另存成txt,内部编码可能变成GBK或带BOM。解决:解压时指定编码,txt一律用Python的encoding="utf-8"或"gbk"去读,读出来后再统一存成utf-8。给一条最省事的检查命令:
file labels/*.txt | head输出里看到UTF-8 Unicode text就是正常的,看到ISO-8859或with CRLF line terminators就要处理。CRLF本身YOLO能容忍,但如果每行末尾多出\r,某些版本的读取代码会把\r算进class_id里,训练时直接报标签格式错误。解决:用sed -i 's/\r$//' labels/*.txt批量清理,或者直接在Python读取时用line.strip()。这个小问题一旦触发,排查路径能让你浪费一上午,先查编码再查坐标。
6. 迁移学习与错检回灌:把110张数据沉淀成可复用的评估基线
6.1 先冻结再微调的实验顺序
小样本用预训练权重几乎是不用讨论的默认操作,但冻结backbone不一定好。颗粒外形简单,YOLOv8n的backbone在ImageNet和COCO上学到的纹理特征已经够用,冻结反而限制了对颗粒边缘的适应。我一般先全量fine-tune跑一次,如果val mAP低于预期,再试freeze=10只训练后面几层。要比较两个方案,必须用同一份划分、同一份命令,只改freeze参数,否则对比出来的差异是数据扰动造成的。跑完看runs/detect/train目录下的results.png,如果冻结版的P曲线上不去了,就不要省那点显存。
6.2 错检回灌:把验证集变成数据采集清单
训练结束不是终点,质检项目真正值钱的是错检回灌流程。跑一批推理,把置信度在0.3到0.6之间、且有误检或漏检的图导出来,集中看一遍。常见误判有三种:把阴影当颗粒、把颗粒叠影当多个目标、把包装内气泡当异物。每确认一种,收集对应场景的图补进数据集,一两周把110张扩到300张,模型mAP往往能涨三到五个点,比调任何参数都管用。导出低置信度样本的脚本:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") res = model.predict(source="val/", save=False) for r in res: for box in r.boxes: if box.conf < 0.6: print(r.path, box.cls, box.conf)逻辑是对验证集逐张推理,把低置信度框和对应图片路径打出来。参数说明:0.6这个阈值是经验值,低于它的框往往处在边界情况,人工复查成本低;如果任务容错更严,可以放到0.8。拿到输出文件名后,我到现场把对应图像截出来,整理成补标清单,回到标注工具里把缺的框补齐。这条路我已经迭代过好几个质检项目,最后留下的不是说那些调参技巧,而是每次接手新数据第一件事跑一遍体检脚本这个习惯,少它不行。希望帮到你。
本文还有配套的精品资源,点击获取