铝片表面缺陷目标检测实战:COCO标注转YOLO训练全流程
简介这份数据集聚焦铝片表面工业缺陷目标检测面向计算机视觉初学者和制造业质检算法开发者用于训练与验证针孔、擦伤、脏污、褶皱四类常见缺陷的识别模型。压缩包共402个文件包含400张jpg缺陷图像与2个json标注文件标注采用COCO格式记录缺陷位置和类型约15.74MB体积轻量适合快速开展实验。已有161人学习下载可作为图像分类、目标检测及数据增强等任务的入门练习数据。训练集和验证集划分明确便于评估模型泛化能力研究者可在此基础上进行图像预处理、特征提取和模型调优或通过旋转、缩放等增强策略扩充数据规模逐步搭建更稳健的缺陷检测流程减少人工目检成本并提升产线自动化水平。1. 铝片表面工业缺陷目标检测数据集412 张图里的四类缺陷与 COCO 标注做工业质检的和做通用目标检测的对“数据集”三个字的理解完全不一样。通用场景框出猫狗行人就好工业场景是要在反光、纹理、油污混杂的背景下把只有几个像素的针孔、一道浅划痕从照片里捞出来。这份铝片表面工业缺陷目标检测数据集就是为这个场景准备的。它包含 412 张铝片表面照片按 COCO 格式标注了针孔、擦伤、脏污、褶皱四类缺陷并且已经划分好 train.json 和 valid.json。图片是纯数字命名的 jpg标注文件里带每个缺陷框的位置、类型和面积。适合三类人刚入门目标检测、想拿真实工业数据练手的学生要复现缺陷检测 baseline 的算法工程师正在做产线质检预研、需要一份能直接喂进 YOLO 训练管道的标注数据的人。下面按我实际处理这类数据的流程往下拆。2. 读懂 COCO 标注文件JSON 结构、可视化检查与训练前验证2.1 categories、images、annotations 三张表里存了什么COCO 格式不是一种文件格式而是一种 JSON 组织约定。不管你是用 labelme、LabelImg 还是 X-AnyLabeling 这类标注工具导出的只要自称 COCO 格式顶层就一定有三个数组categories、images、annotations。categories 记录类别 ID 与名称的对应关系images 记录每张图的文件名、宽高和图片 IDannotations 是主体每一条是一个标注框。先跑一段代码把三部分打出来看看import json with open(train.json, r, encodingutf-8) as f: coco json.load(f) print(categories:, coco[categories]) print(images 数量:, len(coco[images])) print(annotations 数量:, len(coco[annotations])) print(第一条 annotation:, coco[annotations][0])输出里最需要关注的是 annotation 的 bbox 字段。COCO bbox 的顺序是 [x, y, width, height]x、y 是框左上角在整张图里的像素坐标width、height 是框宽高不是右下角坐标。新手最容易在这里翻车把 COCO bbox 当成中心点表示去用或者在转 YOLO 时把 w、h 当成右下角去减造成坐标偏移。area 字段是标注面积如果数据来自自动标注平台area 不一定等于 w*h统计面积分布时注意别混用。这里有个容易忽略的细节images 数组里的 id 和文件名字段是独立的。这份数据集的图片是 138.jpg、242.jpg、346.jpg 这类纯数字命名看起来 id 和文件名能对上但你不能默认所有 COCO 数据集都这样。拼接图片路径时永远用 file_name 字段不要用 image_id 去猜。train.json 和 valid.json 的划分是固定的但你不必当成铁律训练前先统计两个集合里各类目标的实例数如果验证集某一类太少合并重划是合理的操作。2.2 可视化检查训练前必须做的一件事拿到这份数据集之后第一件事不是急着装环境而是先把标注框画回图片上人工过一遍。这一步能发现的问题比你训练后从指标里反推要省时间得多。import json import cv2 import random from pathlib import Path with open(train.json, r, encodingutf-8) as f: coco json.load(f) # 建立 id - 名称 / id - 图片信息 的字典避免循环里线性查找 cats {c[id]: c[name] for c in coco[categories]} imgs {im[id]: im for im in coco[images]} # 按图分组标注 anns_by_img {} for ann in coco[annotations]: anns_by_img.setdefault(ann[image_id], []).append(ann) # 随机抽 5 张画框 for img_id in random.sample(list(imgs.keys()), 5): info imgs[img_id] img cv2.imread(str(Path(images) / info[file_name])) if img is None: print(读不到图片:, info[file_name]) continue for ann in anns_by_img.get(img_id, []): x, y, w, h [int(v) for v in ann[bbox]] cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(img, cats[ann[category_id]], (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imwrite(fcheck_{info[file_name]}.jpg, img)逻辑分三步先把 categories 和 images 数组转成字典让按 ID 取信息变成 O(1)再把 annotations 按 image_id 分组避免循环里重复遍历整个标注数组画框时把 bbox 四个值先取整因为 OpenCV 不接受浮点坐标。看到输出图后检查四件事。第一框是否贴着缺陷边缘工业数据最常见的标注问题是框比真实缺陷大一圈模型学到的特征里混进背景脏污和反光的误检很大程度是这么来的。第二框是否超出图像边界或者有宽度、高度为 0 的坏框这类标注在 COCO 里是合法 JSON但训练时会让 loss 震荡。第三同一张图上同类缺陷的框尺寸差异是不是大到离谱比如针孔只有 6×6褶皱占了半张图这种尺度跨度对网络结构有直接影响。第四类别有没有标反擦伤标成褶皱、褶皱标成擦伤在纹理类缺陷里很常见。这四类缺陷各自的检测难点还不一样类别英文名典型形态检测难点针孔pinhole微小穿孔直径几个像素标准小目标特征少下采样后容易消失擦伤scratch细长条状划痕长宽比极大通用锚框不匹配脏污dirt不规则污渍、油渍与反光、阴影、纹理边界易混淆褶皱wrinkle折痕、不平整纹理对比度低光照角度不同表现差异大最后别忘了统计 train.json 和 valid.json 里每类目标的实例数量。如果某一类在验证集里只有几个框评估结果就会有很大随机性同一份权重跑两次 mAP 能差 0.1。这也是后面第 4 章要展开讲的问题。3. 把数据集转成 YOLO 训练格式转换脚本、类别索引与关键参数3.1 为什么先转 YOLO 格式在我接触的缺陷检测项目里YOLOv8 是现在最主流的起步选择原因是它的训练脚本、预训练权重、导出部署链路最成熟社区资料也最多想用 YOLOv8 训练自己的数据集基本绕不开这一步。但 YOLO 系模型不读 COCO 的 JSON它要求每张图对应一个同名的 .txt 文件每行是一个目标格式是 class_id 中心点x 中心点y 宽度 高度全部归一化到 0~1。这和 COCO 的左上角加宽高像素坐标是两套坐标系必须转换。转换之前先把目录结构定成 YOLO 约定aluminum/ ├── images/ │ ├── train/ │ │ ├── 138.jpg │ │ └── ... │ └── valid/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── 138.txt │ │ └── ... │ └── valid/ │ └── ... ├── train.json ├── valid.json └── aluminum.yamltrain.json 和 valid.json 保留不动images 目录直接复用转换脚本负责根据 JSON 生成 labels 目录。aluminum.yaml 是 YOLOv8 的数据集描述文件路径、类别数、类别名都写在里面path: /home/you/aluminum train: images/train val: images/valid nc: 4 names: 0: pinhole 1: scratch 2: dirt 3: wrinkle3.2 转换脚本从 COCO 像素坐标到 YOLO 归一化坐标转换脚本看起来短但有几个细节做错了训练阶段会以很隐晦的方式报错。先看完整代码import json from pathlib import Path def convert_coco(coco_path, images_root, labels_out): with open(coco_path, r, encodingutf-8) as f: coco json.load(f) imgs {im[id]: im for im in coco[images]} cats {c[id]: c[name] for c in coco[categories]} # 用排序后的类别名生成索引保证 train 和 valid 的类别顺序一致 cat_names sorted(set(cats.values())) cat2idx {name: i for i, name in enumerate(cat_names)} print(类别 - 索引:, cat2idx) anns_by_img {} for ann in coco[annotations]: anns_by_img.setdefault(ann[image_id], []).append(ann) Path(labels_out).mkdir(parentsTrue, exist_okTrue) counter {name: 0 for name in cat_names} skipped 0 for img_id, anns in anns_by_img.items(): info imgs[img_id] w, h info[width], info[height] stem Path(info[file_name]).stem lines [] for ann in anns: if ann.get(iscrowd, 0): skipped 1 continue name cats[ann[category_id]] x, y, bw, bh ann[bbox] if bw 0 or bh 0: skipped 1 continue # COCO 左上角 宽高 - YOLO 中心点 宽高再除以图宽高归一化 cx (x bw / 2) / w cy (y bh / 2) / h nw bw / w nh bh / h lines.append(f{cat2idx[name]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) counter[name] 1 if lines: with open(Path(labels_out) / f{stem}.txt, w, encodingutf-8) as f: f.write(\n.join(lines) \n) print(各类目标数量:, counter) print(跳过的标注:, skipped) if __name__ __main__: convert_coco(train.json, images, labels/train) convert_coco(valid.json, images, labels/valid)三个值得展开的点。第一归一化公式本身很简单cx 就是 x 加半个宽再除以图宽但要注意必须在浮点数域运算如果 x、w 是 JSON 读出来的 Python float 就没有问题一旦转成 int小目标的比例误差会被放大训练时这类目标的对齐会变差。第二类别索引我用了 sorted 处理如果 train.json 和 valid.json 里 categories 的定义顺序不一样而你又直接拿 category_id 当索引同一种缺陷在训练集和验证集里就是两个不同的类训练看起来收敛验证结果一团糟。第三iscrowd 标注在检测任务里通常表示密集的一簇目标不适合直接当独立目标训练先跳过宽度或高度为 0 的框是导出坏了也必须跳过否则归一化后坐标越界训练时 loss 算出来的梯度是错的。转换完成后打开一个 txt 检查。比如 138.txt 内容类似2 0.4513 0.6231 0.0821 0.0543 0 0.1200 0.4800 0.0100 0.0150第一列是类别索引后面四个浮点数是 cx、cy、width、height。把所有值扫一眼只要出现负数或大于 1就是原标注越界了回到第 2 章的画框脚本定位是哪张图的哪条标注。3.3 训练参数imgsz 是这个小目标场景的关键目录就位后直接跑训练yolo detect train \ modelyolov8n.pt \ dataaluminum.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectruns/detect \ namealuminum_defect第一次跑建议用 yolov8n.pt参数量小、显存占用低先把数据和管道的正确性跑通确认 loss 正常下降、验证能出结果之后再换 yolov8s 或 yolov8m 提精度。从预训练权重开始做迁移学习lr0 用默认的 0.01 就行不需要从头调学习率。注意训练和推理的 imgsz 必须保持一致。训练用 1024、推理用 640 会让模型看到的物体尺度不一致mAP 会明显下降。参数怎么定围绕一个核心矛盾缺陷尺寸。针孔在 640 分辨率下经常只有 4 到 8 个像素网络下采样 32 倍之后基本就没了。关键参数取舍如下参数建议值说明imgsz640 起步小目标换 1024/1280对指标影响最大显存够就往上加batch16~32与 imgsz 互相制约显存不够先降 batchpatience20验证指标连续 20 轮不涨就早停mosaic1.0最后 10 轮自动关闭增强效果强但最后阶段干扰细粒度特征lr00.01从 yolov8n.pt 迁移时用默认即可显存允许的话小目标场景我一般直接 imgsz1024、batch8 起步。后面 4.1 会讲这个选择在指标上的具体回报。4. 训练踩坑与排查手册小目标漏检、mAP 波动与标注脏数据训练过程翻车九成不是网络结构的问题而是数据在你没注意的地方出了岔子。目标检测模型微调崩了基本都能在这四条里找到对应。4.1 四条典型踩坑记录坑一整体 mAP 不低针孔类 recall 接近 0现象训练 100 轮后 mAP50 到了 0.7 左右看着不错。单独看 pinhole 的 recall 只有 0.12等于没检测到。原因针孔在 640 分辨率下只有几个像素经过 32 倍下采样后在特征图上只剩很弱的响应。另外针孔虽然数量多但在 412 张图里分布集中模型没有建立起稳定的模式。解决imgsz 提到 1024 甚至 1280这是回报最直接的手段。我之前第一次跑这个数据集mAP50 到 0.74 就觉得差不多了后来看类别指标才发现 pinhole 的 recall 只有 0.12把 imgsz 提到 1024 重跑recall 直接跳到 0.5 以上。同时检查增强配置把 scale 类增强幅度调低避免本就不大的目标在学习过程中被继续缩小。推理阶段如果还漏用滑窗切图把大图切成 512×512 的 patch 分别检测再合并等价于放大后再看一遍。坑二mAP 曲线像过山车指标波动剧烈现象验证集 mAP 在相邻 epoch 之间从 0.8 掉到 0.5早停触发最优权重不稳定换了随机种子结果也不一样。原因valid.json 的样本太少目标分布极不均匀。某一张图有 30 个针孔另一张图只有 1 个褶皱漏掉那张 30 目标的图指标立刻跳水。这是小验证集评估的随机性不是模型有问题。解决把 train.json 和 valid.json 合并按类别做分层重划分保证验证集里每类至少有 15 到 20 个实例。训练命令里固定 seedYOLOv8 加 seed42 即可。如果数据实在少用 K 折交叉验证取多次训练的平均结果作为最终结论。我见过有人连续调了三轮参数以为在优化模型后来发现纯粹是随机种子在起作用从那以后我训练脚本里永远写死 seed。坑三脏污类误检严重把反光当脏污现象验证集上把铝片边缘反光、螺孔阴影全部框成 dirt精确率掉到 0.3 以下。原因脏污与反光、阴影在灰度纹理上高度相似。工业现场的铝片本来就有轧制纹理反光角度一变纹理看起来就脏这个类别是最容易让标注员崩溃的。如果标注时标准不统一有人标了轻微油渍有人没标模型就会学到“暗色斑块就是脏污”的错误规则。解决用第 2 章的画框脚本重新过一遍脏污的标注统一界定标准。更强的做法是把脏污拆成两类比如“表面污渍”和“反光伪影”让网络显式区分。不想动标注的话在增强里加大对比度和亮度扰动帮助模型把光照变化当成不变量。坑四loss 异常高或中途 NaN现象train loss 从一开始就比正常值大一个量级或者训练到中段直接变 NaN。NaN 有个特征通常不是第一轮就出现而是先 loss 缓慢上升然后某几个特定 epoch 直接爆掉。原因最常见的是转换后的标注坐标越界。COCO bbox 里有负坐标、或者 w、h 为 0转成 YOLO 格式后 cx、cy 或宽高落在 [0,1] 之外loss 计算产生负对数数值不稳定。看到这个模式优先查数据而不是调学习率。解决在第 3 章的转换脚本里对 bbox 做合法性过滤x、y 小于 0 的裁到 0xw 大于图宽的裁到图宽面积小于 4 像素的框直接删掉。转换完成后检查所有 txt 文件里是否有越界值awk {for(i2;i5;i) if($i0 || $i1) print FILENAME, $0} labels/train/*.txt如果这条命令有输出说明还有漏网的坏标注回到原 JSON 修掉再重新转换。我一般会在转换脚本里加一段扫描逻辑一旦发现越界就打印文件名和原始 bbox当场处理不留到训练时报错。4.2 排查工具箱指标图、损失曲线和人工复查训练完不要只看最后一行 mAP。YOLOv8 会在 runs/detect/ 下生成 confusion_matrix.png、PR_curve.png、results.png 这些图按顺序看三张。confusion_matrix.png 的每一列是真实类别。如果某一类大量被分到背景类说明它的特征没学出来如果背景列有明显响应说明模型把大量背景区域当成了目标误检的源头也在那里。PR_curve.png 的曲线靠近右侧是漏检少靠近顶部是误检少小目标类别通常是 recall 端塌陷曲线往左掉这印证了坑一的判断。results.png 里有 box loss 和 cls loss 两条验证分支曲线如果 cls loss 在训练后期不降反升多半是数据增强过强破坏了真实类别特征的稳定性。还要对比 train 分支和 valid 分支的 gap。train loss 一直降但 valid loss 不降就是过拟合的早期信号这时候优先回退增强强度或提前早停而不是继续加 epoch。我还有一个固定习惯从验证集里挑二十张图把训练好的模型用 predict 存成图片人工快速扫一遍。指标会骗人图不会。工业缺陷这种类间相似度高的场景最终判断标准永远是产线上能不能用而不是一个孤立数字。5. 数据增强与可信评估固定种子、留出集与 TTA 的实战取舍最后说三个每次我都会用的小技巧全部围绕一个目标让结果可信。增强不是越多越好。YOLOv8 默认的 mosaic、mixup 对中大目标友好针孔这类小目标在拼图里会被进一步缩小反而更难学。我的做法是降低 scale 增强幅度比如 scale0.2把重心放在两类增强上一是复制粘贴把标注好的针孔、擦伤贴到正常铝片区域的随机位置相当于手工扩充小目标样本二是 HSV 和光照扰动因为产线光源不稳定这是工业场景最大的变化源hsv_s0.7 这类偏强的饱和度扰动对脏污类尤其有效。评估阶段固定随机种子是前提。训练命令里带 seed42 之后同一份数据、同一套参数跑两次结果基本一致你才能分清指标变化来自模型改动还是数据波动。留出集是我后来才养成的习惯。不要在同一个数据集上反复调参调到自己满意然后拿着这个分数宣称模型可用。正确做法是留出 10% 的图片完全不参与训练和验证等所有参数定死之后再在这 10% 上跑一次最终评估模拟真实产线的分布外情况。资源包里的 train.json 和 valid.json 可以按这个思路合并重划留出集与训练集不要有交集。TTA 我的态度是离线质检可以用实时在线别碰。推理时同时跑原图、水平翻转和 0.8、1.0、1.2 三档缩放把框加权合并小目标 recall 能提升 3 到 5 个点但推理时间翻几倍适合出质检报告或最终验收。这套流程我后来每次接工业数据集都强制走一遍先画框检查标注再转换格式并过滤异常框训练前调过增强策略最后在留出集上做最终评估。412 张图的规模不算大想榨出全部价值靠的就是这些重复但必要的动作。希望帮到你。本文还有配套的精品资源点击获取