简介:苹果缺陷目标检测数据集面向深度学习初学者,定位为可直接用于目标检测项目实训的图片与标注配套资料。资源包含七百一十三张苹果图像,以及七百一十三份采用PASCAL VOC规范的XML标签文件,内容覆盖苹果边界框位置与表面缺陷类型,可支撑训练模型区分正常果实与异常果实,为后续迁移学习或从零训练提供基础语料。压缩包总大小不到六兆字节,文件总数为一千四百二十六个,以jpg图像与xml标注一一对应组织,体量精巧,便于本地快速下载和加载。目前已有四千五百五十人学习下载。借助这些数据,初学者能从读取XML标注、构建数据集、划分训练集与验证集开始,逐步实践加载预训练权重、微调YOLO或SSD等检测模型、计算mAP指标、理解边界框回归与IoU计算等核心概念,完整走通一条入门级目标检测实验流程,既适合课程设计与期末实训,也适合个人自学时搭建本地环境反复演练。
1. 苹果缺陷目标检测数据集:别急着调模型,先看明白标注和数据分布
在苹果缺陷目标检测数据集上训练的模型,验证集 mAP 很容易超过 90%,可一上真产线就翻车——两类缺陷几乎全部漏检。我排查到最后,问题不在模型,在数据集本身:某个公开的苹果缺陷目标检测数据集,缺陷类别划分含糊,小目标标注框尺寸差得离谱,背景全是干净的黑色幕布,和产线传送带的真实环境完全不匹配。这类数据集,就是今天要聊的主角:一份按目标检测格式整理、带边界框标注的苹果表面缺陷图片集合。它解决的是“有没有一套能训练和评估缺陷检测模型的数据”的问题,服务果品分级、采后质检、分拣机器人这类视觉任务。刚入门的同学拿它学完整的目标检测流程,熟手则更关心标注格式、数据分布和那些藏在标注里的坑。
2. 苹果缺陷目标检测数据集:缺陷类别、标注格式与数据分布
2.1 苹果缺陷常见的类别划分与标注粒度
苹果表面缺陷在数据集里一般会归成几类:碰伤、腐烂、霉斑、虫眼、裂果、日灼斑、药斑。碰伤是机械作业最常见的一类,特征是果面局部凹陷、表皮变色,但边界往往很浅;腐烂会在短时间内扩散,颜色从浅褐到深褐变化大;霉斑是带毛絮的白色或灰绿色区域;虫眼则是一个极小的洞。不同数据集对这些类的命名不完全一致,有的叫“bruise”“rot”“scab”,有的直接叫“defect”,这本身就是一个信息。
如果你是第一次拿到这样的数据集,先别着急训练,问自己一个问题:业务上到底需要分多细。只做“能不能卖”,建议把所有缺陷合并成一个 positive 类,模型只需要输出“有缺陷”和“没有缺陷”;要做“一级果、二级果、等外果”分级,才需要拆成碰伤、腐烂、霉斑等多个类。类别拆得越细,标注一致性越难保证,训练难度也越高。我见过有人把缺陷拆成 8 类,结果两类样本加起来不到 100 个框,模型从头到尾就没学会这两类。
标注粒度同样影响结果。目标检测用的是水平矩形框,但苹果缺陷不全是规整的矩形。腐烂区域往往边界模糊,真实状况是“从核心腐烂往外逐渐过渡”,所以标注时不能追求框严格贴合边界,而是要把核心病变区域和明显过渡带一起框住。碰伤这类低对比缺陷,框稍微大一点反而有助于模型建立“区域异常”的概念。一个更实用的原则:边界清晰的小缺陷用紧框,边界模糊的中等缺陷用略带外扩的框,外扩控制在原始图宽度的 2% 到 5% 之间。
比“框得准”更重要的是“标得一致”。同一张图,两个人分别标,一个人只标了最大的碰伤,一个人把三个小碰伤全标了,模型学出来的东西就会很混乱。常见做法是定一个最低标注尺寸规则,比如原始分辨率下小于 8×8 像素的缺陷不标,大于这个尺寸的全部标,并在标注规范里写清楚。标注一致性是数据集的底线,模型再强也补不上标注的随意。
2.2 标注格式选型:COCO、VOC、YOLO txt 的取舍
目标检测数据集的标注格式,主流的就三种:COCO JSON、VOC XML、YOLO txt。它们各有各的生态,选型不应该看“哪个高级”,而要看“你要跑什么模型、后续怎么迭代”。
| 格式 | 典型文件 | 优点 | 缺点 |
|---|---|---|---|
| COCO JSON | annotations/instances_xxx.json | 生态好,实例分割、关键点扩展都能承载;数据集元信息完整 | 结构嵌套深,空标注处理麻烦,手工改容易改坏 JSON |
| VOC XML | annotations/*.xml | 人眼可读,标注工具支持广,方便逐图核查 | 一个框一段 XML,文件多,批量解析慢 |
| YOLO txt | labels/*.txt | 轻量,训练框架直接读,坐标归一化后便于增强 | 每张图一个 txt,缺省类别映射,全局统计要额外写脚本 |
如果只跑 YOLO 系列,直接转 YOLO txt 是效率最高的路径;如果要在不同检测框架之间来回换,或者要做可视化、实例分割这类扩展,COCO 是更好的中间载体。VOC 更适合“标注工具导出后人工检查”的场景,文件结构直观,但真正训练前一般还会再转一次。
格式转换里最容易出问题的两件事:类别 ID 映射和坐标精度。COCO 的 category_id 不一定是连续的,YOLO 要求类别号从 0 开始连续排列,所以拿到数据集第一步就是建一张自己的映射表。坐标则在归一化后一定要保留浮点精度,不能用整型;否则小目标的框会偏移几个像素,训练时变成定位误差的来源。转换完随手抽一个 txt 看内容:
# 一条正常记录:类别号 + 归一化中心x + 归一化中心y + 归一化宽 + 归一化高 0 0.431672 0.528124 0.065312 0.048521看到坐标不在 0 到 1 之间、或者宽高为负的记录,一定是转换脚本或者原始标注出了岔子。这类问题在公开数据集里并不少见,别等训练完才发现。
2.3 数据分布统计:类别不均衡、小目标与背景干扰
拿到数据集先做统计,这一步能省掉后面 80% 的排查时间。我一般会先算四个指标:图片总数、标注框总数、每类的框数量、每张图的框数量分布。只看“图片数”会骗人,一个数据集可能图片很多,但有标注的只有一部分;也可能某张图上标了 50 个腐烂点,把整类样本量撑大了。按框来统计更接近训练时的真实信号。
| 统计项 | 作用 | 异常信号 |
|---|---|---|
| 每类标注框数量 | 判断类别平衡 | 最大类与最小类差 10 倍以上,小类别容易被模型忽略 |
| 框面积占原图比例 | 判断目标尺度 | 中位数低于 5%,要考虑提高输入分辨率和小目标增强 |
| 空标注图片比例 | 判断标注完整性 | 超过 20%,检查是漏标还是本来就是背景负样本 |
| 同一图片重复次数 | 检查数据泄漏 | 训练集验证集出现相同文件,评估会虚高 |
小目标问题是苹果缺陷检测里最头疼的。虫眼类缺陷在 640×640 的输入下可能只有十几个像素,模型在特征图上几乎看不到它。如果你统计完发现框面积中位数很小,训练时就要考虑 imgsz=960 甚至 1280。背景干扰则是另一个大坑:苹果表面高光在灰度上和腐烂非常接近,如果数据集的背景全是单一黑色幕布,模型会学到“偏黑区域就是缺陷”这种错误捷径。公开数据集为了拍得好看,往往用干净的吸光布,这恰恰和真实产线环境差别最大。
拿到数据集后我建议按这个清单过一遍:每个类别至少翻 5 张原图和标注叠加图,确认框和缺陷的真实边界关系;画一张框面积分布直方图,看有没有大量“异常小”的标注;统计一下同一张图是否同时出现在 train 和 val 里。这一步花不了半小时,但能避免把时间浪费在一个没法收敛的数据集上。数据分布里隐藏的玄学,大部分都能在这一步被识破。
3. 把数据集跑通:格式转换、划分与最小训练命令
3.1 第一步:用统计脚本普查数据集
我习惯先把 COCO 格式的标注文件解析一遍,确认数据没有明显问题再动手转格式。下面的脚本读取 COCO JSON 并输出基础统计信息,任何拿到 COCO 数据集的人都可以直接改路径用。
import json from collections import Counter from pathlib import Path ann_file = Path("/data/apple_defect_dataset/annotations/instances_default.json") with open(ann_file, "r", encoding="utf-8") as f: coco = json.load(f) images = coco["images"] annotations = coco["annotations"] categories = {c["id"]: c["name"] for c in coco["categories"]} print(f"图片数量: {len(images)}") print(f"标注框数量: {len(annotations)}") print(f"类别映射: {categories}") cat_counter = Counter(a["category_id"] for a in annotations) for cat_id, count in cat_counter.items(): print(f"{categories[cat_id]}: {count} 个框") # 找出没有标注的图片 annotations_per_image = Counter(a["image_id"] for a in annotations) empty_images = [im for im in images if annotations_per_image[im["id"]] == 0] print(f"无标注图片数量: {len(empty_images)}")逻辑说明:先加载 JSON,再用 Counter 统计每个类别出现的框次数,最后根据 image_id 找出没有标注的图片。空标注图片本身不算错误,但在目标检测里,它们如果不参与训练,就只是占用磁盘空间的背景数据;如果参与训练而被当作背景负样本,则需要确认它们确实不包含任何缺陷。
参数说明:ann_file 路径替换成你本地的标注文件即可。cat_id 一定要通过 categories 字段反查真实类别名,不能凭 0、1、2 的序号猜。如果这里发现类别数与预期不符,先解决标注映射问题再往下走。
3.2 第二步:COCO 转 YOLO txt 的转换脚本
公开的苹果缺陷目标检测数据集最常见的分发格式是 COCO JSON,但跑 YOLO 需要它的 labels 目录。转换脚本不难,麻烦的是处理好边界裁剪和类别映射。
import json from collections import defaultdict from pathlib import Path def coco2yolo(coco_path, label_dir, cat_map): label_dir.mkdir(parents=True, exist_ok=True) with open(coco_path, "r", encoding="utf-8") as f: coco = json.load(f) img_info = {im["id"]: im for im in coco["images"]} lines_per_img = defaultdict(list) for ann in coco["annotations"]: im = img_info[ann["image_id"]] img_w, img_h = im["width"], im["height"] x, y, w, h = ann["bbox"] # COCO 是左上角坐标 + 宽高 # 剪掉越界部分,防止归一化值超出 0~1 x = max(0.0, float(x)) y = max(0.0, float(y)) w = min(float(w), img_w - x) h = min(float(h), img_h - y) if w <= 0 or h <= 0: continue xc = (x + w / 2) / img_w yc = (y + h / 2) / img_h wn = w / img_w hn = h / img_h cls_id = cat_map[ann["category_id"]] lines_per_img[im["file_name"]].append( f"{cls_id} {xc:.6f} {yc:.6f} {wn:.6f} {hn:.6f}" ) for file_name, lines in lines_per_img.items(): txt_path = label_dir / f"{Path(file_name).stem}.txt" txt_path.write_text("\n".join(lines) + "\n", encoding="utf-8")逻辑说明:先按 image_id 索引图片宽高,再把每个 COCO 框从“左上角+宽高”转成“中心点+宽高”并归一化。关键一步是越界裁剪:很多标注工具会生成超出图片边界的框,不裁剪归一化值就会小于 0 或大于 1,训练时直接报错。无效尺寸的框直接丢弃,这类标注通常是误操作产生的。最后按图片聚合所有标注并一次性写入文件,避免反复 open 造成重复内容。
参数说明:cat_map 需要手动构造,例如{1: 0, 2: 1},把 COCO 的 category_id 映射到从 0 开始的连续编号。即使数据集只有单类,也要映射到 0 而不是 1,YOLO 的类别号从 0 开始是硬性要求。转换完成后,检查一个 txt 文件,确认每行五个数字、前一个数是整数、后四个数在 0 到 1 之间。这个检查比训练一次还快,值得养成习惯。
3.3 第三步:训练命令与关键超参数
数据格式就绪后,用 YOLO 训练库可以很快跑起第一版。下面是我在模拟项目 X 里验证数据装配是否正确的最小流程:
from ultralytics import YOLO yaml_text = """ path: /data/apple_defect_dataset train: images/train val: images/val nc: 3 names: ["bruise", "rot", "scab"] """ with open("/data/apple_defect_dataset/apple_defect.yaml", "w", encoding="utf-8") as f: f.write(yaml_text) model = YOLO("yolov8n.pt") model.train( data="/data/apple_defect_dataset/apple_defect.yaml", epochs=100, imgsz=640, batch=16, patience=20, project="/data/apple_defect_dataset/runs", name="exp_debug", exist_ok=True, )逻辑说明:数据 YAML 里的 train/val 路径指向图片目录,训练库会自动在同级目录找 labels 子目录,不需要手工指定标签路径。nc 和 names 写清楚即可,names 的数组顺序必须与 txt 里的类别号一一对应。这里选 yolov8n.pt 作为预训练权重,是为了先在调试阶段快速验证数据可用性,确认没问题后再换更大模型。训练到第 10 到 20 轮时看一眼 loss 曲线:如果 loss 在下降,说明数据链路通;如果 loss 在抖动后横着走,大概率是标注或配错了数据。
参数说明:imgsz=640 是最小建议,苹果缺陷里有大量小目标,正式训练我会提到 960,但首轮验证别用大尺寸,省时间。batch=16 在普通显卡上够用,显存不够就减半。patience=20 是早停耐心值,100 轮里如果连续 20 轮验证集指标不涨就提前停,这个值对调试阶段很重要。
提示:训练前会先跑一两次验证确认 YAML 可读,输出日志里会打印检测结果示例图。这时务必看一眼示例图中框是否套在缺陷上,而不是套在果柄或高光上。这一步比看任何指标都直接。
4. 苹果缺陷数据集训练中的 5 个常见翻车点与排查
4.1 翻车点1:浅碰伤漏检,模型学到的不是缺陷而是影子
现象:训练集 mAP 不低,但把验证图片输进去,浅碰伤区域完全没有框,反而果梗位置的阴影经常出现高置信度误检。打开预测图,框几乎都落在暗色边缘上。排查时把标注和原图叠加,才发现浅碰伤的标注框普遍比缺陷实际范围大一圈,把四周的正常果面也包进去了。
原因:浅碰伤和正常果面之间的纹理差异本来就很弱,当标注框把周围阴影也当成正样本时,模型学到了“深色边界”这个容易区分的捷径。阴影和高光在灰度上的表现接近腐烂区域,模型自然就分不清。
解决:先检查浅碰伤类别的标注框原始尺寸,把边框回缩到核心变色区域,宁可漏掉一点过渡带也不要包进正常果肉。然后提高输入分辨率,让局部纹理在特征图上有足够感受野,imgsz 从 640 提到 960 会有肉眼可见的提升。最后在增强里加入局部对比度拉伸,模拟不同光照下的成像差异,迫使模型关注缺陷本身的纹理而不是绝对亮度。
4.2 翻车点2:类别不均衡导致虫眼几乎不检
现象:数据集里腐烂类有 4000 多个框,虫眼类只有 120 个框。训练完看分类指标,腐烂类 AP 接近 90,虫眼类不到 20,几乎属于没学会。这种结果不是模型参数的问题,而是样本量悬殊导致小类别的梯度信号被大类淹没。
原因:单阶段检测模型训练时,负样本远多于正样本,而少样本类别的正样本在整个训练 batch 里占比极低,模型优化方向完全被多数类主导。就算用了带权损失,极端不均衡下权重也只是一个常数缩放,作用有限。
解决:比加权重更有效的是回采样稀有类别图片,把虫眼图片复制多份,每一份配合不同的增强参数重新进训练集。Mosaic 增强里也可以手动调高稀有类的参与比例。如果业务允许,直接把虫眼合并到“损伤”大类里,一个模型框住“所有缺陷”比精确区分每类更容易收敛。这里没有银弹,类别不均衡要从数据层和业务层同时想办法。
4.3 翻车点3:标注框边界太松,把好果肉框进正样本
现象:预测框比真实缺陷大一圈,但置信度很高,NMS 之后把完全正常的果肉边缘也框了进去。这和生产线上“把好苹果误判成坏苹果”的表现完全一样,客户很难接受。
原因:数据集的标注规范如果用了“框住所有变色区域”的宽松策略,过渡带和正常果肉就都被标成正样本。模型学到的是区域整体颜色异常,而不是缺陷边界的形状变化。误差并不在模型,而在标注时就把正样本的边界污染了。
解决:对已经训练过一轮的模型,把预测框回缩 3% 到 5% 再让标注人员复核,可以在不改原始数据的前提下得到一批更干净的边界样本。如果重新标注成本高,至少把宽松框批量收缩后再用于下一轮训练。我的经验是,框稍微小于真实缺陷,后果远比框大更能容忍——漏一点边缘可以靠后续现场数据找回,框进大量正常区域则直接造成误判。
4.4 翻车点4:背景过拟合,模型把包装袋当缺陷
现象:训练集里苹果全部在黑色幕布上,测试时换成传送带和塑料垫板,误检率暴增。仔细看预测图,盒子阴影、塑料膜褶皱、垫板边缘全被框了出来,真正的腐烂样本反而漏检。
原因:背景形态单一,训练时模型不需要分辨“苹果区域”和“非苹果区域”,只要看到“非黑色幕布的东西”就倾向于报警。一旦部署环境变化,原本被幕布隐藏的背景特征全部暴露,模型把场景中的一切当成异常。
解决:训练时加入背景负样本图片,完全不含苹果但有类似纹理的包装袋、传送带、托盘照片,并标注为空背景。增强阶段可以做一个“随机器替换背景”的操作:把苹果前景从原图抠出,贴到不同背景里,合成样本放进训练集。这里要注意贴图的边界模糊和光照一致性,否则模型会在贴图边缘上做文章。一个背景干净的公开数据集,永远替代不了真实产线的负样本采集。
4.5 翻车点5:训练集指标很好,部署现场掉点
现象:验证集 mAP 90% 以上,第一轮现场测试就漏检了一堆。回看现场图片,整体偏暗,部分样本带运动模糊,苹果表面还有水珠反光。失败样本高度集中在单侧光照和水渍区域。
原因:这是典型的 data shift。公开数据集拍的是静态、受控光照,现场是动态、自然光照,模型没有在“变了的环境”里见过这些样本。任何检测模型都不可能泛化到它没见过的分布上。
解决:先收集现场图片,用现有模型生成预标注,人工复核后直接增量训练。这比调整任何训练参数都有效。如果现场标注来不及,至少把现场的光照、模糊、水珠效果离线模拟出来,加进训练集做一个临时版本。血泪教训是:公开数据集训出的模型只是起点,一定要预留现场数据复标的资源,上线前做至少一轮校准训练。
5. 数据不够用:采集、增强与半自动标注
5.1 采集建议:光照、角度、距离与数量
公开数据集的缺陷样本量一般只够跑通流程,真要做产线验证,自己采集是绕不开的一步。采集不是“拿手机随便拍”,光照、角度、距离都会直接影响模型学到的东西。
| 采集维度 | 建议方案 | 理由 |
|---|---|---|
| 光照 | 散射光源,避免单侧强光 | 强光会在果面形成高光,高光在灰度上与腐烂区域几乎一致 |
| 角度 | 正面一张 + 斜 45 度一张 | 斜拍增强缺陷凹凸感,对碰伤类检测收益明显 |
| 距离 | 固定工作距离,苹果占画面 60% 到 80% | 距离不固定会让标注框尺寸分布混乱,增加小目标难度 |
| 数量 | 每类缺陷至少 800 个框;总图量 2000 张起步 | 数量太少时模型只能记住样本,学不到类别规律 |
| 清洗 | 删除模糊、过曝、虚焦图片 | 脏数据进入训练集,损失会被模型当成正常特征 |
最怕的是只拍一个品种的红苹果。红苹果表面底色深,碰伤和霉斑的对比度与青苹果完全不同。如果分级线要处理多个品种,采集时就要把每个品种的底色和纹理都覆盖到。这个投入不是可选项,而是数据集中“多样性”的真正来源。
5.2 增强策略:针对缺陷检测的增强组合
数据增强要克制。苹果缺陷检测里,常见的旋转、翻转并不一定安全。水平翻转这件事,苹果本身左右对称,但缺陷的出现位置有分布倾向,碰撞伤集中在果柄附近,翻转后会让模型对缺陷的位置分布产生错误先验。我在模拟项目 X 里对比过,加了水平翻转后,整体 mAP 反而掉了 1 到 2 个点。
以下增强组合是目前我用于苹果缺陷检测的基础配置:
import albumentations as A import cv2 transform = A.Compose([ A.RandomBrightnessContrast( brightness_limit=0.15, contrast_limit=0.1, p=0.5 ), A.HueSaturationValue( hue_shift_limit=5, sat_shift_limit=20, val_shift_limit=20, p=0.5 ), A.GaussianBlur(blur_limit=(1, 3), p=0.3), A.GaussNoise(var_limit=(5.0, 15.0), p=0.2), A.GridDistortion(distort_limit=0.1, p=0.2), A.RandomScale(scale_limit=(0.8, 1.2), p=0.3), ], bbox_params=A.BboxParams( format="yolo", label_fields=["class_labels"] )) # img = cv2.imread("/data/apple_defect_dataset/images/0001.jpg") # 读取对应 yolo txt 里的框和类别,调用: # transformed = transform(image=img, bboxes=boxes, class_labels=labels)逻辑说明:RandomBrightnessContrast 模拟不同环境亮度,但幅度限制在 0.15 内,防止把轻微碰伤的对比度洗掉。GaussianBlur 模拟相机轻微失焦和产线运动模糊,kernel 控制在 3 以内,太强的模糊会让小缺陷在特征图上直接消失。GridDistortion 模拟苹果表面曲面带来的非线性形变,这对真实果面很友好。RandomScale 改变目标尺度,配合后续的多尺度训练。
参数说明:这个组合没有 Flip 类操作,原因上面已经提过。如果光线来源固定,HueSaturationValue 的 hue 偏移控制在 5 以内,避免苹果表面颜色从一个品种漂移到另一个品种。每次跑完增强,把输出图叠上框看一张,确认框没有发生位移,Albumentations 会对 bbox 同步变换,但这个保证只在你传入的 boxes 是“目标检测框”时才成立,别把分割多边形传进去。
注意:很多公开代码库默认开启 Mosaic 增强,它在目标检测里效果显著,但对小目标缺陷偶尔也有副作用——缺陷被拼接后可能被裁剪到边缘。如果某个缺陷类别 AP 一直上不去,先把 Mosaic 关闭对比一轮,再决定要不要开。
5.3 半自动标注流程:模型辅助、人工复核、清洗回灌
采集了大量现场图片后,从头手动标注是一个巨大的时间黑洞。常见的工程做法是“模型辅助标注”:用一个已经能用的模型在新图片上生成预标注,人工在标注工具里复核修正,再把确认后的数据清洗回灌到训练集。
具体步骤是这样:
- 用现有模型对新图片做推理,把置信度阈值调到 0.25 以下,宁可多出框也不要漏框。
- 将预测结果导出成 VOC XML 或 COCO JSON,用开源标注工具打开,逐张修正框位置和类别。
- 用清洗脚本删除小框、重复框和低置信度框。
- 把清洗后的数据合并进原数据集,重新划分 train/val,训练新模型并与上一轮对比。
清洗脚本是保证回灌质量的关键,我常用的逻辑如下:
def clean_predictions(pred_boxes, image_area, conf_thresh=0.1, min_area_ratio=0.0005, iou_thresh=0.7): """清洗预标注框:删除低置信度、太小和高度重叠的框""" keep = [] pred_boxes = [b for b in pred_boxes if b["conf"] >= conf_thresh] # 按面积从小到大排序,保留大框、去除重叠的小框 pred_boxes.sort(key=lambda b: b["area"]) for box in pred_boxes: if box["area"] / image_area < min_area_ratio: continue if any(iou(box, k) > iou_thresh for k in keep): continue keep.append(box) return keep逻辑说明:先按置信度过滤,再用面积比例过滤掉原始图上只有十几个像素的标注,这类小框很可能是模型幻觉或噪点。最后用 IoU 判断重叠:如果两个预测框 IoU 超过 0.7,保留面积更大的那个,因为大框通常对应更完整的缺陷区域。这样回灌的数据质量比模型原始输出稳定得多。
参数说明:conf_thresh 取 0.1 而不是更高,是因为半自动标注阶段宁可多保留可疑框让人类判断,也不要在第一步就把召回丢了。min_area_ratio 即“框面积/原图面积”的下限,0.0005 在 640×640 的图里大约是 200 像素,低于这个值的框基本不可辨。iou_thresh 取 0.7,高了去不掉重复框,低了会把部分相邻缺陷错误合并。
注意:半自动标注适合“已有可用模型 + 大量同分布新图”的场景。一个新领域、没有任何预训练模型时,不要用这个方案,直接投人做高质量标注反而更快。清洗脚本能过滤低质量框,但过滤不了标错类别的问题,类别修正必须靠人工复核完成。
6. 验证模型是不是真的在看缺陷:热力图与错误分析
6.1 用热力图定位模型注意力
指标会撒谎,热力图不会。训练完模型后,我会对一批验证样本做热力图叠加,确认模型真正关注的区域。如果是浅碰伤,热力应该集中在碰伤凹陷处;如果集中在高光区或果柄,说明模型学到的是亮度异常而不是缺陷本身。
一个可用的示意流程如下:
import torch from ultralytics import YOLO model = YOLO("/data/apple_defect_dataset/runs/exp_debug/weights/best.pt") results = model.predict("现场缺陷样本.jpg", conf=0.3, save_crop=True) # 对每个预测框区域提取特征图梯度,用 Grad-CAM 生成热力图 # 实现细节依赖具体的 CAM 库,核心是获得预测类别对特征图的梯度 # 热力图生成后与原图按 0.5:0.5 叠加显示逻辑说明:Grad-CAM 的核心是对最后一层特征图求类别得分的梯度,梯度大的区域对分类决策贡献高。目标检测模型里,这个 CAM 计算的是“该区域是否为该类缺陷”的注意力,不是框的回归置信度。所以热力图比 bbox 更能反映模型的内在判断依据。
参数说明:推理时 conf 设成 0.3,比自己训练时常用的 0.5 门槛低,目的是把更多可疑样本带进分析视野,而不是只看“模型很有把握”的样本。输出时做成三栏对比图:原图、预测框图、热力图叠加图,这样开会和别人沟通时不用口头描述,直接看两张图就能对齐问题。
6.2 错误聚类:把漏检和误检样本分组复盘
热力图看单张,聚类看整体。把测试集结果导成结构化表格,并按状态分组,能快速定位“模型到底错得有没有规律”。
| 分组 | 判定条件 | 优先排查方向 |
|---|---|---|
| 漏检 | 有真值框,无匹配预测 | 目标太小、对比度太低、光照方向异常 |
| 误检 | 有预测框,无真值框 | 背景高光、包装纹理、果柄阴影 |
| 定位偏移 | 与真值 IoU 在 0.3 到 0.6 之间 | 标注边界不统一、框回缩策略不合理 |
实际操作时,不需要做全量分析,从每类漏检样本里随机抽 40 到 50 张足够。把图片按缺陷类型、背景类型、光照情况各分一栏,找出出现频率最高的组合。如果漏检集中在“左下角 + 强光照射”的样本上,那就不是模型问题,而是采集环节的光照死角,调整设备比调模型更快。聚类的意义不是给模型定罪,而是把数据问题、模型问题、业务问题分开。
6.3 回灌现场样本,形成迭代闭环
模型上线后的第一周,是最有价值的迭代窗口。我习惯在这里做一件事:每天从现场收集误检和漏检图,先人工过一遍,确认是“真缺陷没检到”还是“模型判断偏差”,再把这些图清洗、去重后加入训练集,重新跑一轮。这个过程不需要等一个大版本迭代,而是小步快跑。
每次回灌后,不只对比 mAP,还要回到热力图那一步,看看新样本的注意力是否真的落在缺陷上。如果 mAP 涨了但热力图依然关注高光,说明这次回灌只是让模型记住了更多高光样本,并没有真正学会区分,下一轮现场测试迟早还会翻车。我最早做模拟项目 X 时,喜欢把所有拿到手的数据都一股脑塞进训练集,样本量越大越好,结果模型每次回测指标都很漂亮,一到现场就露馅。后来才明白,泛化能力来自验证集中的真实分布,来自每一轮诚实面对那些漏检图。从那以后,每个数据集我都会先花时间做统计和可视化,再谈训练参数。这个习惯帮我避免了很多次无效投入,希望帮到你。
本文还有配套的精品资源,点击获取