ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

铝片表面缺陷检测:YOLO数据准备、格式转换与训练避坑指南

铝片表面缺陷检测:YOLO数据准备、格式转换与训练避坑指南 简介一套完整的YOLO铝片表面缺陷检测数据集与训练配套方案面向工业质检、表面缺陷识别等方向的初学者和算法工程师。数据集中含1000张真实产线场景的高清铝片图像缺陷类型多样场景光照与角度变化丰富所有图像均使用LabelImg人工精标标注框质量高并统一输出VOC(xml)、COCO(json)、YOLO(txt)三种标签格式可直接用于YOLO系列模型训练与验证。资源包共2000个文件除样本图片及标签外还附带数据集划分脚本和详细训练教程教程覆盖Linux与Windows环境搭建、案例修改、训练集/验证集/测试集自动划分等内容可减少从零配置环境的成本。压缩包整体约60.69MB目录结构清晰便于按需取用。目前已有933人学习下载是快速上手铝片缺陷检测任务的高性价比资料。1. 铝片表面缺陷检测真正卡住的不是模型是数据能不能直接开训很多做铝片表面缺陷检测的团队第一步就倒在数据准备上。工厂里合格品随手就是几千张但划痕、压伤、氧化斑这类缺陷样本本身就难凑凑齐了还要转成 YOLO 能读的标签格式中间任何一个环节出错后面几百个 epoch 全白跑。这个标题里的数据集压缩包给的就是一条现成的路径1000 张图片、VOC/COCO/YOLO 三种格式标签、一个划分脚本、一份训练教程拿到手可以顺着它把整个流程走通。它适合两类人一类是刚接触 YOLO 的工程师想通过一个能跑通的完整项目把训练、验证、推理串起来另一类是在评估铝片表面缺陷检测可行性的方案选型人员先用现成数据验证平台和参数再决定自己的产线数据该怎么投入。2. 先别急着训练把 1000 张图和三种格式标签看清楚2.1 1000 张图够不够取决于缺陷分布而不是总数先说一个容易被忽视的事实缺陷检测数据集的价值不在于总张数而在于“缺陷目标”的分布。铝片表面的缺陷通常是小目标一张 1920×1080 的图里可能只有一小块划痕区域。如果 1000 张图里每一类缺陷只有二三十个标注框那训练出来的模型大概率只能记住背景学不会区分缺陷类型。反过来如果一张图里有多处缺陷1000 张图对应着 3000 多个标注实例这个规模是完全够跑通一个基础方案的。和轴承缺陷检测、硅片缺陷检测类似铝片表面缺陷数据天然是长尾分布某种缺陷占了 80%其他几种各占一点。拿到数据集后第一件事不是解压就跑训练而是把所有标签文件解析一遍统计每个类别出现的次数。提示至少保证每个参与训练的缺陷类别有 30 个以上的标注框低于这个量级测试集里几乎不可能出现该类缺陷的有效评估。如果某个类别只有零星几个框宁可先只训练数据量足够的三个主类把冷门类别留到第二版增量数据里再加也不要硬着头皮让模型去学一个只有 8 个样本的类别。2.2 VOC、COCO、YOLO 三种标签格式的差异同一个标注结果用三种格式表达最容易踩的坑就是坐标含义不一致。表格里一眼能看明白格式文件形式坐标描述类别表示典型读取方式VOC.xmlxmin、ymin、xmax、ymax像素绝对值字符串 name直接用 xml 解析COCO.jsonbbox 为 [x, y, width, height]像素绝对值category_id 整数pycocotools 或 json 解析YOLO.txtx_center、y_center、width、height全部归一化到 0~1整数序号从 0 开始按行读取每行一个目标VOC 的 xml 里size节点还同时存了图片原始宽高转换脚本依赖这个宽高做归一化COCO 的 json 里每张图片对象同样带 width 和 height。YOLO 的 txt 不存图片尺寸因为所有值已经除过宽高所以如果转换时把图片尺寸取错了训练时模型读到的是完全错位的坐标还很难一眼发现。COCO 还有一个隐藏坑bbox是[x, y, width, height]不是[xmin, ymin, xmax, ymax]。很多转换脚本写错就错在这一行后面会单独讲。2.3 解压之后先做一次四件事核对我知道拿到压缩包的人都会急着解压开箱但这里建议先花十分钟做一次数据体检。解压 .rar 用 7-Zip 或 WinRAR解压不完整会直接导致后续训练报“图片解码失败”。第一件事看文件数量是否匹配。用一段简单的 bash 命令在数据集根目录统计cd 铝片数据集目录 for ext in jpg jpeg png xml json txt; do echo $ext: $(find . -name *.${ext} | wc -l) done理想情况下图片数量与 txt 数量一致xml 和 json 数量也应与图片一致。只要数量对不上说明要么解压不完整要么原始数据本身就是缺标签的这时去找缺失原因比直接训练更紧迫。第二件事抽 3 到 5 个 xml看object节点里的 name 是否都在同一个类别集合内抽 3 到 5 个 json看 categories 列表里有多少类。这一步能避免后面训练时报“类别超界”或“标签文件存在但内容为空”。第三件事检查 YOLO txt 里的坐标值是否都在 0~1 之间。如果出现 2.5 这种数字说明转换时没有按图片宽高归一化或者图片本身有 EXIF 旋转导致宽高错位。第四件事随机挑两张图把标注框画上去看一眼。用 OpenCV 就能干这个可视化检查也是后面所有转换脚本的第一道验收标准。传统 OpenCV 缺陷检测靠阈值分割和滤波对光照敏感所以现在大多数铝片表面缺陷方案都转向 YOLO 这类数据驱动模型前提就是标注格式别错。3. 格式转换与划分脚本把数据整理成 YOLO 能直接吃的样子3.1 一套完整目录应该长什么样不管原始压缩包里给了多少种格式训练前最终都要整理成 YOLO 的标准目录结构。常规做法是建一个独立数据集目录aluminum_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml └── classes.txt图片放 images同名 txt 标签放 labelstrain/val/test 三个子目录两边一一对应。YOLOv5 和 YOLOv8 都认这个结构。如果压缩包里只有 VOC 和 COCO 格式的标签那就需要先转成 YOLO txt 再做划分。下面这部分转换脚本是固定的套路可以直接抄。3.2 VOC 转 YOLO解析 xml 并归一化坐标import os import xml.etree.ElementTree as ET CLASSES [scratch, dent, stain] # 按你的数据集实际类别调整顺序 def voc2yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() # 图片尺寸必须从xml里读真实值不能写死 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: continue # 跳过标注里不在类别表中的物体 box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # VOC是左上/右下角点YOLO要中心点宽高且全部除以图片宽高 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{CLASSES.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) base os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(out_dir, base .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines)) # 批量转换 xml_dir annotations/voc txt_dir labels os.makedirs(txt_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): voc2yolo(os.path.join(xml_dir, xml_file), txt_dir)这段脚本值得注意的点有三处一是CLASSES列表的顺序就是最终 txt 里类别编号的映射表一旦按这个顺序转换并开始训练就不要中途改动顺序否则整个标签体系全乱二是坐标换算公式里分母必须用 xml 里记录的原始宽高不能用cv2.imread读出来的宽高因为某些标注软件会修改图片分辨率导致两者不一致三是如果 xml 里存在difficult标记的目标建议直接跳过它的标注本身质量就不可靠。3.3 COCO 转 YOLO避开的那个 [x,y,w,h] 坑import json import os def coco2yolo(annotation_file, label_dir): with open(annotation_file, encodingutf-8) as f: coco json.load(f) images {img[id]: img for img in coco[images]} categories coco[categories] # 关键COCO的category_id不一定是连续的要重新映射成0开始的序号 id2cls {} cat_names [] for idx, cat in enumerate(categories): id2cls[cat[id]] idx cat_names.append(cat[name]) # 按图片id聚合标注 anno_map {} for ann in coco[annotations]: anno_map.setdefault(ann[image_id], []).append(ann) for img_id, anns in anno_map.items(): img images[img_id] img_w, img_h img[width], img[height] base os.path.splitext(img[file_name])[0] lines [] for ann in anns: if ann.get(iscrowd, 0): continue # 群目标不适合常规检测任务 cls_idx id2cls[ann[category_id]] x, y, w, h ann[bbox] # 注意COCO格式是左上角x,y加宽高 cx (x w / 2) / img_w cy (y h / 2) / img_h bw w / img_w bh h / img_h lines.append(f{cls_idx} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(label_dir, base .txt), w, encodingutf-8) as f: f.write(\n.join(lines)) # 调用示例 coco2yolo(annotations/instances_aluminum.json, labels)这里最容易翻车的就是ann[bbox]那行。COCO 的 bbox 语义是[左上角x, 左上角y, 宽, 高]如果按 VOC 的[xmin, ymin, xmax, ymax]去理解中心点坐标会偏到右下角宽高会变成两个角点坐标差值之后再除以图片尺寸结果整个框全部错位。同时COCO 的category_id不一定是从 0 开始连续的必须通过id2cls重新映射直接用原始 id 当 YOLO 类别号会让类别数对不上。3.4 划分脚本训练、验证、测试怎么切才不出事标签转换完成后下一步做数据集划分。常见做法是 70% 训练、15% 验证、15% 测试。如果数据集总量不大验证集和测试集各 15% 就是每边 150 张图够评估用了。import os import random import shutil from collections import Counter random.seed(2024) image_dir images_all # 原始全量图片 label_dir labels_all # 原始全量标签 train_dirs [images/train, labels/train] val_dirs [images/val, labels/val] test_dirs [images/test, labels/test] for path in train_dirs val_dirs test_dirs: os.makedirs(path, exist_okTrue) # 只保留有配对标签的图片 files os.listdir(image_dir) paired [] for f in files: base os.path.splitext(f)[0] if os.path.exists(os.path.join(label_dir, base .txt)): paired.append(f) random.shuffle(paired) n len(paired) n_val int(n * 0.15) n_test int(n * 0.15) val_files set(paired[:n_val]) test_files set(paired[n_val:n_val n_test]) for f in paired: base os.path.splitext(f)[0] img_src os.path.join(image_dir, f) label_src os.path.join(label_dir, base .txt) if f in val_files: shutil.copy2(img_src, os.path.join(images/val, f)) shutil.copy2(label_src, os.path.join(labels/val, base .txt)) elif f in test_files: shutil.copy2(img_src, os.path.join(images/test, f)) shutil.copy2(label_src, os.path.join(labels/test, base .txt)) else: shutil.copy2(img_src, os.path.join(images/train, f)) shutil.copy2(label_src, os.path.join(labels/train, base .txt)) # 校验打印训练集里每个类别的样本数量 def count_cls(label_root): counter Counter() for txt in os.listdir(label_root): with open(os.path.join(label_root, txt)) as f: for line in f: if line.strip(): counter[line.split()[0]] 1 return counter print(train classes:, count_cls(labels/train)) print(val classes:, count_cls(labels/val))这段脚本在划分后立刻打印每个类别在训练集和验证集里的样本数。这一步很关键如果某个冷门缺陷在验证集里一个样本都没有那训练出来的模型对这个缺陷的 mAP 根本无从评估后面看到召回率偏低时先怀疑是数据划分问题而不是模型问题。3.5 写 data.yaml 并跑通第一次训练数据整理完毕后在数据集根目录创建 data.yaml# data.yaml path: /home/user/aluminum_dataset # 改成你的数据集绝对路径 train: images/train val: images/val test: images/test nc: 3 names: [scratch, dent, stain]path必须是绝对路径或者保持相对路径时训练命令要在数据集根目录下执行。nc是类别总数names的顺序必须和转换脚本里的CLASSES完全一致这是整个流程里最容易被忽略但影响最严重的一处一致性问题。跑通第一次训练直接用预训练权重起步# YOLOv5 方式 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt python train.py --data ../aluminum_dataset/data.yaml \ --weights yolov5s.pt \ --epochs 200 \ --batch-size 16 \ --img 640 \ --cache ram \ --name aluminum_v1如果用的是 YOLOv8训练命令更简洁pip install ultralytics yolo detect train data../aluminum_dataset/data.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ cacheTrue这两个命令的语义相同加载 COCO 预训练权重在铝片数据上微调。等第一次训练跑起来再谈参数调优。4. 跑通训练环境配置、参数调优、预训练权重和损失曲线怎么看4.1 预训练权重怎么选为什么不要从零训练标题里的“训练教程”通常默认走 yolov5 或 yolov8 的预训练微调路线。先在 COCO 上训练过的yolov5s.pt或yolov8s.pt权重已经学会了通用的边缘、纹理、形状特征铝片缺陷里的划痕和压伤跟 COCO 里的物体边缘模式有可迁移的部分所以从预训练权重继续训几百个 epoch 就能收敛从零训练同一个数据集常常要两三倍的时间效果还不一定好。YOLOv8 的模型文件在首次执行yolo detect train时会自动下载YOLOv5 需要手动下载yolov5s.pt放到仓库根目录。如果网络受限可以把官方 release 里对应的 .pt 文件提前下好放进来。选择模型规格的参考很直接s 模型在 1080Ti 上基本够跑m 和 l 对显存的要求逐级上升。缺陷检测目标通常较小模型容量对最终 mAP 的影响不如输入分辨率和标签质量大所以先上 s 模型把流程跑通再看瓶颈在哪。4.2 三个最影响缺陷检测效果的训练参数第一个是imgsz。640 是 YOLOv5/v8 的默认值但铝片原图如果是 2000×2000 的大图划痕可能只有几十像素宽直接缩到 640 会让缺陷缩成几个像素模型等于在学“猜位置”。常见做法是先看原图里缺陷框占整图的比例如果面积比例小于 5%建议把训练尺寸提到 1280或者把大图切片成 640×640 的 patches 再训练。切片方案后期会增加推理拼接的复杂度但往往比盲目提升imgsz换显存更划算。第二个是batch-size和cache的配合。batch 太小会让 BN 统计量抖动训练曲线像锯齿batch 太大又撑爆显存。V100 这类大显存卡可以开到 64 甚至 128消费级显卡 16 通常是安全值。--cache ram在显存富余时也能缓解数据读取瓶颈让 GPU 不至于饿着等图。第三个是数据增强。YOLOv8 默认开启 mosaic、mixup、scale、fliplr。小缺陷数据集上 mosaic 有奇效因为它把四张图拼成一张让模型看到更多缺陷出现在不同背景下的组合。但到了训练后期mosaic 会持续干扰 BN 统计导致验证精度上不去。YOLOv8 可以直接在训练命令里加close_mosaic10意思是最后 10 个 epoch 关闭 mosaic 让模型精调这个参数值得一试。4.3 yolo 损失函数的曲线怎么读训练日志里 YOLOv8 会输出box_loss、cls_loss、dfl_loss三个分量YOLOv5 则合成了train/box_loss、train/cls_loss等。很多人只盯着总 loss其实应该分别看。box_loss衡量预测框和真实框的重合度它下降得越快说明定位学习越顺利。cls_loss衡量类别分错的程度缺陷类别相近时这个值下降会比较慢。比如划痕和擦伤本身视觉边界模糊cls_loss半天不降是正常的。dfl_loss是 YOLOv8 里分布式焦点损失服务于框的回归精度。读曲线的节奏一般是前 10 个 epoch 在 warmup下降慢是正常的20 到 50 个 epoch 之间 mAP 开始快速爬升之后进入平台期。如果训练集 loss 还在下降但验证集 mAP 连续 30 个 epoch 不涨说明过拟合已经开始继续加大数据增强不如回头补数据。4.4 训练中 BN 崩溃的现场和补救BN 崩溃是 YOLO 训练里比较典型的翻车现场。表现为 loss 在某个 epoch 突然跳到 NaN或者训练集 mAP 和验证集 mAP 差距大到离谱。原因集中在三处学习率初始值过高导致 BN 层的统计量被极端激活值破坏batch-size 太小使得每个 batch 的均值和方差都不可靠梯度在深层网络里爆炸后无法自愈。常规做法是先保住训练进度把lr0调到 0.0001batch 至少不低于 8再检查标签 txt 里有没有超过 [0,1] 范围的坐标。BN 崩溃不是一个纯玄学问题只要标签坐标合法、学习率合理、batch 不过小它出现的概率极低。5. 避坑做缺陷检测标注和训练时最常见的五处翻车5.1 类别 id 错位模型在推理时把划痕认成压伤现象训练时 loss 正常下降mAP 曲线也正常但推理阶段发现类别跟真实缺陷对不上比如把划痕全部识别成压伤。原因转换脚本里CLASSES列表的顺序和第一次训练时 data.yaml 里names的顺序不是同一套。txt 里的数字 0 在训练时被解释成了names[0]而标注阶段 0 代表的可能是另一个类别。解决训练前把classes.txt和 data.yaml 的names逐行对齐固定后用脚本校验所有 txt 里出现过的类别序号最大值是否小于nc。建议把类别顺序表作为配置文件放在数据集根目录后续任何人重新训练都先读这个文件。5.2 划分后训练报 No labels检查发现标签没同步现象训练一开始就报AssertionError: train: No labels in .../images/train但 labels 目录里明明有内容。原因划分脚本只复制了图片到images/train标签没有按同名对应关系复制到labels/train。YOLO 按图片文件名去找标签文件找不到就不给这张图配 label数量少了就整体报错。解决划分脚本里保证图片和标签用同一个base文件名同步处理并在划分结束后统计images/train和labels/train的文件数做断言ls images/train | wc -l ls labels/train | wc -l两边数量相等再开始训练。5.3 稀有缺陷类别在验证集里消失模型对它们直接失明现象训练结果里总的 mAP 达到 0.8 以上但人工抽检发现某种冷门缺陷全部漏检。原因按图片随机划分时稀有缺陷本来就只出现在十几张图里随机抽样后验证集可能一张都没分到。模型即便学到了该类特征也无法在评估指标里体现因为验证集里压根没有它的样本。解决划分时做分层抽样先按图片包含的类别集合把数据分组再在每组内部按比例切分。最简单的实现是遍历每张图的标签统计图片对应的缺陷类别然后按类别集合分层。如果某个类别样本实在太少可以在训练时额外做该类别图片的过采样或先剔除该类别等数据补齐再训第二版。5.4 COCO 的 bbox 被当成角点坐标边界框全部画到右下角现象转换后抽查可视化框的左上角几乎都贴着真实目标的右下边缘看起来像是框整体偏移。原因COCO 的bbox是[x, y, width, height]转换脚本里按[xmin, ymin, xmax, ymax]算了中心点。解决转换代码里用cx x w / 2而不是(x xmax) / 2。所有转换脚本完成后随机抽取 20 张图做可视化检查把框画在原图上逐张看过再进训练。这一步虽然费几分钟但能省下后面重新转换全数据集的时间成本。5.5 训练中途 loss 变 NaN日志里一片红灯现象训练在 30 到 60 个 epoch 之间突然loss: nan并且之后的验证全部失效。原因学习率设置偏高在缺陷数据这种样本量小的数据集上梯度更新幅度过大BN 统计量被推入数值不稳定区域另一类常见原因是标签坐标超过 [0,1]比如归一化后出现 2.3 这样的值反向传播直接产生 NaN。解决先把lr0降到 0.0001batch-size 提到 16 以上再用脚本扫描所有 txt 坐标过滤出数值范围超界的标签文件修正源头。注意不要通过降低训练周期数来掩盖这个问题它的根因在数据或超参不在训练代数。6. 验证模型混淆矩阵、PR 曲线与产线现场复核的边界训练完成后用最佳权重对测试集做验证yolo detect val data../aluminum_dataset/data.yaml \ modelruns/detect/aluminum_v1/weights/best.pt运行时生成的confusion_matrix.png和PR_curve.png才是真正要细看的文件。很多人在这一步会问为什么混淆矩阵里每个格子的数字加起来不是测试集的目标总数这是一个很正常的现象。YOLOv5/v8 的混淆矩阵除了各个类别之间互相错分的目标还包含背景被误检成缺陷的列、以及缺陷被漏检后计入背景的行所以总数天然比目标总数多。不要盯着这个数字算准确率要看归一化后每行每列的比例重点确认哪两类缺陷容易被互相混淆。PR 曲线决定的是置信度阈值的选择。产线缺陷检测的诉求通常是“宁错杀、不漏过”因为漏检一块缺陷铝片的损失比多检几块片子重得多。实际操作里把置信度阈值设到 0.15 到 0.25 之间换取更高的召回率后续再用人工或复检工位排除误检比把阈值拉高追求精确率更符合现场逻辑。最后一步是 badcase 人工复核。把验证集里置信度低于阈值但真实存在缺陷的图、以及置信度很高但检测框明显对不上的图全部导出逐张看一遍。我个人的习惯是每次都把坏案例截图存到一个单独目录对比上一版模型确认新增的漏检是数据分布问题还是模型退化问题再决定下一步是补数据还是调参数。这套流程走下来一个 YOLO 铝片缺陷检测方案能不能投入现场心里就有底了而不是只看最后打印出来的 mAP 数字。希望帮到你。本文还有配套的精品资源点击获取
返回列表