ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

铝片表面缺陷检测:400张VOC+YOLO数据集训练与调优实战

铝片表面缺陷检测:400张VOC+YOLO数据集训练与调优实战 简介本资源为面向工业质检与机器视觉方向的铝片表面缺陷检测数据集适用于目标检测模型训练、算法验证及课程实践尤其适合需要真实产线缺陷样本的中高级开发者与研究人员。数据集共400张jpg图片同步提供Pascal VOC格式xml标注与YOLO格式txt标注标注工具为labelImg覆盖ca_shang、zang_wu、zhe_zhou、zhen_kong四类缺陷各类框数分别为270、456、124、212总框数达1062类别分布清晰便于直接用于YOLO或VOC系列检测网络训练与评估。压缩包为7z格式内含1202个文件其中402个txt、400个xml、400个jpg整体约15.25MB体积轻量、结构规整方便快速解压与划分训练集验证集。目前已有526人学习下载可作为铝片表面缺陷识别、工业质检算法对比与数据增强实验的实用基础数据。1. 铝片表面缺陷检测400 张 VOCYOLO 数据集到底能不能训出能用的模型手里只有 400 张铝片表面缺陷图4 个类别VOC 和 YOLO 两种标注格式各一份很多人第一反应是「这点数据能干嘛」。我一开始也这么想直到产线上真拿它跑了一版基线才发现小样本工业缺陷检测的关键从来不是数据量绝对值而是类别边界清不清楚、标注一致性够不够、增强策略对不对路。铝片表面的典型缺陷——划痕、凹坑、脏污、氧化斑——在灰度上差异其实不小400 张如果覆盖了主要光照和角度做一版可用的初筛模型完全够。这篇笔记就围绕这份铝片表面工业缺陷检测数据集VOCYOLO 格式400 张4 类别把从数据检查、格式转换、训练配置到踩坑排查的完整路径讲清楚适合刚接手工业质检项目、手里只有小样本数据集的算法和视觉工程师。2. 先搞清楚 VOC 和 YOLO 两套标注到底差在哪2.1 VOC XML 与 YOLO TXT 的结构差异VOC 格式每张图对应一个 XML 文件核心字段是size里的宽高和object里的类别名与bndbox的 xmin/ymin/xmax/ymax。YOLO 格式每张图对应一个 TXT每行是class_id cx cy w h全部归一化到 0~1。两者不是简单换个后缀坐标系原点、是否归一化、类别是字符串还是索引三处都不一样。很多人拿到数据集直接开训结果 mAP 一直是 0回头查才发现 TXT 里的坐标没归一化或者 class_id 从 1 开始而不是 0。铝片缺陷这类目标通常偏小划痕可能是细长条凹坑可能只有几十像素。VOC 的绝对像素坐标在转换时如果没做边界裁剪xmax 超出图像宽度归一化后 w 会大于 1训练时框直接飞出去。所以转换前先做一轮合法性检查比转换本身更重要。2.2 用脚本做一次全量标注体检下面这段脚本同时检查 VOC XML 的边界合法性和类别分布输出问题文件清单。建议在转换前跑一遍把有问题的图挑出来人工复核。import os import xml.etree.ElementTree as ET from collections import Counter ANNO_DIR annotations # VOC XML 存放目录 IMG_DIR images CLASSES [scratch, pit, stain, oxidation] # 按实际类别名改 def check_voc(anno_dir, img_dir, classes): issues [] counter Counter() for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue path os.path.join(anno_dir, xml_file) tree ET.parse(path) root tree.getroot() size root.find(size) W int(size.find(width).text) H int(size.find(height).text) # 检查对应图片是否存在 img_name os.path.splitext(xml_file)[0] .jpg if not os.path.exists(os.path.join(img_dir, img_name)): issues.append((xml_file, missing_image)) for obj in root.findall(object): name obj.find(name).text counter[name] 1 if name not in classes: issues.append((xml_file, funknown_class:{name})) 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) # 边界合法性不能越界宽高不能为负 if xmin 0 or ymin 0 or xmax W or ymax H: issues.append((xml_file, fout_of_bound:{xmin},{ymin},{xmax},{ymax})) if xmax xmin or ymax ymin: issues.append((xml_file, zero_or_neg_box)) return issues, counter issues, counter check_voc(ANNO_DIR, IMG_DIR, CLASSES) print(类别分布:, counter) print(问题文件数:, len(issues)) for f, r in issues[:20]: print(f, r)逻辑说明先读size拿到图像宽高再逐个object检查类别名是否在预设列表内、框是否越界、宽高是否非正。参数上CLASSES必须和后续 YOLO 的names顺序一致否则类别索引会错位。跑完如果out_of_bound较多说明标注时用了缩放后的坐标或裁剪图坐标需要先还原到原图尺寸再转 YOLO。2.3 VOC 转 YOLO 的转换脚本与四个边界坑转换本身不复杂坑都在边界处理上。下面脚本把 VOC 转成 YOLO TXT同时处理越界裁剪和类别映射。import os import xml.etree.ElementTree as ET ANNO_DIR annotations OUT_DIR labels CLASSES [scratch, pit, stain, oxidation] os.makedirs(OUT_DIR, exist_okTrue) def voc_to_yolo(anno_dir, out_dir, classes): cls2id {c: i for i, c in enumerate(classes)} for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_file)) root tree.getroot() size root.find(size) W int(size.find(width).text) H int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in cls2id: 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) # 关键先裁剪到图像范围内再归一化 xmin max(0, min(xmin, W)) xmax max(0, min(xmax, W)) ymin max(0, min(ymin, H)) ymax max(0, min(ymax, H)) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2.0 / W cy (ymin ymax) / 2.0 / H w (xmax - xmin) / W h (ymax - ymin) / H lines.append(f{cls2id[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines)) voc_to_yolo(ANNO_DIR, OUT_DIR, CLASSES)四个边界坑第一坐标越界不裁剪归一化后 w/h 大于 1训练时损失异常第二类别名大小写或空格不一致映射不到直接丢框第三图像实际尺寸和 XML 里size不一致常见于中途换过图但没更新标注第四空标注图没生成空 TXTYOLO 训练时按文件名找标签会报缺失。转换后建议随机抽 5 张用可视化脚本画框核对别嫌麻烦这一步省了后面调参全是玄学。3. 400 张 4 类别怎么切分和增强才不浪费3.1 小样本下的训练验证划分策略400 张 4 类平均每类 100 个框左右有的类可能只有几十个。这种量级下按 8:1:1 随机切分验证集每类可能只剩个位数mAP 波动极大今天 0.6 明天 0.4根本没法判断模型好坏。我一般用分层抽样保证每个类别在训练集和验证集里的框数量比例接近验证集每类至少留 15 个框。如果某类实在少就把它在验证集里的比例调低但至少留 5 个否则那一类的 AP 没有统计意义。切分脚本核心是按类别分组后分别 shuffle 再按比例取不要直接对文件列表 shuffle。另外铝片缺陷图如果来自连续拍摄相邻帧高度相似随机切分会导致训练集和验证集泄漏验证指标虚高。正确做法是按拍摄批次或时间分组切分同一批次的图要么全在训练集要么全在验证集。3.2 针对铝片缺陷的增强参数怎么设通用增强里翻转、旋转、亮度对比度扰动对铝片缺陷都有效但要注意几点。划痕有方向性水平翻转和垂直翻转可以但 90 度旋转会让划痕方向分布偏移如果产线上划痕方向固定旋转增强反而引入噪声。Mosaic 增强对小目标友好但 400 张里如果小目标占比高Mosaic 四拼后单张小目标更小可能低于模型下采样后的最小感受野建议 Mosaic 概率不要开到 1.00.5 左右先试。HSV 扰动里铝片表面反光强亮度变化范围可以大一点但饱和度不要动太多因为缺陷和背景的色差本来就小。下面是一份 YOLOv8 训练配置里增强相关的参数片段按小样本工业缺陷场景调过。# 数据配置 data.yaml path: ./aluminum_defect train: images/train val: images/val names: 0: scratch 1: pit 2: stain 3: oxidation # 训练超参命令行或 cfg 里覆盖 # hsv_h: 0.015 色调扰动小缺陷色差本来就弱 # hsv_s: 0.3 饱和度适中 # hsv_v: 0.5 亮度扰动大模拟产线光照波动 # degrees: 10.0 小角度旋转避免划痕方向分布偏移 # translate: 0.1 # scale: 0.3 缩放适中小目标别缩太小 # fliplr: 0.5 # flipud: 0.5 铝片无固定上下方向可开 # mosaic: 0.5 小样本别开满防止小目标过小 # mixup: 0.0 小样本 mixup 容易造出不合理纹理先关参数说明hsv_v给到 0.5 是因为铝片反光导致同一缺陷在不同光照下灰度差异大增强亮度能提升泛化mosaic降到 0.5 是血泪经验开满后验证集 mAP 反而掉原因是四拼图里小缺陷被压缩到几个像素模型学不到有效特征。mixup在工业缺陷上我基本不开两种缺陷叠加出来的纹理在真实产线不存在属于自欺欺人。3.3 用预训练权重和冻结策略稳住小样本训练400 张从头训基本没戏必须用 COCO 预训练权重。YOLOv8n 或 YOLOv8s 在 400 张上微调前 10 个 epoch 冻结 backbone只训 head学习率可以给到 0.001之后解冻全部学习率降到 0.0001 再跑 50~100 epoch。冻结阶段的作用是让 head 先适配 4 类缺陷的输出维度避免随机初始化的 head 把 backbone 带偏。如果验证集 mAP 在解冻后先升后降说明过拟合了早停 patience 设 20 左右。另外类别不均衡时可以在损失里给少样本类加权但权重别超过 3否则模型会疯狂预测少样本类误检飙升。铝片缺陷检测里误检比漏检更让人头疼产线上一堆假报警工人直接不信任系统。4. 训练和推理环节最容易翻车的几个地方4.1 数据路径和类别索引对不上现象训练启动就报Label class xxx exceeds nc或者训练正常但推理时类别名全错。原因通常是 data.yaml 里names的顺序和 TXT 里 class_id 的映射不一致或者转换脚本里类别列表顺序和 yaml 不同。解决把转换脚本里的CLASSES和 data.yaml 的names用同一份配置生成别手写两遍。推理后处理时类别名从 yaml 读不要硬编码。4.2 小目标在默认 anchor 下召回低现象划痕、小凹坑这类目标 mAP 明显低于其他类可视化发现大量漏检。原因默认 anchor 或 YOLOv8 的 anchor-free 头对小目标特征响应弱加上输入分辨率 640 下采样 32 倍后几十像素的缺陷只剩几个特征点。解决把输入尺寸提到 960 或 1280 再训一版对比或者用切图推理把大图切成小块分别检测再合并。代价是推理速度下降产线节拍紧的话要权衡。4.3 验证集指标高但产线误检多现象验证集 mAP 0.85上线后误检一堆工人抱怨。原因验证集和训练集同分布但产线光照、铝片型号、相机角度和数据集采集时不同域偏移导致模型把正常纹理当缺陷。解决从产线新采一批正常图只含背景不含缺陷作为负样本加入训练集比例大概 1:5让模型学会区分正常纹理和缺陷。这一步比调任何超参都管用。4.4 标注框贴边导致训练不稳定现象loss 震荡某些 epoch 突然飙升。原因部分标注框紧贴图像边缘增强时旋转平移后框被裁掉一半归一化坐标异常。解决转换阶段对贴边小于 2 像素的框做内缩或者增强时对贴边目标降低变换幅度。铝片图如果拍摄时缺陷经常在边缘这条尤其要注意。4.5 类别名带空格或中文导致映射失败现象转换后 TXT 里某些类别丢失或者训练时提示类别数不对。原因VOC XML 里类别名有前后空格、大小写不一致、或者混了中文。解决转换前统一 strip 并转小写用映射表把别名归一到标准名。别指望模型自动处理这些标注规范问题必须在数据层解决。5. 把 400 张用到极致的两个进阶技巧第一个技巧是切图训练加合并推理。铝片图如果分辨率较高比如 2000×2000 以上直接缩到 640 会丢失小缺陷。做法是把原图切成 640×640 有重叠的子图重叠 128 像素用子图训练推理时也切子图检测框映射回原图后用 NMS 合并。这样等效于把 400 张变成几千张训练样本小目标召回提升明显。代价是训练和推理时间增加但工业质检通常单张图允许几百毫秒完全扛得住。切图时注意边缘子图要 pad 到 640否则尺寸不一致。第二个技巧是用验证集做一轮困难样本挖掘。训练完第一版后把验证集里漏检和误检的图挑出来人工复核标注把漏标的补上、错标的改掉然后加入训练集重新训。400 张里通常有 10%~20% 的标注质量问题这一轮下来 mAP 能涨 3~5 个点比换模型结构划算得多。我一般会写个脚本把预测结果和 GT 画在一起按 IoU 排序优先看 IoU 在 0.3~0.5 之间的框这些是模型「犹豫」的地方往往对应标注模糊或难样本。最后说个习惯每次改完数据或增强参数先跑 10 个 epoch 看 loss 曲线和验证集前几张预测图别等训完 100 epoch 才发现类别映射错了。小样本训练试错成本低但前提是你每次只改一个变量并且留好上一版的权重和配置。铝片缺陷检测这方向数据质量的决定性远大于模型选型400 张标注干净的图比 4000 张脏数据训出来的模型靠谱得多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进