ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

焊接件表面缺陷检测实战:VOC转YOLO格式与训练避坑指南

焊接件表面缺陷检测实战:VOC转YOLO格式与训练避坑指南 简介面向焊接件表面缺陷检测的标注数据集适用于目标检测算法训练、模型评估和工业质检场景研究也适合初学者作为缺陷识别入门样本。数据覆盖折痕、月牙间隙、夹杂、油斑、冲孔、压坑、丝斑、腰部折叠、水斑、焊缝线共10个缺陷类别标注框总计3563个类别名称与标签一一对应既有表面污染类缺陷也有冲压、轧制环节产生的结构异常分布贴近真实产线便于观察不同缺陷的形态差异。包内共2000个文件其中1999个为XML标注文件、1个为说明txt压缩后约256MB文件命名连续、目录结构清晰可按批次灵活拆分训练集、验证集。已有1134人学习下载适合算法工程师、科研人员和学生快速上手直接用于模型微调、指标对比与检测方案验证省去大量人工标注时间。1. 焊接件表面缺陷检测数据集2292 张图够训练一个能上线的缺陷模型吗焊接质检工位最缺的不是算法而是干净、标签不乱、格式统一的图像数据。焊接件表面缺陷检测数据集VOCYOLO 格式、2292 张、10 类别刚好是这类资源里比较典型的样本类别覆盖气孔、咬边、焊瘤等常见焊接缺陷格式上同时给了 XML 标注和 YOLO 的 txt 标注省去自己从裸图开始标注的功夫。不过拿到数据别急着开训。这套数据真正的价值不是“能跑通”而是帮你验证一套完整流程格式解析、类别核对、坐标换算、分层划分、训练参数调整、小目标优化。它是工业检测里“小样本 类别不均衡 缺陷尺度差异大”的缩影把它啃下来换到其他质检数据集上你也能举一反三。这篇文章就是按这条链路写的适合刚入行做工业视觉检测、或者拿检测模型做毕设的开发者也适合想在产线上快速验证缺陷识别可行性的工程师。2. VOC 与 YOLO 双格式解包标注结构决定你的训练下限2.1 10 个缺陷类别怎么定从焊工质检表到模型标签先别管格式先看类别。焊接件表面缺陷在工业上有一张通用质检表这套数据集的 10 类基本对应主流缺陷类型气孔porosity、咬边undercut、焊瘤overlap、裂纹crack、未焊透incomplete penetration、夹渣slag inclusion、飞溅spatter、凹陷sag、偏焊misalignment、过烧burn through。每个类别在视觉上有明显差异也有一部分互相混淆。气孔是圆形或椭圆形的暗斑尺寸通常很小分布在焊缝表面或内部飞溅是焊接时溅出的金属小颗粒形状不规则常散落在焊缝附近检测时容易跟气孔混在一起。咬边是焊趾处被烧出的沟槽形态细长紧贴焊缝边缘焊瘤则是熔池外溢形成的金属疙瘩形状鼓出投影面积比气孔大一个量级。裂纹最不好办它细、长、对比度低在金属纹理背景里经常断成好几段一个标注框里可能只框住其中一段。这类别设计对检测模型的挑战在于两点一是小目标占比高气孔、飞溅、裂纹这类缺陷在原图里可能只有十几个像素宽二是部分类别形状相似加背景复杂焊接飞溅、弧光、反光会制造大量假候选框。所以标签的规范程度直接决定训练效果。如果数据集里类别名不统一比如同一家工厂把“气孔”标成“porosity”另一批标成“blowhole”模型就会把同一个缺陷当成两个类mAP 必然被拖垮。我拿到数据第一件事就是枚举全部 XML 里的 name 字段确认实际类别名和 names 列表完全一致这一步能避免后面所有类别错位的问题。2.2 VOC 标注的 XML 结构先读懂 size 和 bndboxVOC 格式的核心是每个图片对应一个 XML 文件文件里记录了图片名、尺寸和每个目标的类别及边界框。下面这段是我从一套典型数据里精简出来的结构annotation folderJPEGImages/folder filenameweld_0001.jpg/filename size width1280/width height800/height depth3/depth /size object nameporosity/name difficult0/difficult bndbox xmin102/xmin ymin335/ymin xmax138/xmax ymax372/ymax /bndbox /object /annotation解析 XML 时要抓住几个关键点。size 里的 width 和 height 必须和对应 jpg 的真实尺寸一致这是后面转 YOLO 归一化坐标的唯一依据。bndbox 里的四个值是目标框在像素坐标系下的绝对坐标xmin/ymin 是左上角xmax/ymax 是右下角框的宽度就是 xmax 减 xmin高度是 ymax 减 ymin。difficult 字段表示该目标是否难识别训练框架默认会过滤掉 difficult1 的样本转格式时要不要保留得自己想清楚我一般选择直接丢弃因为难样本留着会在训练初期干扰收敛。还有一个常见坑同一张图可能有多个 object 节点也可能一个 object 都没有。空 XML 对应的图片是背景图这类样本在缺陷检测里很值钱能有效压低误检率所以转 YOLO 格式时不要因为一个标签文件为空就跳过它应该生成一个空的 txt 文件后续训练时它照样参与负样本学习。2.3 转 YOLO 格式一个脚本说清坐标归一化YOLO 格式的标注比 XML 简洁得多每行一个目标五个数字分别是类别 id、归一化中心点 x、归一化中心点 y、归一化宽、归一化高。这个转换是最容易出错的一步我把常用脚本直接贴出来import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, img_w_refNone, img_h_refNone): tree ET.parse(xml_path) root tree.getroot() # 优先取 XML 里的 size 作为图片尺寸 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(f未知类别 {name}文件: {xml_path}) continue cls_id class_names.index(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) # 对越界框做裁剪防止出现坐标大于 1 的标注 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) w xmax - xmin h ymax - ymin if w 0 or h 0: continue # 归一化时除以图片宽和高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w w / img_w box_h h / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) txt_path os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt) with open(txt_path, w) as f: f.write(\n.join(lines)) # class_names 的顺序必须和后续训练配置里的 names 完全一致 class_names [porosity, undercut, overlap, crack, incomplete_penetration, slag_inclusion, spatter, sag, misalignment, burn_through]这段代码的转换逻辑很直接先读 XML 的 size 拿到图片宽高再逐目标取像素坐标计算中心点后各除以图片宽高。归一化的结果必然在 0 到 1 之间因为框本身被裁剪过。要注意这里我没有直接用 OpenCV 读图来拿尺寸而是信任 XML 里的 size速度会快很多但代价是如果 XML 的尺寸和真实图片不一致转出来所有框都会偏移。更稳的做法是先跑一遍第 3 章里的校验脚本把图片真实尺寸和 XML 尺寸做一次全量比对再转。参数上值得关注的只有两点round 位置和类别过滤。坐标保留 6 位小数足够YOLO 训练时用的是 float32位数再多没意义。类别不在 class_names 里的目标会被跳过这一行会打印警告如果你发现某些 XML 转换后行数比 object 数少多半是类别名有出入优先回头查这个。3. 从标注到训练数据划分、标签校验与 YOLO 训练命令3.1 分层划分训练集/验证集按类别数而不是按文件序号直接按文件名顺序砍一刀做训练集、验证集在缺陷检测里是大忌。焊接缺陷长在焊缝的不同区段可能前 100 张图全是焊缝 A 区几乎都是气孔后 100 张全是焊缝 B 区全是咬边。顺序划分会让某个类别整个从训练集消失验证集里却泛滥。我通常按“样本中最稀有的类别”做分层采样让稀有类别在训练集和验证集里都留下分布import glob import random import xml.etree.ElementTree as ET from collections import Counter random.seed(42) val_ratio 0.2 xml_files glob.glob(Annotations/*.xml) sample_labels {} global_counter Counter() # 第一遍扫描统计每个类别的全局出现次数 for xml_path in xml_files: root ET.parse(xml_path).getroot() labels [obj.find(name).text for obj in root.findall(object)] sample_labels[xml_path] labels global_counter.update(labels) buckets {} # 第二遍按样本中最稀有类别分桶 for xml_path, labels in sample_labels.items(): if not labels: continue rarest min(labels, keylambda name: global_counter[name]) buckets.setdefault(rarest, []).append(xml_path) val_files set() # 每个桶都抽 val_ratio 比例进验证集稀有类别不会漏 for rarest_cls, paths in buckets.items(): n max(1, round(len(paths) * val_ratio)) val_files.update(random.sample(paths, n)) train_files [p for p in xml_files if p not in val_files] def write_list(paths, out_path): with open(out_path, w) as f: for p in paths: img_path p.replace(Annotations, JPEGImages).replace(.xml, .jpg) f.write(img_path \n) write_list(train_files, train.txt) write_list(val_files, val.txt)这里我把“稀有度”定义为该类别在所有标注中出现次数最少分桶键取样本内所有类别里全局频次最低的那个。这样包含稀有缺陷的样本会被集中到稀有类别桶里再从桶里均匀抽 20% 进验证集避免“验证集里一个夹渣都没有”的尴尬。random.seed(42) 保证可复现val_ratio 我一般取 0.22292 张图分下来训练集约 1830 张验证集约 460 张对工业小样本来说是合理比例。如果数据量大到上万张可以降到 0.1但这里没必要。这个方案比完全随机划分更稳但也有边界如果某个桶只有一个样本max(1, round(...)) 会硬把这个样本抽进验证集导致训练集丢掉它。遇到这种极端情况我会改成 val_ratio0.1或者干脆人工单独挑一个代表性样本进验证集其余全给训练。3.2 标签可视化与越界检查训练前最后一道闸很多训练异常其实是标签问题不是模型问题。训练前我必跑一次标签体检脚本检查三件事标签文件是否缺失、标注字段是否齐全、归一化坐标换算回像素后是否越界或退化成点。脚本用图片真实尺寸做基准不信任 XMLimport os import cv2 import glob for img_path in glob.glob(JPEGImages/*.jpg): label_path img_path.replace(JPEGImages, labels).replace(.jpg, .txt) if not os.path.exists(label_path): print(f缺失标签: {img_path}) continue img cv2.imread(img_path) if img is None: print(f图片损坏: {img_path}) continue h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f字段数量不为5: {label_path} | {line}) continue x_c, y_c, bw, bh map(float, parts[1:]) # 归一化坐标还原成像素坐标检查边界 xmin (x_c - bw / 2) * w ymin (y_c - bh / 2) * h xmax (x_c bw / 2) * w ymax (y_c bh / 2) * h if bw 0 or bh 0: print(f退化框: {label_path} | {line}) if xmin 0 or ymin 0 or xmax w or ymax h: print(f越界框: {label_path} | {line})越界框是最隐蔽的问题。有的标注工具在目标贴近边缘时会把框拉出图像边界转成 YOLO 归一化坐标后数值大于 1 或小于 0训练框架通常会做 clamp但 clamp 之后框的中心点偏了损失计算也跟着偏。退化框则是宽或高为 0 的框可能是 XML 里 xmin 和 xmax 写反了也可能是两值相等这种框训练时会报 negative loss 或者干脆跳过。跑完这个脚本把打印出来的问题框数量统计一下如果超过总框数的 1%建议回源头修标签而不是在下游硬扛。可视化这一步也别省。我习惯随机抽 50 张图用 OpenCV 把归一化坐标转回像素框画在原图上人工扫一遍。这一步能发现自动化脚本发现不了的问题比如框比缺陷大两倍、一个缺陷被标注成两个重叠框、气孔标成了飞溅。画框代码只有十来行这里不重复贴核心思路就是用 rectangle 函数在原图上画框再叠加类别名文本看一眼就知道标签质量在什么水平。3.3 最小训练命令与关键参数s 模型、640 分辨率、epoch 怎么定标签确认没问题后配置训练。用 YOLO 检测框架训练这套数据最小配置就三个文件train.txt、val.txt、一个描述类别的 yaml。yaml 内容如下train: ./train.txt val: ./val.txt nc: 10 names: - porosity - undercut - overlap - crack - incomplete_penetration - slag_inclusion - spatter - sag - misalignment - burn_through注意 names 的顺序和转换脚本里 class_names 的顺序必须一个字都不差YOLO 训练时按行号映射类别 id顺序错一位所有标签的类别含义就全错了。我见过最多的问题就是在数据预处理里重排过列表忘了同步这里结果训练曲线正常但推理出来的类别跟标注对不上。训练命令我用得最多的是这条yolo detect train \ dataweld.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0模型为什么选 s 而不是 n 或 mn 模型参数最少推理快但焊接缺陷里气孔这类小目标需要足够的感受野和特征表达能力n 在 640 输入下经常漏检m 精度会更高可 2292 张的小样本数据用 m 容易过拟合收敛还慢。s 是这套数据量下性价比最稳的选择。imgsz 我设 640这是精度和显存的折中点如果目标以气孔为主可以尝试提到 800小目标会多召回一些代价是训练时间几乎翻倍。batch 16 对应单张 24GB 左右显存显存小就降到 8学习率框架会自动缩放一般不用手调。epoch 100 是底线不是终点。焊接缺陷特征细模型在前 30 个 epoch 往往还在学背景纹理到 50 个 epoch 以后 mAP 才开始明显爬升。训练中我主要看验证集 mAP0.5如果它在 80 到 100 epoch 之间仍缓慢上升就延长到 150如果提前 30 个 epoch 就开始下降说明过拟合来了优先做数据增强而不是提前停。关于增强的取舍放到最后一章细说。4. 焊接缺陷训练避坑指南4 个最常见的翻车现场4.1 训练 loss 正常下降但验证 mAP 一直不动现象训练 loss 曲线很漂亮从 2 跌到 0.5但每个 epoch 结束验证集 mAP 始终在 0.2 以下徘徊甚至接近随机。原因最常见的是标注框和缺陷边缘对不齐。焊接件的缺陷边界模糊标注人员手动画框时习惯把气孔、咬边框得比真实目标大一圈模型在训练时学到的框和真实框的 IoU 天然偏低推理时预测框稍微准一点也打不过验证集里同样偏松的标注。另一个常见原因是背景负样本过多焊缝周围的飞溅和弧光被反复识别成前景模型学到的特征和真实缺陷没关系。解决先跑标签可视化随机抽 100 张图看框和目标的贴合度如果手滑框占比超过三分之一就不要急着调模型先用标注工具把所有框边界重新刷一遍。如果标注质量没问题再把验证集里的背景样本删掉一部分观察 mAP 是否跳升能迅速定位是不是背景误检在拉低指标。这套数据的标注流程要跑通第一关就是过标注质量这一关否则后面所有参数分析都是自欺欺人。4.2 气孔和裂纹的 AP 始终偏低其他类别正常现象10 个类别里焊瘤、咬边这类大目标 AP 能到 0.7 以上气孔、裂纹这种小目标 AP 只有 0.2 左右训练集里它们的框数量并不少。原因两个方向叠加。一是特征层面气孔只有几像素到十几像素宽经过 YOLO 的多次下采样到检测头所在特征层可能只剩 1 到 2 个像素特征几乎被抹平二是锚框匹配层面默认锚框尺寸明显偏大小缺陷的框和预设锚框 IoU 匹配不上正样本数量不足模型根本没学到几个小目标样本。解决最直接的是把 imgsz 从 640 提到 800 或 960让小目标在下采样前保留更多像素。其次是换更大的模型结构s 换 m感受野和特征表达能力都强一些代价是显存和训练时间。这两步不行再考虑调整锚框基于训练集标签重新计算锚框尺寸。需要注意的是气孔这类目标密集出现时可能一张图里有几十个框训练时 NMS 会压掉很多重叠框可以适当调低 NMS 阈值比如从 0.7 降到 0.5能挽回一部分小目标召回。4.3 VOC 转 YOLO 后预测框全部偏移到左上角现象转完格式、跑完训练推理结果里框的位置全都偏到图片左上角而且框的大小比实际缺陷小一圈。原因XML 里记录的 size 和 jpg 真实尺寸不一致。很多标注工具在预处理时会缩放图片但忘记同步 XML 的尺寸字段比如 XML 写的是 1280×800实际图片已经压缩成 640×400转 YOLO 时用 1280 做归一化分母训练时框架按 640 解析框的真实中心点就被压缩到左上角方向。这正是我在第 2 章反复强调用图片真实尺寸做基准的原因。解决转格式前先全量比对图片真实尺寸和 XML size把不一致的样本挑出来以图片真实尺寸为准修正 XML。修正逻辑很简单读图拿真实宽高直接用它们替代 XML 里的 width 和 height再重新执行转换脚本。这个坑只要在数据预处理阶段做一次全量校验就能彻底避免但因为它不影响训练 loss 曲线等到推理阶段才暴露时往往已经浪费了几个小时的训练时间。4.4 训练一加载数据就报 IndexError: index 10 is out of bounds现象数据加载阶段直接报错提示类别索引超出范围训练根本起不来或者起来后某些样本被直接丢弃日志里出现大量 warning。原因数据集中某个 XML 的 name 字段写了一个第 11 个类别比如“weld_0732.jpg”里标了“spatter2”或者某处把“slag_inclusion”写成了“slag”。转成 YOLO 后 class_id 超出 0~9 的范围训练框架的类别张量就装不下了。另一个隐蔽变体是 XML 里有两个类别名看着像同一个缺陷比如“crack”和“cracking”被当成两类导致某个类别的样本量被腰斩。解决跑一次全量扫描把 XML 里所有 name 枚举出来跟 yaml 的 names 做集合差。处理方式很简单把多余类别重命名并合并到标准类别或者在转换脚本里加一层映射 dict。风险最大的是人工在 Excel 里改标注文件名最容易漏掉某个小类别。这个检查做在转格式之前代价只有几秒钟排在可视化之前。4.5 数据增强一开小缺陷全部变成重影和残影现象训练前 20 个 epoch 一切正常开启随机旋转和宽高比变换增强后验证集 mAP 反而倒退可视化训练图里小缺陷的标注框出现重影有的框框在缺陷一半的位置。原因随机旋转角度较大时图像旋转后缺陷本身被旋转出原位置但标注框仍然按轴对齐边界框保留导致框里包含大量背景缺陷只占一小块。当旋转角度接近 90 度时细长形缺陷如裂纹甚至会缩成接近点的形状标注语义丢失。解决对小目标占比高的焊接缺陷数据随机旋转角度控制在正负 10 度以内或者直接关掉旋转只保留水平翻转和轻微尺度抖动。更稳的方案是不用单一旋转增强改用 mosaic 增强拼接多张图时每个目标的标签自动重算不存在旋转导致的语义漂移。如果一定要做旋转在标注层面先把框转成旋转框但这需要训练框架支持普通 YOLO 检测不吃这一套所以最好的选择就是别在旋转上较劲。5. 把 2292 张的潜力榨干增强组合、伪标签迭代与迁移微调增强不是开得越多越好要按缺陷特征挑。焊接件这套数据我的组合是 mosaic 开 1.0 权重、水平翻转开 0.5、随机亮度对比度调整开 0.2 左右旋转只开正负 10 度。mosaic 对气孔这类小目标几乎是必选项因为它把多张图拼接成一张大图相当于变相放大了目标数量模型能在一个小批次里看到更多缺陷上下文。但 mosaic 用到后期也会引入噪声真实验证集里不会有四张图拼一起的情况所以最后 20 个 epoch 我习惯把 mosaic 关掉让模型在真实分布上微调回来。迁移微调的顺序也值得讲究。先用 COCO 预训练权重直接训练全部层容易在初期就被金属纹理带偏更好的做法是分两阶段。第一阶段冻结 backbone只训练检测头让模型先把类别映射和框回归学对通常 30 个 epoch 就能让 mAP 过 0.5第二阶段解冻整个模型用较小的学习率比如 0.001 微调 50 到 80 个 epoch。这个流程对 2292 张的小样本尤其稳能避免早期梯度在预训练特征上瞎冲。数据不够时伪标签迭代是性价比最高的打法。用训练好的模型去跑同款式但没有标注的焊接图片置信度阈值设在 0.7 以上把高置信度结果转成标签人工随机抽 5% 确认一遍合入训练集再训一轮。伪标签脚本的关键是置信度阈值和类别过滤低于 0.7 的框直接扔掉宁可漏检也别把噪声带进标签for result in results: with open(label_path, w) as f: for cls_id, box in zip(result.boxes.cls, result.boxes.xywhn): conf box.conf if hasattr(box, conf) else 1.0 if float(conf) 0.7: continue x_c, y_c, w, h box.xywhn[0].tolist() f.write(f{int(cls_id)} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}\n)验证环节我习惯把 mAP0.5 放在和 mAP0.5:0.95 同等重要的位置。焊接缺陷检测落地看的是“框有没有框中、错漏多不多”mAP0.5 就能反映大部分产线需求mAP0.5:0.95 对框贴合度过于苛刻在标注本身不够精细的工业数据上它会掩盖真实进步。这也是很多开发者对着 mAP 曲线调半天参数、一上产线效果反而变差的原因。最后说一个个人的笨习惯每次模型改进跑完我都把错检、漏检的样本截图存成一个固定的坏案例文件夹下次迭代替换模型时重新跑一遍这批图确认老问题没复发再更新线上模型。工业缺陷检测里模型回归比精度提升更常见这个文件夹就是最好的后悔药。整套流程从格式校验到伪标签迭代把 2292 张图用好、用透一个能稳定跑在产线上的 10 类焊接缺陷检测模型是可以真正落地的。希望这篇文章能帮你少走几趟弯路把时间留到后面跟产线死磕。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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