简介:YOLO腰果缺陷检测数据集面向工业质检与目标检测应用场景,适合希望快速上手YOLOv5训练与评估的算法工程师、研究生及竞赛选手,可直接用于缺陷检测模型训练与效果验证。数据涵盖Broken、Defect、SplitDown、SplitUp、Whole五类腰果外观缺陷,标注采用YOLO相对坐标格式(类别、中心点x/y、宽高),并已按训练集3186张、验证集304张、测试集150张划分,目录结构沿用YOLOv5默认组织方式,下载后即可开始训练,省去手动划分与格式转换流程。压缩包共2000个文件,其中1999个为对应图片的txt标签文件,1个为Python可视化脚本show.py;脚本无需修改,随机指定一张图片即可绘制边界框并保存结果,便于快速核对标注质量。附带的class类别文本文件可直接用于模型类别配置。整体大小约101.94MB,已有63人浏览学习。除标注与脚本外,资源还内置数据可视化工具,结合作者YOLOv5改进实战专栏,可帮助理解缺陷检测流程、模型调优与数据排错思路,是一份兼顾训练与调试的实用数据集包。
1. 腰果缺陷检测数据集:不是标注完就能直接训练
做工业质检项目的人大概都有同感:真正折磨人的往往不是网络结构,而是数据集本身。腰果这种曲面农产品,缺陷小、反差低,外壳裂纹和果仁皱缩在 640×640 的输入里可能只占十几个像素,标注得乱一点、划分得随意一点,后面所有训练和调参都是在垃圾数据上做无用功。这个 YOLO 数据集项目把腰果缺陷检测(5类)的标注样本、划分好的 train/val/test、类别 class 文件和数据可视化脚本一次配齐,省掉最容易出错的整理环节,让你直接进训练和迭代。适合刚把 YOLO 跑通、正要转向工业质检场景的工程师,也适合需要在两三天内出一版可演示结果的产线原型项目。
2. 读懂交付物与标签文件:class 文件和数据可视化脚本先把数据看明白
收到一套 YOLO 数据集,第一件事不是急着训练,而是确认你拿到的是不是一套「能闭环」的交付。所谓闭环,指三个东西对得上:图片文件、标注 txt 文件、class 类别文件。三者里任何一个编号错位,训练都不会报错,但模型学的东西完全是错的。
2.1 标注 txt 里到底存了什么
YOLO 的标签文件不是 XML 那种树形结构,就是纯文本,每行代表一个目标框。打开任意一个标注文件,你会看到类似这样的内容:
2 0.48671875 0.4421875 0.09375 0.1265625 0 0.2140625 0.390625 0.0671875 0.103125从左到右分别是:类别编号、归一化中心点 x、归一化中心点 y、归一化宽度 w、归一化高度 h。注意两个关键点:坐标是相对于原图宽高的比例,取值 0 到 1;类别编号是整数,对应 class 文件里第几行。比如标注第一行的数字 2,对应 class.txt 里第 3 行那个类名,不是第 2 行,编号从 0 开始计数,这个约定跟 Python 的索引习惯一致,但跟人眼的第几个很容易错位。
腰果这类缺陷检测里,框的尺寸通常很小,所以 w 和 h 经常只有 0.02 到 0.15 这个量级。如果你在某个 txt 里看到 w 超过 0.5,那大概率是标注把整颗腰果圈进去了,而不是圈了缺陷区域。这在做缺陷检测时需要特别注意,因为训练时模型会把「整个物体」和「缺陷位置」当成同一个概念,后面推理时就会出现「框出来但不知道缺陷在哪」的假阳性。
2.2 跑通数据可视化脚本:直出带标注的质检效果图
拿到交付里的数据可视化脚本,第一步是先让它跑起来,把标注框画回原图上。这个动作看似简单,却是检验数据质量最直接的途径。脚本的核心逻辑不复杂,把归一化坐标换算成像素坐标,再用 OpenCV 画框,示意代码如下:
import cv2 from pathlib import Path def draw_yolo_labels(image_path, label_path, class_names, colors): img = cv2.imread(str(image_path)) if img is None: raise FileNotFoundError(f"图片读不到: {image_path}") h_img, w_img = img.shape[:2] with open(label_path, "r", encoding="utf-8") as f: lines = f.readlines() for line in lines: line = line.strip() if not line: continue parts = line.split() cls = int(parts[0]) x_center, y_center, w, h = map(float, parts[1:5]) # YOLO 坐标是归一化的中心点,必须先换算成像素,再转左上角/右下角 x1 = int((x_center - w / 2) * w_img) y1 = int((y_center - h / 2) * h_img) x2 = int((x_center + w / 2) * w_img) y2 = int((y_center + h / 2) * h_img) cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls % len(colors)], 2) cv2.putText(img, class_names[cls], (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls % len(colors)], 1) return img if __name__ == "__main__": class_names = ["broken", "incomplete", "mold", "foreign", "normal"] colors = [(0, 0, 255), (0, 255, 0), (255, 0, 0), (0, 255, 255), (255, 255, 0)] result = draw_yolo_labels( "datasets/images/val/cashew_001.jpg", "datasets/labels/val/cashew_001.txt", class_names, colors ) cv2.imwrite("visual_check.jpg", result)这段脚本有几个参数值得说明。colors 用的是 OpenCV 的 BGR 顺序,不是 RGB,所以 (0, 0, 255) 画出来是红色,别在视觉排查时把红绿看反。class_names 的顺序必须跟 class.txt 完全一致,如果交付里 5 个类别顺序跟你预期不同,不要手动改代码里的列表,应该先看 class.txt,再回来改脚本,否则画出来的框标签会全部错位。还有一个常见问题是图片路径里有中文字符,OpenCV 的 imread 在 Windows 下可能直接返回 None,建议项目路径全部用英文。
跑完这张图后,建议把它当「验收测试」:随机挑 train、val、test 各 20 张,把可视化结果拼成一张大图,确认每个框都贴住目标。如果框整体偏移但大小没错,问题出在坐标换算逻辑;如果有的框大一圈有的框小一圈,多半是标注时框得随意;如果框和类别对不上,优先怀疑 class.txt 顺序。
2.3 标签统计脚本:像看损失曲线一样看数据分布
画完框只是第一步,你还需要知道这 5 个类别在训练集里到底有多少样本、每张图里通常有几个目标。我一般会用一个很轻量的统计脚本,不数框的总数,而是数「每个类别出现在多少张图里」。
from collections import Counter from pathlib import Path label_dir = Path("datasets/labels/train") # img_count: 参与统计的图像数 # cls_in_img_count: 类别 c 出现在多少张图里 img_count = 0 cls_in_img_count = Counter() for txt_path in label_dir.glob("*.txt"): img_count += 1 seen_cls_in_img = set() with open(txt_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if not parts: continue cls_id = int(parts[0]) seen_cls_in_img.add(cls_id) for cls_id in seen_cls_in_img: cls_in_img_count[cls_id] += 1 print(f"参与统计的图像数: {img_count}") for cls_id in sorted(cls_in_img_count.keys()): print(f"class {cls_id}: 出现在 {cls_in_img_count[cls_id]} 张图里")为什么统计「图数」而不是「框数」?因为训练时一个 batch 里的正样本强度取决于「有多少张图包含这个类」,而不是图里有多少个框。假设某个缺陷类别总共有 1000 个框,但它们集中在 20 张图里,那训练时每轮能抽到包含该类的图只有 20 张,模型对这个类的梯度贡献极不稳定。这个统计结果出来以后,如果发现某个类别只出现在个位数图像里,先别急着训练,你有两条路:一是补充采集,二是把它从 5 类里暂时合并成 4 类,让模型先把能学好的学好。
3. 配置 class 文件与训练管线:让 YOLO 真正读进腰果数据集
数据看明白了,接下来才是训练。很多人在这一步翻车,不是因为 YOLO 多难,而是 class 文件的编号和训练配置里的 names 列表对不上,导致模型训练时把「正常腰果」当成「缺陷腰果」来学。
3.1 从 class.txt 到 data.yaml:类别顺序就是推理顺序
交付里的 class 文件通常长这样,一行一个类名:
broken incomplete mold foreign normal这个文件的顺序决定了每个 txt 标注里数字 0 到 4 的含义。YOLOv8 或 YOLOv5 训练时不认识这个 txt,它认的是 data.yaml,所以你得把 class.txt 的内容抄进 YAML 配置里,顺序一行都不能动。以下是一个典型的 data.yaml:
# data.yaml 中 names 的顺序必须与 class.txt 完全一致 path: /data/cashew_defect train: images/train val: images/val test: images/test names: 0: broken 1: incomplete 2: mold 3: foreign 4: normal有个细节容易被忽略:path 字段。如果你用的是相对路径,比如 train 直接写images/train,ultralytics 会以当前工作目录为基准去拼路径,你在项目根目录跑和在家里目录跑,结果可能完全不同。我一般把 path 写成绝对路径,或者把 train 写成相对于 yaml 文件的路径,然后无论如何都在同一个目录下执行训练命令,避免「昨天能跑今天不能跑」的玄学问题。
另外注意,data.yaml 里 test 并不是必填项,YOLOv8 在训练阶段只用到 train 和 val,test 留给你后面做最终评估。交付里如果把 test 也划分好了,说明这套数据集是奔着「真正评估泛化能力」去的,不是让你在 val 上反复调参调出来的虚高结果。
3.2 预训练模型下载与加载:weights 该放哪、版本怎么核
训练腰果缺陷检测,几乎不会从零训练随机权重,都会从预训练模型开始。常见做法是直接用 ultralytics 的自动下载机制,第一次跑训练命令时它会去拉取对应版本的权重文件。如果你所处的环境拉取不下来,就得手动下载,然后把 yolo 权重文件放到本地路径。
手动放权重时有三个坑值得提。第一,权重文件名必须和命令里写的一致,yolov8n.pt和yolov8s.pt是两个不同规模的模型,混着用不会报错,但加载时会出现结构不匹配的警告。第二,YOLOv5 的权重不能直接给 YOLOv8 用,虽然两家都叫 YOLO,但网络结构不同,强行的后果是 KeyError 或者参数形状对不上。第三,下载到一半的文件大小不对,训练时通常会在读取权重文件时报错,这类问题排查起来很隐蔽,建议下载后先看一下文件大小是否在合理范围内,比如 yolov8n.pt 应该在 6MB 左右。
预训练模型的作用不只是「少训练几个 epoch」。对腰果这种工业场景,预训练模型已经学到了通用的边缘、纹理和形状特征,你实际要学的是「什么样的纹理组合算缺陷」。所以加载预训练权重之后,不需要把 backbone 冻死,让它跟着一起微调,腰果的纹理跟 ImageNet 里的自然图像差别很大,冻结 backbone 反而会让缺陷特征学不出来。
3.3 用 YOLOv8 跑通第一次训练:最小可复现命令
环境装好、数据路径配好之后,训练命令其实短得超出很多人预期。以下是在 ultralytics 框架下跑通一次完整训练的最小命令:
yolo detect train \ model=yolov8n.pt \ data=cashew.yaml \ imgsz=640 \ epochs=100 \ batch=16 \ patience=20 \ project=runs/cashew_v1 \ name=train_001参数的含义拆开说。model=yolov8n.pt表示从预训练权重继续训练,而不是新建随机权重;如果这个文件不存在,ultralytics 会尝试自动下载。data=cashew.yaml是上一步配好的数据配置。imgsz=640是输入分辨率,腰果缺陷偏小的话,我建议先 640 起步,后面再试 1280,不要一上来就开大分辨率,显存和训练时间会拖垮迭代速度。epochs=100是最大训练轮数,配合patience=20使用,表示连续 20 轮 val 指标不提升就提前停止,实际跑完可能只要 60 轮。
有一个容易忽略的参数是batch=16。如果你的显卡显存只有 8GB,batch 16 加 imgsz 640 很可能直接 OOM,此时把 batch 降到 8 或 4 比硬顶着跑更划算。batch 减小后学习率最好也相应调低,比如 lr0 从默认的 0.01 降到 0.005,否则梯度噪声太大,loss 曲线会像锯齿一样抖。
训练结束后,在runs/cashew_v1/train_001/下会生成 weights/best.pt 和 weights/last.pt,以及一堆训练曲线图。记住一点:best.pt 的判定标准是 val 上的 mAP,不是 train loss。train loss 低到 0.01 但 val mAP 上不去,说明模型在背训练集,这种情况后面会单独讲。
3.4 类别不均衡与损失函数:第一个 epoch 就要盯的两个数
腰果缺陷检测天然面临类别不均衡。正常品可能占了一半样本,发霉缺陷只有百分之几。YOLO 的损失函数由三部分组成:框回归损失、分类损失、DFL 损失,训练时三部分加权求和。类别不均衡主要影响分类损失,通俗说就是「正常品」的梯度贡献远大于「发霉」,模型会偏向把一切都预测成正常品,val mAP 里的发霉类 recall 可能惨不忍睹。
YOLOv8 的损失函数没有内置的类别权重参数,所以处理类别不均衡的常见手段放在数据侧:欠采样多数类、过采样少数类、或者对少数类做离线增强。腰果样本采集成本不高的话,我一般优先补少数类数据,而不是靠调 loss 权重,因为数据侧解决不均衡最直观,模型学到的特征也更真实。
另外训练时要盯一个容易忽略的数:各类别在 val 上的 recall。训练日志里默认打的是整体 mAP50 和 mAP50-95,整体数字好看不代表每个类都好。我会在训练结束后单独跑一次模型对 val 集的验证,按类别输出结果,如果某个缺陷类别的 recall 明显低于其他类,下一步就是把该类别的误检图拉出来看,是标得太紧、还是该类样本本来就长在光线不好的位置。
4. 划分好的数据集意味着什么:划分脚本与划分策略
标题里特意写了「划分好的数据集」,说明这套交付把 train/val/test 已经拆好了。但「拆好了」不等于「拆对了」。做工业质检的工程师,拿到划分后的数据,至少要做两件事:一是能复现划分逻辑,二是能判断划分是否科学。
4.1 从原始图到 train/val/test:一个可复用的划分脚本
如果之后要补充数据,或者你想用自己的方式重新划分,一个简单的划分脚本是必备的。核心逻辑是:把同一张图的 jpg 和 txt 同时移动,不能只移图片不留标签。以下脚本按 8:1:1 的比例随机划分:
import random import shutil from pathlib import Path random.seed(42) src_img = Path("datasets/images_all") src_lbl = Path("datasets/labels_all") out_img = Path("datasets/images") out_lbl = Path("datasets/labels") # 以图片文件名为基准,同一个 stem 对应同名的 txt stems = [p.stem for p in src_img.glob("*.jpg")] random.shuffle(stems) n = len(stems) n_train = int(n * 0.8) n_val = int(n * 0.1) def copy_sample(stem, split): img_src = src_img / f"{stem}.jpg" lbl_src = src_lbl / f"{stem}.txt" img_dst = out_img / split / img_src.name lbl_dst = out_lbl / split / lbl_src.name img_dst.parent.mkdir(parents=True, exist_ok=True) lbl_dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy(img_src, img_dst) if lbl_src.exists(): shutil.copy(lbl_src, lbl_dst) else: print(f"警告: {stem}.jpg 没有对应的 txt,已跳过标签") for stem in stems[:n_train]: copy_sample(stem, "train") for stem in stems[n_train:n_train + n_val]: copy_sample(stem, "val") for stem in stems[n_train + n_val:]: copy_sample(stem, "test")这个脚本里有几个参数是刻意这样写的。random.seed(42)固定随机种子,保证你划分一次和下一次结果一致,方便复现。.stem是取文件名不带后缀的部分,jpg 和 txt 靠这个字段配对。lbl_src.exists()的检查很关键,实际项目里总会有几张图漏标,如果直接 copy 会在训练时报「label 文件找不到」的错,所以划分时先暴露出来。
如果你希望按缺陷类别做分层划分,就得先统计每张图包含哪些类别,然后按最稀缺的类别来分层抽样,保证 train 和 val 里各个类别的比例接近。这个逻辑比纯随机复杂不少,但对腰果这种不均衡场景很值得做,能让 val 上的指标更可信。
4.2 为什么随机划分在产线采集场景里会翻车
如果采集照片的方式是「腰果在输送带上连续走动,相机连续拍照」,那么相邻帧之间的目标很可能是同一颗腰果的不同姿态。这种情况下做随机划分,同一颗腰果的照片会分别出现在 train 和 val 里,模型相当于见过 val 的目标了,val mAP 会虚高,等你换一条产线重新拍一批数据来测,精度立刻掉下来。
这个问题的本质是数据泄漏,也叫同源样本污染。判断方法很简单:找几张 val 里特征明显的腰果缺陷图,去 train 里搜相似图,如果能在 train 里找到只是角度略微不同的同一颗果仁,说明划分方式不适合产线场景。
解法是按组划分。常见做法是采集时把照片按时间段或按输送带批次分组,划分时以组为单位,整个组只进 train 或只进 val。上面的脚本改成按组名聚合后再 shuffle,改动很小,但能从根本上堵住泄漏。如果你拿到的交付没有按组划分,但从文件名能看出批次信息,也可以自己重新聚组划分。
4.3 划分完之后要检查什么
划分完成不等于万事大吉。我会跑一组快速检查,确认三件事:第一,train 里的图片数和标签数是否一致,多了或少了都说明有漏标或多余文件;第二,val 里每个类别至少有几张图,如果某个类在 val 里只有 1 张图,那这个类的 mAP 基本没有统计意义;第三,抽查 test 集里的图片,确认没有跟 train 重复的文件名。
检查脚本可以简单粗暴,用文件名列表求交集就能发现重复。还有一个更容易被忽略的点:标注文件里可能有一行是空的。空行本身不影响训练,但如果 txt 文件完全为空,说明这张图没有目标,YOLO 训练时会把这张图当成纯背景样本。如果这类空标签图太多,模型会倾向于「什么都不框」,这跟类别不均衡是两个方向的翻车,都要在划分阶段统计出来。
5. 腰果缺陷检测常见问题与避坑:跑不准通常不是网络的问题
这一章的每一条都是实际项目里反复出现过的现象。按「现象 → 原因 → 解决」来写,方便你遇到问题时直接对号入座。
5.1 现象:训练一切正常,但小缺陷目标的 mAP 始终上不去
训练日志很漂亮,loss 在降,val mAP 也在涨,但前 20 轮之后就不再变化,mAP50 卡在某个值附近。打开验证集的可视化结果发现,小裂纹和小碎渣要么没框出来,要么框出来一半。
原因大概率是输入分辨率不够。腰果树皮上的裂纹在原始高清图里可能只有 20×30 像素,缩放到 640×640 后只剩 8×12 像素,几乎就是几个像素点,模型根本提取不到足够的纹理信息。这不是 YOLO 的问题,是输入尺度和目标尺度的匹配问题。
解决路径有三条。第一条是把 imgsz 从 640 提到 1280,显存够的话这是最直接的提升。第二条是用切图策略,把原始大图切成若干 640×640 的 patch 再训练和推理,相当于让模型在「近距离观察」缺陷。第三条是换用 P2 层或更高分辨率的模型结构,让浅层特征保留更多细节。我一般先试第一条,因为改动最小;不行再上切图,虽然部署时多一步拼框逻辑,但对小目标缺陷的提升非常明显。
5.2 现象:可视化脚本画出来的框整体偏移,但训练没报错
跑可视化脚本时,所有框都比实际目标偏左上方一点,或者框的大小不对。训练过程完全正常,loss 曲线漂亮,因为数据加载器的坐标换算逻辑跟可视化脚本可能用的不是同一套代码。
原因通常是坐标单位没有统一。YOLO 标签是归一化坐标,但有的脚本写成「x1 = int(x_center * w_img)」,漏了减去 w/2 这一步,导致框的左上角点成了中心点,看起来又偏又小。另一个常见原因是图片读取时被 Resize 过,比如脚本里用 PIL 打开图片后做了等比缩放,但画框时用的是原始图的宽高,坐标就错位了。
解决方法是把可视化脚本的坐标换算单独抽成一个函数,每次换数据或换脚本都先跑一遍这个函数,确认拿到的 x1,y1,x2,y2 是像素坐标再画。还有一个更省心的办法:直接用标注软件自带的预览功能或者 yolov8 的 predict 可视化模式,让框架自己处理坐标,绕开手写画框逻辑的坑。
5.3 现象:class.txt 顺序和 data.yaml 对不上,模型学的东西全错位
这是我见过最隐蔽的坑。class.txt 里第 0 行是 broken,data.yaml 里 names 第 0 位写的却是 normal。训练不会报任何错,因为每张图的标注数字 0 对应的类别在 data.yaml 里是 normal,模型就去学「把正常腰果识别成一个缺陷」,最后 val mAP 看起来还行,但实际推理时输出的类别全是错的。
原因很简单:class.txt 是标注阶段的人写的,data.yaml 是训练阶段的人写的,两边只要有一次手动整理顺序,就会出现错位。解决方法是训练前写一个几行的校验脚本,读取 class.txt 和 data.yaml,逐一对比每个索引对应的类名,不一致直接报错退出。这个校验值得放进训练流程里,每次跑训练前自动执行,成本极低,收益是避免一次整轮训练白跑。
5.4 现象:val 指标虚高,换一批真实数据立刻崩
训练时 val mAP50 到 0.9,你觉得模型已经能上线了,结果拉到产线新拍的数据上一测,跌到 0.5 以下。这个现象比训练不起来还让人焦虑,因为它说明模型没有学到「缺陷」的本质特征,只是记住了采集环境。
原因就是 4.2 里说的同源样本泄漏,或者叫场景过拟合。腰果在固定光源、固定角度下采集,val 和 train 的光照背景几乎一样,模型很容易靠背景纹理做判断,而不是靠缺陷本身。
解决方法是把 test 集当成「模拟产线数据」:从不同时间、不同光照条件下采集的数据单独留一部分,训练时绝不碰它。每次调参只允许看 val 指标,test 只在最终验证时跑一次,这样才能得到真实的上线预期。如果 test 也不好,问题就回到数据多样性上,需要补拍不同光照、不同角度、不同传送带速度下的样本。
5.5 现象:训练到中途 loss 变成 NaN,或者 BN 崩溃
训练顺畅跑了几十个 epoch,突然 train loss 跳成 NaN,之后一路空警。有的场景里,日志会显示 BN 层的 running mean 或 running var 出现异常值,然后精度崩到底。
原因通常是两个方向:一是学习率过大,优化器在某个 batch 上梯度爆炸;二是数据增强太猛,Mosaic 拼接时把四张图的边界拼得乱七八糟,标签框落在拼接缝隙上,模型收到了自相矛盾的梯度。腰果这类表面纹理复杂的对象,在 Mosaic 增强下尤其容易出这种问题。
解决做法是分两步排查。先看训练日志里 loss 变 NaN 之前的那个 batch 的 loss 值,如果是一次性跳变,先降低 lr0,比如从 0.01 降到 0.002 再试。如果降学习率后仍然 NaN,就关掉增强里的 Mosaic,或者在最后 10 个 epoch 关闭 Mosaic 做精调。这是常见且可靠的操作路径,不要一上来就换优化器,大多数 BN 崩溃跟优化器没关系。
6. 用混淆矩阵配合可视化做上线前自测:从「框出来」到「敢上线」
训练完 best.pt,很多人直接拿 val 集的 mAP 当上线依据,这是不够的。我最后一步习惯是:用数据可视化脚本批量跑一遍 val 和 test 的预测结果,然后结合混淆矩阵逐类确认,而不是只看一个加权平均的精度数字。
先看混淆矩阵。YOLOv8 训练完成后会在 runs 目录下生成 confusion_matrix.png,横轴是预测类别,纵轴是真实类别。这里有个容易误读的细节:YOLO 的混淆矩阵每行总和并不一定等于该类别的真实样本数,因为检测任务里「一个真实目标可能被重复检测」,也可能因为 IoU 低于阈值被归到背景。所以不要拿对角线数字直接除以行总数算准确率,更靠谱的做法是看「哪两类最容易互相混」。腰果场景里常见的混淆是「broken」和「incomplete」,视觉特征相似,人眼都容易看错,模型混也是合理的,这类混淆可以通过后处理合并类别来降风险。
再看可视化预测结果。我会写一个批处理脚本,遍历 val 的每张图,用 best.pt 跑推理并把结果拼成一张大图:
from ultralytics import YOLO import cv2 from pathlib import Path model = YOLO("runs/cashew_v1/train_001/weights/best.pt") val_images = sorted(Path("datasets/images/val").glob("*.jpg")) out_dir = Path("val_inspect") out_dir.mkdir(exist_ok=True) for img_path in val_images: results = model.predict(str(img_path), conf=0.25, imgsz=640, verbose=False) annotated = results[0].plot() # 返回带框的 BGR 图像 cv2.imwrite(str(out_dir / f"{img_path.stem}_pred.jpg"), annotated)conf=0.25是置信度阈值,上线前可以设低一点,比如 0.1,这样能看到模型「在犹豫什么」。如果低阈值下出现大量误检,说明特征没学够;如果低阈值下只是多框了几个相近区域,那可以通过提高 conf 在部署时压掉。results[0].plot()返回的是 BGR 图像,直接用 cv2.imwrite 保存就行,不用再转 RGB。
这批可视化图出来以后,我会快速翻一遍,重点看三类图:漏检的、误检的、框但置信度低的。漏检的图去回复查标注,看是标得太严还是缺陷本身模棱两可;误检的图看是背景纹理触发还是正常腰果被误判;置信度低的图最有用,它往往指向收集新样本的方向。这样一轮下来,才敢说对这套腰果缺陷检测方案的真实水平有底。
我的习惯是:混淆矩阵定「方向」,可视化定「细节」,两者结合后才决定要不要调参数。如果已经做到这一步,剩下的就是根据结果补数据、调阈值,而不是再盲目改网络结构。这套流程走完,缺陷检测项目的上线风险会小很多,希望帮到你。
本文还有配套的精品资源,点击获取