铝片表面缺陷目标检测实战:数据集构建与YOLO训练指南

发布时间:2026/10/7 13:56:22
铝片表面缺陷目标检测实战:数据集构建与YOLO训练指南 简介铝片表面缺陷目标检测数据集面向工业视觉检测与深度学习研究者聚焦铝材生产中常见的针孔、擦伤、脏污、褶皱四类缺陷可辅助产线质检环节实现自动化识别。压缩包共含402个文件包括400张JPEG缺陷图片与2个COCO格式JSON标注文件整体仅15.74MB下载与离线实验都很轻量。图片已划分为训练集与验证集标注字段包含缺陷位置、类别等关键信息可直接输入目标检测网络也可用于数据增强、特征提取等预处理研究。目前已有161人学习下载适合图像处理初学者快速上手标注格式亦可供专业人士验证算法精度与鲁棒性。借助该数据集能省去自建样本的采集成本让研究者专注模型调优并推动铝材缺陷检测从人工目检向智能化升级。1. 铝片表面工业缺陷目标检测做起来最耗时的一环不是模型而是数据集铝片表面工业缺陷目标检测做起来最耗时的一环不是模型而是数据集。铝片在轧制和转运过程中表面常出现划痕、辊印、凹坑、油污、针孔等缺陷直接决定下游冲压、氧化、涂装工序的良率把目标检测模型引入产线质检时最常遇到的障碍就是没有一份可用的数据集而不是 YOLO 还是 Faster R-CNN 的选型问题。铝片是高反射金属面缺陷对比度低、形态细长、尺寸从零点几毫米到几厘米不等通用数据集上预训练好的模型直接下放到产线几乎没法用。下面几章从一线落地视角把铝片表面缺陷数据集的采集、标注、清洗、切图、格式转换、训练调参与排错讲清楚适合正打算用深度学习做铝材质检的算法工程师也适合 0 基础纯小白照着走一遍。2. 先给“缺陷”定标铝片缺陷分类、采集规范与标注规则拿到一批现场图像就直接标注是铝片缺陷检测项目最常见的翻车起点。标出来的类别混乱、边界漂移模型训练出来验证集 mAP 很高上线就掉链子。这一步省不得先定义清楚要检测哪些缺陷再按规范采集和标注数据集才有价值。2.1 铝片表面缺陷怎么分类按工艺环节划分的缺陷清单我先把自己在铝加工产线里处理过的缺陷类别列一下。不是说所有铝片只有这些而是说没有这套清单标注员会按自己的感觉发挥模型训练时类别逻辑就乱了。缺陷命名常见出现环节形态特征检测难点划痕 scratch轧制、搬运、卷取细长线条方向随机反光下明暗反转容易断成几截辊印 roll_mark轧辊损伤造成周期性重复条纹与铝片本身轧制纹理难分凹坑 pit辊压、冲压、磕碰圆形/椭圆凹陷边缘有褶皱低对比度肉眼都要凑近看油污 oil_stain轧制油、设备漏油不规则暗斑反光下降和氧化斑长得几乎一样黑点 black_dot夹杂、碳化物小而清晰的黑颗粒尺寸小通常只有几到几十像素氧化斑 oxidation_stain热处理、清洗后残留水渍状灰白/暗色斑块边界模糊和油污混叠针孔 pinhole箔材轧制穿透性缺陷十分微小的贯通孔极小目标分辨率不够基本看不到这张表最重要的用途是作为标注文档的开头。每新增一类缺陷就补一行并给标注员配一张“典型图 边界图”的示例。我一般会在项目开始前把每类定义写成文档配 3 张典型样本和 2 张边界案例写清楚“这类和那类同时出现时怎么标”。不要小看这一步后面标注质量全靠它兜底。类别数量控制在 610 个比较合理。分类过细比如把划痕再拆成细划痕、粗划痕、团状划痕会让每个类的样本数量不足模型分不清分类过粗把所有表面瑕疵都叫缺陷又会导致检测结果对工艺改进没有参考价值。如果初期样本少我宁可先合并成“划痕、辊印、凹坑、油污、氧化斑、针孔”六类训完一版再用难例去细分。2.2 标注规范bbox 标注的三个原则和一个争议点工业缺陷标注最常见的做法是画正矩形框也就是 axis-aligned bounding box。要不要用旋转框或分割多边形我的建议是能不转就不转。YOLO、Faster R-CNN 这类通用目标检测器只能输出水平框旋转框要上 mmrotate 之类的专用库工程复杂度会高出不少。对于产线质检第一位是稳定落地不是学术指标。bbox 标注有三个原则。第一只标缺陷本体不要把背景纹理圈进来。铝片表面本来就有轧制纹路如果框画大了模型会学到“有纹路就是缺陷”误检率压不下去。第二细长缺陷按实际外接矩形标允许框内有部分背景但不要把一条断断续续的划痕拆成七八个框。划痕被反光切成几段、但人工判断它们在同一条母线上时我一般建议合并成一个框否则模型会对同一缺陷反复报警。第三粘连目标分开标多个凹坑挤在一起时每个坑单独一个框框不要互相包含。完全模糊到连人眼都确认不了的样本是个争议点。有人建议标上让模型去学有人建议剔除。我的经验是如果人工都定不了边界模型只会学到噪声。这类样本单独放一个 ignore 文件夹不参与训练也不进测试集。等模型跑起来后拿高置信度误检再回来看这些样本反而能帮我们补充标注规范。标注工具方面LabelImg、LabelMe、X-AnyLabeling 都行LabelImg 适合纯 bboxX-AnyLabeling 支持自动标注加速选一个顺手就好。标注完要做一致性检查。让两个人独立标同一批图用 IoU 和类别一致性去校验IoU 一致率低于 0.7 就要补充规范、重新标注。这一步看起来很费时实际上是在给后面的训练止损。2.3 采集方案光源和角度决定了数据集的天花板在铝片这类反光表面上采集方案对数据质量的影响比算法大。线扫相机配合低角度环形光或条光是现场最常见的配置。低角度光把铝面的平整区域反射到一边而划痕、凹坑的漫反射会显现出来同轴光则适合检测规则的辊印和氧化斑。如果你在整理一个拿来即用的数据集而不是自己搭产线至少要注意三点不同光源、角度下同一缺陷长得不一样图来自多个工位时必须在标注文档里记录每张图的光源类型分辨率不能低到缺陷只有几个像素图像中如有油污、脏物等频发干扰要单独建一个“伪缺陷”类或者把它们作为背景样本放进训练集。网上常见的做法是把缺陷检测任务直接套到公开数据集上但工业缺陷数据集不像公开数据集那样有统一标注口径。你在网上能看到的一些铝材缺陷数据集也是从产线采样后按上述规则整理的。与其盲目下载不如自己按标准做一份至少标注口径和你们的质检标准一致。采集数量的经验基线我一般按每类 1000 个实例作为完整训练集的基线但工业现场往往只有 200300这种情况下用迁移学习也能先跑通。图像总数 500010000 张、每类 5001000 实例是比较稳的组合。负样本也要单独拍一批完全无缺陷的图片占比控制在总图数的 20%30% 之间。不要全是干净样本也不要全是满屏缺陷这两头都会把模型带偏。3. 把原图变成训练集大图切 patch、格式转换与数据集划分采集回来的原图往往是一大张几千乘几千像素的图标注工具直接打不开GPU 也没法整图训练。这一章按我平时最顺的顺序来先清洗再切图再转格式最后划分。3.1 数据清洗先处理坏图和亮度过曝、过暗的样本从产线拷回来的图第一件事不是标注是先清一遍。用下面这个脚本扫描目录下的图像输出损坏文件、尺寸差异和亮度统计。import os import numpy as np from PIL import Image def inspect_and_clean(image_dir): 扫描目录下的图像输出损坏文件、尺寸差异和亮度统计 ok_files [] bad_files [] sizes {} brightness_list [] exts (.png, .jpg, .jpeg, .bmp, .tif, .tiff) for fn in os.listdir(image_dir): if not fn.lower().endswith(exts): continue path os.path.join(image_dir, fn) try: img np.array(Image.open(path).convert(L)) sizes[img.shape] sizes.get(img.shape, 0) 1 brightness_list.append(img.mean()) ok_files.append(fn) except Exception as e: bad_files.append((fn, str(e))) print(f可读图像: {len(ok_files)} 张损坏图像: {len(bad_files)} 张) print(尺寸分布:, sizes) if brightness_list: print(灰度均值范围: {:.1f} ~ {:.1f}.format(min(brightness_list), max(brightness_list))) for fn, err in bad_files[:10]: print(f损坏: {fn} - {err}) return ok_files, bad_files ok, bad inspect_and_clean(raw_images)这段代码先把目录里的图逐张读一遍读不出来的单独登记然后统计尺寸分布如果混入了不同相机的分辨率这里一眼就能看出来灰度均值如果出现大于 200 或小于 50 的图在后续切图时就要重点复查。这个脚本不是玄学是我每次拿到新数据集的第一步。很多训练中断不是因为模型而是里面混了几张损坏图和不同分辨率的图。参数方面exts 按需加上.bmp和.tif工业相机经常给 tiff 格式convert(L)是转灰度只统计亮度、节省内存。如果图像太多一次装不下只取前几百张做统计也行。3.2 大图切 patch用滑动窗口把标注同步切出来直接拿大图训练小目标会以极小像素存在检测效果很差GPU 显存也会爆。常见做法是把大图切成 640 或 1024 的 patch同时把原标注同步平移。下面这段脚本输入 YOLO 格式的 txt 标注输出切好的 patch 和对应的新标注。import os import cv2 def slide_crop(img_path, txt_path, save_img_dir, save_txt_dir, crop_size640, stride480, min_cover0.5): 滑动窗口切图并将YOLO格式标注同步映射到patch。 参数说明 crop_size: patch边长常用640/1024。缺陷很小时用640缺陷较大时用1024。 stride: 滑窗步长。小于crop_size会有重叠保证切到完整缺陷一般取crop_size*0.75。 min_cover: 目标被切到patch内的面积占比低于该值则丢弃避免训练出一堆半截目标。 img cv2.imread(img_path) if img is None: print(f读不到图像: {img_path}) return 0 h, w img.shape[:2] boxes [] if os.path.exists(txt_path): with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_c, y_c, b_w, b_h float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) cx, cy x_c * w, y_c * h bw, bh b_w * w, b_h * h boxes.append((cls_id, cx, cy, bw, bh)) count 0 for y0 in range(0, h - crop_size 1, stride): for x0 in range(0, w - crop_size 1, stride): crop img[y0:y0crop_size, x0:x0crop_size] if crop.shape[0] crop_size or crop.shape[1] crop_size: continue crop_boxes [] for cls_id, cx, cy, bw, bh in boxes: x1 max(0, cx - bw / 2 - x0) y1 max(0, cy - bh / 2 - y0) x2 min(crop_size, cx bw / 2 - x0) y2 min(crop_size, cy bh / 2 - y0) if x2 x1 or y2 y1: continue inter_area (x2 - x1) * (y2 - y1) orig_area bw * bh if inter_area / orig_area min_cover: continue x_c_new (x1 x2) / 2 / crop_size y_c_new (y1 y2) / 2 / crop_size b_w_new (x2 - x1) / crop_size b_h_new (y2 - y1) / crop_size crop_boxes.append(f{cls_id} {x_c_new:.4f} {y_c_new:.4f} {b_w_new:.4f} {b_h_new:.4f}) if not crop_boxes: continue base os.path.splitext(os.path.basename(img_path))[0] f_{x0}_{y0} cv2.imwrite(os.path.join(save_img_dir, base .jpg), crop) with open(os.path.join(save_txt_dir, base .txt), w) as f: f.write(\n.join(crop_boxes) \n) count 1 return count为什么步长要小于 patch 尺寸缺陷若恰好落在两片 patch 的接缝处步长等于 crop_size 时它会被切掉一半而丢弃步长取 0.75 倍 crop_size让同一缺陷在多个 patch 里反复出现等于免费的尺度增强。min_cover 为 0.5 的含义是目标只有 20% 面积被切进来时框会贴着 patch 边缘模型学到的特征不完整不如丢弃。切完统计每个 patch 的目标个数如果大量 patch 只有 1 个目标说明缺陷稀疏可以适当提高 stride 降低冗余如果每个目标都横跨多个 patch说明 crop_size 小于目标尺寸要换 1024 或更大。3.3 标注格式转换把 LabelMe 的 JSON 转成 YOLO txt很多标注工具导出的是 LabelMe JSON 或 COCO JSONYOLO 训练需要每张图像一个 txt 文件每行class x_c y_c w h。这个转换脚本可以直接抄。import os import json import cv2 from glob import glob def labelme_to_yolo(json_dir, image_dir, output_dir, class_names): 将LabelMe导出的JSON转换为YOLO txt格式。 LabelMe的points是多边形顶点转换成水平外接矩形 坐标可能超出图像范围转换前要裁剪。 os.makedirs(output_dir, exist_okTrue) class_to_id {name: i for i, name in enumerate(class_names)} json_files glob(os.path.join(json_dir, *.json)) for json_path in json_files: with open(json_path, r, encodingutf-8) as f: data json.load(f) img_path os.path.join(image_dir, data[imagePath]) img cv2.imread(img_path) if img is None: print(f读不到图像: {img_path}) continue h, w img.shape[:2] lines [] for shape in data.get(shapes, []): label shape[label] if label not in class_to_id: continue points shape[points] x1 min(p[0] for p in points) y1 min(p[1] for p in points) x2 max(p[0] for p in points) y2 max(p[1] for p in points) x1, y1 max(0, int(x1)), max(0, int(y1)) x2, y2 min(w, int(x2)), min(h, int(y2)) if x2 - x1 5 or y2 - y1 5: continue x_c (x1 x2) / 2 / w y_c (y1 y2) / 2 / h b_w (x2 - x1) / w b_h (y2 - y1) / h lines.append(f{class_to_id[label]} {x_c:.4f} {y_c:.4f} {b_w:.4f} {b_h:.4f}) base os.path.splitext(os.path.basename(json_path))[0] with open(os.path.join(output_dir, base .txt), w) as f: f.write(\n.join(lines) \n) print(f转换完成输出目录: {output_dir}) class_names [scratch, roll_mark, pit, oil_stain, oxidation_stain, pinhole] labelme_to_yolo(annotations, images, yolo_labels, class_names)LabelMe 的 points 是多边形顶点取 min/max 生成水平外接矩形过滤掉小于 5 像素的框是为了避免标注噪声。COCO JSON 的结构类似区别是 annotation 里直接有 bbox 字段[x, y, w, h]改一下读取位置即可。转换完成后txt 文件名必须与图像名完全一致目录结构和 data.yaml 对应就行。3.4 数据集划分按卷号或批次分不要按文件名随机分很多人标注完成后直接 random split这在工业缺陷检测里会翻车。同一卷铝片拍摄出来的连续图像高度相似随机划分会让验证集变成开卷考试mAP 虚高工厂一上线就露馅。常见做法是按“卷号、炉次、拍摄时间段”分组保证同一批次数据只出现在一个集合里。如果文件名里带了批次信息比如al_20240607_B01_0001.jpg可以按前缀分组再划分。import os import random import shutil from collections import defaultdict def split_by_group(image_src, label_src, img_dst_root, label_dst_root, split(0.8, 0.1, 0.1), seed42): 按文件名前缀分组划分train/val/test防止同一卷铝片的数据同时出现在训练和验证集。 image_src: 所有patch图像的目录 label_src: 所有txt标注的目录 img_dst_root: 输出目录如 dataset/images random.seed(seed) groups defaultdict(list) for fn in os.listdir(image_src): if not fn.endswith(.jpg): continue parts fn.rsplit(_, 2) group parts[0] _ parts[1] groups[group].append(fn) group_names list(groups.keys()) random.shuffle(group_names) n1 int(len(group_names) * split[0]) n2 int(len(group_names) * (split[0] split[1])) split_map {} for g in group_names[:n1]: split_map[g] train for g in group_names[n1:n2]: split_map[g] val for g in group_names[n2:]: split_map[g] test for split_name in (train, val, test): os.makedirs(os.path.join(img_dst_root, split_name), exist_okTrue) os.makedirs(os.path.join(label_dst_root, split_name), exist_okTrue) for group, fns in groups.items(): dest split_map[group] for fn in fns: shutil.copy(os.path.join(image_src, fn), os.path.join(img_dst_root, dest, fn)) txt fn.replace(.jpg, .txt) if os.path.exists(os.path.join(label_src, txt)): shutil.copy(os.path.join(label_src, txt), os.path.join(label_dst_root, dest, txt)) print(train/val/test 组数:, len([g for g, v in split_map.items() if v train]), len([g for g, v in split_map.items() if v val]), len([g for g, v in split_map.items() if v test])) split_by_group(dataset/patch_images, dataset/patch_labels, dataset/images, dataset/labels)这里用rsplit(_, 2)把al_20240607_B01_0001.jpg拆成前缀和序号group parts[0] _ parts[1]得到al_20240607_B01同一卷的 patch 天然归组。如果文件名没有这个规律先跑 3.1 的脚本把文件名打出来按实际规则改 group 提取方式。划分时用 copy 而不是 move保留原始切图目录后面做数据版本对比时还有后悔药。4. 用 YOLO 系列训练铝片缺陷检测ultralytics 环境配置与必调参数数据准备好以后训练反而是最公式化的部分。我习惯用 ultralytics 的 YOLOv8 / YOLO11 系列包内模型名写作 yolov8n.pt 或 yolo11n.pt安装和训练命令几乎一致。这一章就直接照着一个可跑的流程走。4.1 环境配置适合 0 基础纯小白的超详细步骤Ultralytics 统一封装了 YOLOv8、YOLO11 的检测、分割、分类接口安装非常省事。python -m venv venv # Windows: venv\Scripts\activate Linux/macOS: source venv/bin/activate pip install -U pip pip install ultralytics opencv-python装完先确认版本和 GPU 是否可用这一步能省掉后面半天排错python -c import ultralytics; print(ultralytics.__version__) python -c import torch; print(torch.cuda.is_available())第二行输出 True 才说明 GPU 可用输出 False 就让训练跑在 CPU 上batch 和 imgsz 要相应缩小。公司服务器不能联网时预训练权重需要手动下载yolo11n.pt放到当前目录训练命令不变。如果pip install装 torch 很慢可以先到 pytorch 官网按 CUDA 版本装对应 torch再装 ultralytics。4.2 写 data.yaml让训练器知道类别和路径YOLO 训练要求一个 data.yaml内容如下path: /data/aluminum_defect train: images/train val: images/val test: images/test names: 0: scratch 1: roll_mark 2: pit 3: oil_stain 4: oxidation_stain 5: pinholepath 是数据集根目录绝对路径train、val、test 是相对 path 的目录指向图像目录训练器会自动在相邻的 labels 目录找同名 txt。names 的编号必须和 txt 里第一列的类别 id 完全一致。如果你改了类别列表最稳的办法是重新跑一遍 3.3 的转换脚本而不是手改 txt。4.3 启动训练命令、参数表与第一次训练的保守配置yolo detect train \ modelyolo11n.pt \ dataaluminum_defect.yaml \ epochs100 \ imgsz640 \ batch16 \ device0 \ patience20 \ lr00.005 \ cacheTrue \ workers4 \ projectaluminum_runs \ namedefects_v1关键参数按我的习惯列一下参数常见取值作用与经验modelyolo11n.pt / yolov8n.pt / 上次的 best.ptn 版速度最快s 版精度略高先用 n 跑通再换 s 加长训练imgsz640 / 1024和切 patch 尺寸保持一致小目标多时尽量用 1024显存不够就回 640batch832报 CUDA OOM 就减半类别不均衡时 batch 不要开太大epochs100300用 patience 早停不用死等patience1530验证集 mAP 连续多少轮不提升就停省时间lr00.01 官方默认 / 0.005工业小数据集我常用 0.005避免 loss 发散cacheTrue / False数据集不大就开 True把图片缓存进内存训练快很多如果你的缺陷数量很少每类不足 300 实例第一次训练建议不要一上来跑几百轮先用 50 轮看 loss 曲线训练 loss 下降、验证 loss 在底部震荡说明模型在学但数据不足如果训练 loss 都降不下去先回头查标注和格式转换不要把时间花在调超参上。4.4 类别不均衡某类缺陷只有几十个怎么办铝片缺陷数据集里划痕、针孔等类别数量差异经常很大。常见的处理手段有三个按实例数量统计、对稀有类做离线增强、把难分类别合并。先统计每个类别的实例数from collections import Counter import os def count_instances(label_dir): 统计每个类别在数据集里的实例数用于判断是否需要做类平衡 counter Counter() for fn in os.listdir(label_dir): if not fn.endswith(.txt): continue with open(os.path.join(label_dir, fn)) as f: for line in f: line line.strip() if line: counter[int(line.split()[0])] 1 return counter counts count_instances(labels/train) total sum(counts.values()) print(counts) for cls, n in counts.items(): print(f类别 {cls}: {n} ({n / total * 100:.1f}%))当某类占比不足 5% 时就值得做离线增强。对于稀有类常见做法是把缺陷 box 从原图抠出来做随机旋转、亮度变化后粘贴到无缺陷的铝面背景 patch 上。但直接贴会产生亮度伪影模型会学到“边缘有亮带”这个假特征所以粘贴后要用高斯模糊或seamlessClone做融合。如果缺陷对比度低比如氧化斑、细划痕还可以在增强时对图像做 Haar 小波分解把高频子带叠加回原图相当于把细纹理缺陷的边缘放大。这个方向我在低对比度缺陷上试过划痕类的 mAP50 能涨几个点但要注意全数据集的增强口径要统一否则模型会学乱。5. 铝片缺陷数据集避坑指南反光、小目标与标注噪声这一章列的每一条都是我在铝片数据集上实际踩过或帮别人排查过的坑按“现象 → 原因 → 解决”来写。读的时候可以对照自己手头的数据集中一条就处理一条。5.1 训练时一切正常测产线图像时缺陷却“隐身”了现象模型在验证集上 mAP50 超过 0.9一到产线真实图像就大量漏检换一个班次、换一种灯光更明显。原因验证集和训练集来自同一台相机、同一个光源角度。铝面是镜面低角度光下划痕是暗线同轴光下它可能是亮线模型学的是“某种照明下的暗线”而不是“划痕”本身。光源一变缺陷就隐身了。解决采集时给每张图记录光源类型训练集和测试集按光源分组而不是全部混在一起随机分。训练时做亮度、对比度、高斯噪声增强模拟光照漂移。快速排查方法是对比两个采集批次的灰度均值差如果均值差超过 40说明光照变化已经很大必须按光源重新划分数据集这个比调任何模型参数都管用。5.2 小目标缺陷训练后完全漏检现象划痕、凹坑都能测出来针孔、黑点这类微小缺陷 AP 直接是 0。原因针孔在 640x640 patch 里往往只占 3x3 到 8x8 像素YOLO 下采样到 80x80 特征图时目标退化成几乎一个点特征全丢了。解决在切图环节就把 crop_size 降到 320或者让 patch 专门覆盖含小目标的区域另一种做法是喂 1024 分辨率训练小目标像素占比会翻倍。最直接的工程手段是局部放大对含针孔的 patch 做 2 倍放大后再训练。这类极小目标不要指望调参解决必须从数据分辨率层面改变像素占比。推理阶段同样要滑窗切成小 patch再拼结果整图直接推理基本会漏。5.3 训练 loss 不降或验证指标忽高忽低先从标注找原因现象训练到 40 轮左右 loss 开始震荡best.pt 的验证结果忽高忽低PR 曲线形状也异常。原因标注框边界漂移同一类缺陷有的框很紧、有的框大了两三倍模型既要学缺陷本身还要适应框的尺度抖动loss 自然不稳。解决随机抽 50 张训练图把预测框和真实框画在一起人眼去看 IoU 分布。出现大量预测框和真实框明显错位时直接回炉标注。量化的方式是两个标注员独立标同一批图IoU 大于 0.7 且类别一致的比例要达到 90% 以上才算标注稳定这个阈值是我自己的经验值不同产线可以微调。标注规范里要写明细长缺陷用外接矩形的一个框还是按可见线段分段标团队里必须统一。5.4 验证集 mAP 很高一上推理服务就被误检轰炸现象mAP50 0.95部署到推理服务后误检率高得离谱把轧制纹理、水渍全报成缺陷。原因最典型的是数据泄漏。同一卷铝片的连续图像随机划分时一部分进训练、一部分进验证模型等于开卷考试验证分数虚高而真实场景里纹理背景稍微不同模型就乱报。解决用 3.4 的按批次分组划分重新划分。然后收集误检图把它们作为负样本单独建目录混入训练集。工业上的伪缺陷像正常轧制纹理、水渍但不构成缺陷等级的情况模型必须见过才能不报。这里也可以先用 Halcon 传统机器视觉方案做初筛把高频报警的图像挑出来人工确认后回填进数据集这种混合流程在产线上很实用。5.5 训练集里空标签图太多模型总是漏掉真缺陷现象切出来的 patch 大量是纯背景模型训练完变成“只见背景不见缺陷”真实缺陷反而漏检。原因滑动窗口切图时为了高召回把步长设得很小导致无缺陷 patch 数量爆炸。YOLO 训练时背景样本主导 loss模型倾向把所有候选框都判为背景召回率就下来了。解决切图时过滤掉没有目标的 patch就像 3.2 代码里if not crop_boxes: continue那样。负样本图像要单独留存比例控制在训练集总数 30% 以内并且每次训练时随机抽一部分参与保证模型见过背景又不被背景淹没。判断是否过量的办法是看训练日志里cls_loss和box_loss如果训练稳定后cls_loss还一路走低但召回不涨就要回查负样本比例。6. 让数据集可迭代验证指标、难例挖掘与版本管理有了第一版模型真正的质检模型才开始。这一步不是调参而是建立数据集的迭代闭环。首先是验证指标。工业缺陷检测不要只看 mAP要同时看每一类的 precision、recall 和混淆矩阵。油污和氧化斑这类缺陷在风格上经常互混用 ultralytics 的 val 命令很容易拿到结果yolo detect val \ modelaluminum_runs/defects_v1/weights/best.pt \ dataaluminum_defect.yaml \ plotsTrueplotsTrue会生成混淆矩阵和 PR 曲线图。翻confusion_matrix.png时重点去看哪两类互相误检。我第一版做铝片缺陷时就是被混淆矩阵提醒把油污和氧化斑合并成一类后整个模型的训练难度降了一个档次。肉眼看着是两类但对下游工艺来说处理方式一样就没有必要让模型硬分。其次是难例挖掘。把被误判为“无缺陷”的真实缺陷图找出来常见路径有两种模型预测置信度极低但实际是缺陷的样本以及 NMS 后被抑制掉但 gt 框里确实存在的样本。把它们收到 hard_examples 目录做离线增强后再混入下一次训练。如果现场有 Halcon 传统视觉做初筛也可以用它的报警记录当难例候选这个我在产线上见过能明显加速数据迭代。数据集本身要版本管理。工业数据集会随产线运行不断补充新缺陷样本我给数据集定版本号 v1、v2每次变更记录图像数、类别分布、标注规则和采集条件。版本变更后重新划分不允许直接往原目录里塞新图。这是我自己的血泪经验有一次不装版本直接合并新缺陷图结果和旧数据在划分上冲突验证组里混进同一批次产品指标虚高排错排了两天。后来统一用目录加 version.txt 管理虽然土但有效。最后一个我受用很久的习惯每次训练开始前先打印一遍 train、val、test 的类别分布确认和预期一致再按回车。这一步只要 10 秒能挡掉大部分“转换脚本跑错”的事故。数据集的坑都在看不见的细节里把这一步当成数据集的“后悔药”希望你也能少踩几个我踩过的坑。本文还有配套的精品资源点击获取