
简介YOLO铝片表面缺陷检测数据集是一套面向工业视觉与目标检测学习者的标注图像资源能够直接用于YOLO系列模型的缺陷识别训练与算法验证。压缩包共2000个文件大小约60.69MB标签部分提供VOC(xml)、COCO(json)和YOLO(txt)三种格式标注并配套HTML格式的Windows/Linux环境搭建与训练教程、Python划分脚本及YAML配置文件适合不同环境下的快速上手。已有932人浏览学习热度稳定。除高质量标注数据外还附有三套数据集划分脚本可按照实际需求灵活生成训练集、验证集和测试集教程覆盖环境安装、案例修改到自定义数据训练能帮助初学者少走弯路也方便中高级开发者直接复用数据流程。1. 铝片表面缺陷检测为什么值得用 YOLO 自己做一套金属铝材在轧制、冲压、运输过程中表面容易出现划痕、凹坑、氧化斑、压伤这几类典型缺陷。产线上常见的做法是靠老师傅肉眼盯速度慢、标准不统一而且细划痕在特定光照下才看得到漏检率一直压不下来。用 YOLO 做铝片表面缺陷检测就是把“老师傅的眼睛”换成模型输入一张铝片表面图像输出每个缺陷的位置框和类别。相比传统机器视觉的边缘检测、阈值分割方案YOLO 这类单阶段检测器对小目标、低对比度缺陷更鲁棒而且一套标注数据训完换产线只需增量标注不用重写算法。这套数据集的组合拳——1000 张图片 voc/coco/yolo 三种格式标签 划分脚本 训练教程正好对应了缺陷检测落地最常被卡住的四件事标注格式不统一、数据划分不合理、训练参数不会调、训练完不知道模型到底行不行。下面直接按这个顺序把这四条链路拆开讲每一步给可复现的命令和代码。2. 三种标注格式的本质区别与互转方法2.1 为什么同一个数据集要同时存在 voc、coco、yolo 三种标签刚接触目标检测的人最容易困惑的是明明都是画框为什么同一张图要存三份标签根源在于不同训练框架读取标签的方式不同。VOCPASCAL VOC格式一张图对应一个 XML 文件框坐标用xmin, ymin, xmax, ymax绝对像素值表示类别用字符串写在name标签里。适合人读适合标注工具导出但训练框架读取时需要额外解析。COCO 格式所有标注汇总到一个 JSON 文件按images、annotations、categories三个数组组织。框坐标是[x, y, width, height]浮点绝对像素值。它的优势是能同时表达检测、分割、关键点信息MMDetection、Detectron2 原生支持但手写 JSON 极易出错。YOLO 格式darknet / ultralytics 通用每张图对应一个 txt 文件每行是class_id x_center y_center width height坐标全部归一化到 0-1 区间类别用整数 id 表示。这种格式最紧凑训练读取最快但坐标是相对值一旦图片尺寸变了框会跟着图等比缩放不会错位。工业界常见做法是标注工具如 LabelImg、X-AnyLabeling导出 VOC然后自己写转换脚本生成 COCO 和 YOLO。这样做的好处是标注阶段永远只维护一份源标注另外两种格式随时可以重新生成不会出现三份标签改了一份忘了另外两份的同步事故。2.2 用 Python 脚本把 VOC 转成 YOLO 格式假设标注目录结构如下Annotations/ 存放 xml 文件 JPEGImages/ 存放 jpg 图片转换脚本如下import xml.etree.ElementTree as ET import os # 类别列表顺序就是 YOLO 训练时的 class_id CLASSES [scratch, indentation, oxidation, stain] def voc_to_yolo(xml_path, out_dir, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: continue # 跳过未定义类别 class_id CLASSES.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 除 0 保护正常情况下 VOC 坐标不会越界 x_center ((xmin xmax) / 2) / img_width y_center ((ymin ymax) / 2) / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height # 裁剪到合法区间防止浮点误差 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) w min(max(w, 0), 1) h min(max(h, 0), 1) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: return False # 没有有效标注的图片直接跳过 txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) return True这段代码的核心是坐标系的换算VOC 存的是左上角和右下角的绝对像素值YOLO 要的是中心点和宽高的相对值。除以img_width和img_height这一步是把像素坐标拉回到 0-1 区间和图片本身分辨率无关了。注意CLASSES列表的顺序一旦确定就不能随便调整否则之前生成的 txt 标注全部会错位。这里还加了边界裁剪如果标注框框出图像边缘标注工具常见手滑归一化后会出现大于 1 的值YOLO 训练时通常会直接报错。2.3 VOC 转 COCO 时最容易踩的坑COCO 的 JSON 结构比 YOLO 复杂得多很多人手动拼接容易在category_id上出错。COCO 的类别 id 不要求从 0 开始也不要求连续但训练框架在加载时依赖categories数组里的 id 和annotations里的category_id严格对应。import json import xml.etree.ElementTree as ET import os from PIL import Image def voc_to_coco(image_dir, xml_dir, output_json): categories [ {id: 1, name: scratch}, {id: 2, name: indentation}, {id: 3, name: oxidation}, {id: 4, name: stain}, ] # 构建 name - id 映射 cat_map {c[name]: c[id] for c in categories} images, annotations [], [] ann_id 1 for idx, xml_name in enumerate(os.listdir(xml_dir)): if not xml_name.endswith(.xml): continue img_name xml_name.replace(.xml, .jpg) img_path os.path.join(image_dir, img_name) with Image.open(img_path) as img: w, h img.size images.append({ id: idx 1, file_name: img_name, width: w, height: h, }) tree ET.parse(os.path.join(xml_dir, xml_name)) root tree.getroot() for obj in root.iter(object): name obj.find(name).text if name not in cat_map: continue bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) annotations.append({ id: ann_id, image_id: idx 1, category_id: cat_map[name], bbox: [xmin, ymin, xmax - xmin, ymax - ymin], area: (xmax - xmin) * (ymax - ymin), iscrowd: 0, }) ann_id 1 coco {images: images, annotations: annotations, categories: categories} with open(output_json, w) as f: json.dump(coco, f, indent2) print(fdone: {len(images)} images, {len(annotations)} annotations)这里特别提醒两点第一COCO 的bbox是[x, y, width, height]不是两点坐标漏掉这个转换会让框在可视化时全部偏到右下角第二image_id必须是整数且在images数组中存在对应记录否则 MMDetection 在评估阶段会报 KeyError。铝片缺陷这个场景里一张图上可能有十几个细划痕每个划痕都要单独一个 annotation 对象ann_id自增不能重置。3. 划分脚本的正确写法分层采样和同源去重3.1 随机划分在缺陷检测里的隐患很多人拿到数据集第一件事就是train_test_split(test_size0.2, random_state42)这在自然图像分类上问题不大但在工业缺陷检测上会出状况。铝片缺陷数据往往是连拍的同一个缺陷区域从不同角度、不同光照拍了多张图。如果这些图被随机分配到训练集和验证集验证集里就会出现和训练集高度相似的图mAP 会虚高到 0.9 以上一上产线直接崩回 0.3。这就是典型的“数据泄漏”。解决思路是按缺陷样本的“来源批次”或“文件名前缀”做分组划分保证同一个来源的所有图片只进一个集合。如果标注文件里没有批次信息常见的做法是给图片文件名的前几位加上炉批号或产线编号工业采集软件一般都能配置。3.2 带分层逻辑的划分脚本实现下面给出一个适合 1000 张级别的划分脚本支持两种模式按文件名前缀分组划分以及按类别分层划分。import os import random import shutil from collections import defaultdict def group_split_by_prefix(file_list, prefix_len8, train_ratio0.8): file_list: 图片文件名列表 prefix_len: 文件名前 N 个字符作为分组依据如炉批号 返回划分后的 train/val 字典 groups defaultdict(list) for f in file_list: key f[:prefix_len] # 同批次图片共享前缀 groups[key].append(f) group_keys list(groups.keys()) random.shuffle(group_keys) split_idx int(len(group_keys) * train_ratio) train_groups group_keys[:split_idx] val_groups group_keys[split_idx:] train_files [f for k in train_groups for f in groups[k]] val_files [f for k in val_groups for f in groups[k]] return train_files, val_files # 使用示例 all_images [f for f in os.listdir(images) if f.endswith(.jpg)] train, val group_split_by_prefix(all_images, prefix_len8) print(ftrain: {len(train)}, val: {len(val)})核心逻辑是先把图片按前缀分组再打乱组而不是打乱单张图。这样同源图片保证进入同一个集合。prefix_len的取值要看实际文件名结构如果前缀 8 位是炉批号那就用 8如果不足 8 位是日期截到日期即可。另外划分完要做类别分布检查import os def check_class_distribution(txt_dir, num_classes4): counts defaultdict(int) for txt_name in os.listdir(txt_dir): with open(os.path.join(txt_dir, txt_name)) as f: for line in f: class_id int(line.split()[0]) counts[class_id] 1 total sum(counts.values()) for cid in range(num_classes): ratio counts[cid] / total if total else 0 print(fclass {cid}: {counts[cid]} ({ratio:.2%})) check_class_distribution(yolo_labels_train)这一步很有必要。1000 张工业图片里大概率是划痕占 70%、氧化斑占 10%少样本类别可能只有几十个框。如果验证集里某个类别一个框都没有训练完看验证 mAP 会缺失这个类别的评估调参方向直接跑偏。工业缺陷检测的划分目标不是机械地按比例切而是保证验证集里每个类别都有足够样本——一般建议验证集里最少见类别不低于 5 个标注框。3.3 划分后的目录组织建议一套干净的目录结构比名字花哨的管理脚本更重要推荐结构如下dataset/ ├── images/train/ ├── images/val/ ├── labels/train/ ├── labels/val/ ├── data.yaml └── classes.txtdata.yaml是 ultralytics YOLO 训练框架的标准配置直接指向上述目录train: dataset/images/train val: dataset/images/val nc: 4 names: [scratch, indentation, oxidation, stain]一个常见低级错误是图像在images/train标签在labels/val目录名不配对导致训练时全部图片都没有标签。YOLO 框架的标签加载逻辑是“根据图片目录自动找同级的 labels 目录”如果目录层级不对运行训练命令时不会报错但损失函数从头到尾不下降这种情况在排查时最容易浪费时间。4. 训练教程铝片缺陷数据集的参数设置与完整流程4.1 模型选型和预训练权重1000 张图片属于典型的小样本工业视觉数据集。用 YOLOv5s 还是 YOLOv8n我的判断依据是数据量越少模型骨架越小越好。YOLOv8n 的参数量约 3.2MYOLOv5s 约 7.2M在小数据集上后者更容易过拟合——表现为训练集 loss 降到 0.01验证集 mAP 却卡在 0.5 以下。铝片背景单一、目标特征简单划痕是线性结构、凹坑是圆形暗区不需要深层网络提取复杂语义所以首选 YOLOv8n 或 YOLOv5s。用预训练权重而不是随机初始化在这个场景下收益非常明显。虽然 ImageNet 上没有铝片缺陷这类工业图像但预训练模型已经具备了边缘、纹理、形状的基础特征提取能力微调时只需要把最后的检测头适配到 4 类缺陷上。4.2 核心训练命令与参数调优ultralytics YOLOv8 训练命令yolo detect train \ modelyolov8n.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ optimizerSGD \ lr00.01 \ augmentTrue \ projectaluminum_defect \ nameexp1参数说明imgsz640输入分辨率。铝片划痕是细长小目标建议 640 起步如果 GPU 显存足够8G 以上可以试 832小目标召回率通常能提升 3-5 个点。batch16受显存限制。训练时观察 GPU 利用率如果低于 70%加大 batch 或提高 imgsz。patience20验证集 mAP 连续 20 轮不上升就早停。1000 张图大约 80-100 轮就会收敛设 100 epochs 配早停是合理组合。optimizerSGD缺陷检测建议用 SGD 而不是 Adam。Adam 收敛快但最终精度往往不如 SGD小数据集上差距更明显。lr00.01这个值在 COCO 上有效但铝片数据集只有 4 类若训练 loss 剧烈震荡降到 0.005 再试。训练时的数据增强策略也要针对铝片表面特征调整。随机旋转参数默认是 0 度不要打开铝片有明确的纹理方向旋转 90 度会让划痕方向语义错乱水平划痕变成垂直划痕。在data.yaml同级目录放一个augment.yaml覆盖默认增强# augment.yaml fliplr: 0.5 flipud: 0.0 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.5 translate: 0.1 scale: 0.3 mosaic: 0.5铝片表面是金属反光材质光照不均很常见所以 HSV 增强里的饱和度和亮度扰动适度加大有助于泛化但色相扰动不要超过0.015——氧化斑的颜色是特征之一色相扰动过大会让模型把正常铝面误判为氧化斑。4.3 训练过程中看什么指标训练日志里重点盯三个数值train/box_loss、val/box_loss、metrics/mAP50。train/box_loss前 10 轮应从 0.08 左右平滑降到 0.03 以下如果 10 轮后还在 0.08 以上震荡基础学习率太高。val/mAP50到第 30 轮左右应超过 0.6如果还在 0.3 附近徘徊优先怀疑标签问题——用可视化脚本画几张图看标注框是否贴合缺陷边缘。如果出现train loss不断下降但val loss出现拐点回升说明过拟合开始减少 epochs 或加强增强。一个铝片检测特有的问题划痕类目标的长宽比极端细长条YOLO 默认的 anchor 是 COCO 统计出来的通用值对长条形目标不友好。如果训练时发现划痕类 AP 明显低于其他类且召回率recall特别低用聚类脚本重新生成 anchor。YOLOv8 的 anchor 是自适应解耦头不需要手动设 anchor 尺寸但如果用 YOLOv5需要在训练前跑python train.py --data data.yaml --epochs 100 --hyp data/hyps/hyp.scratch-low.yaml --multi-scale--multi-scale每 10 轮随机缩放输入尺寸0.5-1.5 倍等于给模型做了尺度扰动对尺寸分布宽的缺陷目标有帮助。4.4 训练完成后必须做的验证训练结束不要直接拿best.pt就去部署。先用测试集划分脚本里留出的那部分图片跑推理再算一下各类别的完整指标。ultralytics 自带的验证命令yolo detect val modelruns/aluminum_defect/exp1/weights/best.pt datadata.yaml splittest这里注意splittest这个参数如果data.yaml里没有定义test字段它会自动 fallback 到 val输出结果会高估真实表现。正确做法是在data.yaml里补上test: dataset/images/test验证输出里metrics/precision(B)和metrics/recall(B)是总体精度和召回率但更关键的是看每个类别的 AP。用分类别指标判断是整体问题还是少数类问题如果scratch类的 AP 只有 0.4而其他类在 0.8 以上不要盲目调全局参数优先尝试单独对该类做样本增强在训练图像上叠加模拟划痕。5. loss 异常和 mAP 低的两类典型问题排查训练到中途 loss 变 NaN 是工业数据集最常见的翻车现场常见原因和处理方式如下表现象可能原因处理动作训练第 5-10 轮 loss 变 NaN学习率过高梯度爆炸lr0从 0.01 降到 0.001或改用optimizerAdamWloss 从第二轮开始就是 NaN标签坐标出现 0 或负值检查 YOLO 标签文件中是否有超出 0-1 区间的坐标值刚开始训练就报错CUDA error: device-side assert triggerednc数量与标签中最大类别 id 不匹配检查data.yaml中的nc是否等于类别数标签中是否有大于nc-1的 idloss 不下降、反复横跳batch size 太小或背景占比过高调大 batch或检查是否大量图片标注为空背景图占比超过 30%第二个高发问题mAP50 不低但 mAP50-95 特别低。这通常意味着框的位置基本正确但 IoU 阈值拉高后评分下降也就是“框得不够准”。1000 张铝片图上标注框如果有 1-2 像素的偏差mAP50-95 就会从 0.7 掉到 0.5。缓解手段有两个一是检测头的回归分支更敏感选iou0.2的 NMS 阈值默认 0.45 对密集小目标过于激进二是使用--box 0.05增大边框回归损失权重YOLOv8 用box0.05参数直接控制。还有一个被低估的收益点类别不平衡导致的误检。氧化斑和正常铝面反光在灰度特征上极其相似如果数据集中氧化斑只有 30 张图模型大概率把高光区域误检为氧化斑。对这种难例最有效的手段不是调参而是加数据。albumentations做在线增强时可以对训练图像叠加灰度渐变模拟不均匀光照import albumentations as A transform A.Compose([ A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.GaussNoise(var_limit(10.0, 50.0), p0.3), ])这个增强策略针对的是铝片反光特性产线光照不可能完全均匀训练时让模型见过明暗不均的输入推理时才能对高光区域有免疫力。6. 部署时的一个实用技巧置信度阈值校准训练完模型部署到产线时大部分人直接把置信度阈值设为 0.5这是把训练时的评估阈值直接搬到了推理场景。铝片检测和通用目标检测有个重要差异产线上漏检一块缺陷的代价远高于多检一次多检顶多停机人工复检漏检直接流到客户手里变成客诉。看验证集输出的results.csvultralytics 会自动生成找到每个类别的 PR 曲线数据。挑选阈值时以召回率为优先约束import pandas as pd df pd.read_csv(runs/aluminum_defect/exp1/results.csv) # 取每行各类别的 confidence 阈值 # 目标是整体 recall 不低于 0.95 target_recall 0.95 for conf in [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]: metrics df[df[conf] conf] recall metrics[recall].mean() if recall target_recall: print(fselected conf: {conf}, recall: {recall:.4f}) break推理时的置信度阈值是单独传参的YOLO 的 Python API 写法from ultralytics import YOLO model YOLO(best.pt) results model(inspection_image.jpg, conf0.15, iou0.45) for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls int(box.cls[0]) print(f缺陷类别 {cls}置信度 {conf:.3f}位置 ({x1:.0f},{y1:.0f})-({x2:.0f},{y2:.0f}))conf0.15看起来很低实际部署时这个值是否合理要看误检代价。如果现场误检频繁导致停机复检成本过高再往上调 0.05 步进每次调整后至少留 200 张没有缺陷的良品图做负样本验证确保误检率在可接受范围内。这一步在缺陷检测里常常被忽略但它在实际应用中的作用比调整模型网络结构更大。本文还有配套的精品资源点击获取