
简介面向工业焊接质量检测与计算机视觉目标检测研究者这份X射线底片焊缝缺陷检测数据集包含2647张清晰JPG图片并提供与之一一对应的VOCXML和YOLOTXT双格式标注。标注覆盖6类常见缺陷共4766个矩形框其中裂纹1176框、未熔合629框、未渗透562框、孔隙1519框、夹渣704框、未融化176框类别定义清楚适合用于训练YOLO、Faster R-CNN等常见目标检测模型或作为焊缝缺陷识别课题的初始数据。压缩包大小约39.56MB文件总数为2000个以XML标注文件为主1995个另有TXT格式的划分与说明文件JPEGImages、Annotations、labels三个目录分别存放图片、XML标注和TXT标注结构规整便于按需读取与划分数据集。数据本身仅保证标注准确合理不附带模型权重或精度承诺使用前需自行评估。目前已有359人学习对于需要开展焊缝缺陷检测实验的研究者来说是一份可快速上手的开源式数据集。1. X射线底片焊缝缺陷数据集6 类缺陷、4766 个标注框先搞懂再动手拿到一批 X 射线底片焊缝缺陷数据最头疼的往往不是模型选型而是标注够不够干净、格式能不能直接喂给训练脚本。这份「目标检测6类X射线底片焊缝缺陷检测数据集2647张.zip」解决的就是这个前置问题2647 张 JPG 底片每张都配了 VOC 的 XML 和 YOLO 的 TXT6 个缺陷类别加起来共有 4766 个矩形标注框图片清晰且未做增强属于那种「解压后先跑一遍统计脚本就可以直接进入训练」的干净资源。适合做焊接质检自动化预研、缺陷检测算法对比实验、以及给刚入门目标检测的团队当练习数据。下面几章我会按「摸清目录结构 → 解析标注格式 → 规避数据本身的坑 → 落地训练参数 → 调优小类别」的顺序把这包数据彻底拆开。2. 解包后的三个文件夹JPEGImages、Annotations、labels 是怎么对齐的这一章先把压缩包的内脏翻出来看。搞清楚「图片、XML、TXT 三个目录怎么对应」是后续所有操作的地基地基歪了后面训练出来的模型大概率是废的。2.1 目录结构与命名规则文件名前缀暴露了类别线索解压后先用一条命令把目录树打出来确认三个目录是否完整。压缩包内的说明文件只写了「3 个文件夹分别存储图片、xml、txt 文件」但实际使用时我发现JPEGImages 里是 2647 张 jpgAnnotations 里是 2647 个 xmllabels 里是 2647 个 txt数量完全一致是一一对应的。unzip -q 目标检测6类X射线底片焊缝缺陷检测数据集2647张.zip -d weld_defect_data/ cd weld_defect_data find . -maxdepth 2 -type d | sort find JPEGImages -name *.jpg | wc -l find Annotations -name *.xml | wc -l find labels -name *.txt | wc -l逻辑说明先解压到指定目录再分别统计三个子目录的文件数。如果三个数字不相等说明压缩包在传输或解压过程中丢了文件这种损坏在网盘下载的资源里并不少见我一般会在训练前强制走一遍这个统计。参数说明-q表示安静解压避免刷屏-d指定解压目标目录。文件总数以实际解压结果为准正常情况下三个目录都应该是 2647。再从压缩包公开的文件名来看Annotations 里出现了 c_0.xml、c_23.xml、c_25.xml、c_24.xml、c_112.xml 这种「类别字母_编号」的命名模式。前缀字母大概率对应了该图的主要缺陷类别至少说明这份数据集在整理时是按缺陷类别分组的而不是随意乱拍的流水号。labels 目录里的 txt 文件名应该与 jpg、xml 保持同名这正是 VOC 和 YOLO 两种格式能对齐的前提。如果你解压后发现某张图对应不上标注先检查文件名后缀是否一致这是我踩过最多的一类低级坑。2.2 VOC 和 YOLO 两种标注的对应关系同一个框两种存法同一批矩形框VOC 的 XML 里存的是像素坐标系下的 xmin、ymin、xmax、ymax而 YOLO 的 TXT 里存的是归一化后的类别 id 和中心点坐标加宽高。两种格式各有用武之地XML 适合人工阅读和调试TXT 则能直接被主流检测框架读取省去在线转换的耗时。语义VOC XML 字段YOLO TXT 字段类别namec/name第一列如 0 表示 c 类左上角 xxmin32/xmin无由中心点与宽高换算左上角 yymin45/ymin无右下角 xxmax120/xmax无右下角 yymax98/ymax无归一化中心 x无第二列约 0.11归一化中心 y无第三列约 0.15归一化宽无第四列约 0.14归一化高无第五列约 0.09换算公式也很固定x_center (xmin xmax) / 2 / 图片宽width (xmax - xmin) / 图片宽y 方向同理。注意 YOLO 的 TXT 里所有数值都在 0 到 1 之间这是很多人第一次转换时最容易算错的地方——把归一化值当成像素值直接用画框时框会飞出去。后续第三章我会给一段双向校验脚本专门治这种问题。2.3 用脚本核对三目录完整性先跑一遍再谈训练拿到数据集先别急着训练花五分钟跑一遍对齐脚本能省下后面几小时的排查时间。我一般会检查三件事文件名是否一一对应、TXT 里每一行的类别 id 是否都在 0 到 5 之间、TXT 里的归一化坐标是否都在 0 到 1 区间内。import os from pathlib import Path base Path(weld_defect_data) jpg_dir base / JPEGImages ann_dir base / Annotations lbl_dir base / labels jpgs {p.stem for p in jpg_dir.glob(*.jpg)} xmls {p.stem for p in ann_dir.glob(*.xml)} txts {p.stem for p in lbl_dir.glob(*.txt)} print(jpg 数量:, len(jpgs)) print(xml 数量:, len(xmls)) print(txt 数量:, len(txts)) print(只出现在jpg:, jpgs - xmls - txts) print(只出现在xml:, xmls - jpgs - txts) print(只出现在txt:, txts - jpgs - xmls) bad_lines [] for txt in lbl_dir.glob(*.txt): for line in txt.read_text(encodingutf-8).splitlines(): parts line.strip().split() if len(parts) ! 5: bad_lines.append((txt.name, line)) continue cls_id, cx, cy, w, h parts if not (0 int(cls_id) 5): bad_lines.append((txt.name, cls out of range)) if not all(0.0 float(v) 1.0 for v in (cx, cy, w, h)): bad_lines.append((txt.name, coord out of range)) print(非法标注行数:, len(bad_lines))逻辑说明先用集合差找出三个目录里名字对不上的文件再遍历每个 TXT检查每行是否满足「5 列、类别 id 在 0~5、坐标在 0~1」三个约束。这段代码输出的非法行列表就是后续排错的直接索引。参数说明glob(*.txt)只匹配当前目录下的 txt 文件如果你的数据集嵌套在子目录里要改用rglob。类别 id 上限 5 对应摘要里标签名称数组的索引写死成 6 是因为类别数是 6换数据集时这里需要跟着改。3. XML 与 TXT 解析实战把标注读到能训练的程度上一章解决了「文件是否齐全」的问题这一章解决「标注内容怎么读」的问题。目标是把 XML 和 TXT 的每一列、每一个字段都转化成你能直接用在训练和调试里的信息。3.1 XML 里存了什么从 filename 到 bndbox 逐字段拆VOC 格式的 XML 结构很固定以 c_0.xml 为例子核心内容长这样annotation folderJPEGImages/folder filenamec_0.jpg/filename size width1200/width height800/height depth3/depth /size object namec/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin32/xmin ymin45/ymin xmax120/xmax ymax98/ymax /bndbox /object object namec/name bndbox xmin210/xmin ymin180/ymin xmax330/xmax ymax240/ymax /bndbox /object /annotation逻辑说明size节点记录图片真实宽高这是把归一化坐标还原成像素坐标的唯一依据。每个object节点对应一个标注框一份 XML 里可以有多个 object因为一张底片上可能同时存在多处缺陷。这份数据总框数 4766、总图片数 2647平均每张图约 1.8 个框说明多目标样本占比不小训练时要注意 batch 内正样本的覆盖。参数说明difficult为 1 的框通常表示难以辨认、建议训练时忽略的样本。摘要里说「标注合理」实际训练时如果发现某个类别的负样本噪声偏大可以回这里检查 difficult 标记而不是直接开骂标注差。3.2 归一化坐标换算txt 四行数字怎么还原成像素框YOLO 格式的 txt 每一行代表一个框格式固定为class_id x_center y_center width height。下面这段代码把同一张图的 XML 和 TXT 同时读进来用bndbox和归一化值互相验证坐标是否一致不一致立刻报警import xml.etree.ElementTree as ET from pathlib import Path ann_dir Path(weld_defect_data/Annotations) lbl_dir Path(weld_defect_data/labels) def parse_xml(xml_path): root ET.parse(xml_path).getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) boxes [] for obj in root.iter(object): name obj.find(name).text bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) boxes.append((name, xmin, ymin, xmax, ymax)) return width, height, boxes def parse_txt(txt_path): boxes [] for line in txt_path.read_text().strip().splitlines(): parts line.strip().split() cls_id, cx, cy, w, h parts boxes.append((int(cls_id), float(cx), float(cy), float(w), float(h))) return boxes for xml_path in sorted(ann_dir.glob(*.xml))[:50]: txt_path lbl_dir / (xml_path.stem .txt) W, H, xml_boxes parse_xml(xml_path) txt_boxes parse_txt(txt_path) if len(xml_boxes) ! len(txt_boxes): print(数量不一致:, xml_path.name) continue for (name, xmin, ymin, xmax, ymax), (cls_id, cx, cy, w, h) in zip(xml_boxes, txt_boxes): xml_cx (xmin xmax) / 2 / W xml_cy (ymin ymax) / 2 / H xml_w (xmax - xmin) / W xml_h (ymax - ymin) / H if abs(xml_cx - cx) 1e-3 or abs(xml_w - w) 1e-3: print(坐标不一致:, xml_path.name, name, cls_id) break逻辑说明先解析 XML 拿到像素坐标和图片尺寸再解析 TXT 拿到归一化坐标把 XML 的像素值按公式换算成归一化值与 TXT 逐项比对。两边框数量不一致或坐标偏差超过千分之一说明这份样本在格式转换时出了问题。参数说明root.iter(object)比findall(object)更稳健能兼容带命名空间的 XMLzip默认按文件内的框顺序对齐VOC 和 YOLO 转换工具为了保证对应关系通常会保持顺序一致。如果你在别处遇到顺序不一致的数据集这招就失效了需要加框 ID 约束。3.3 六类标签的中英文映射c/lof/lop/p/s/u 各自的判定场景这六个类别是这份数据的核心资产类别分布差异非常悬殊直接影响后面的训练策略。标签全称中文含义影像上通常长什么样cCrack裂纹细长、略带曲折的黑色线条lofLack of Fusion未熔合沿坡口或焊层边界的黑色带状影lopLack of Penetration未渗透根部细黑直线常在焊缝中心pPorosity空隙/气孔圆形或椭圆形黑点可单个可成群sSlag Inclusion夹渣不规则黑斑边缘比气孔模糊uunfused未融化与 lof 形态接近低对比度区域要注意的是lof和u的中文译名都在「未熔合/未融化」这个语义附近实际区分更多靠对比度和位置。我在调试时发现模型到后期最容易把这两个类互相猜错处理办法是别只看标签名翻出原始底片影像对照一遍搞清这份数据里两者到底按什么边界划分。类别的框数分布是c 类 1176 框、lof 类 629 框、lop 类 562 框、p 类 1519 框、s 类 704 框、u 类 176 框p 类框数是 u 类的 8.6 倍这个不平衡比例在训练时必须用策略去对抗具体方案在第四章和第五章展开。4. 避坑记录类别不平衡、同名对齐与划分文件的五个坑位这一章不写理论全是实际跑数据时遇到的翻车现场。每一条都按「现象 → 原因 → 解决」的顺序写可以直接对照排查。4.1 u 类永远不收敛验证集 AP 接近 0现象训练到一百轮左右c、p、lof 都涨到可用的水平唯独 u 类在验证集上精确率和召回率都惨不忍睹跟没学过一样。原因u 类只有 176 个框占总框数 4766 的 3.7%。默认采样策略下一个 batch 里可能连续多轮都抽不到一个 u 类样本梯度信号被其余五类淹没。再加上 u 类与 lof 在影像上本来就相似模型学到的特征大概率被 lof 类的强样本带偏。解决给损失函数加类别权重u 类权重调到其他类的 5 到 8 倍具体做法见 5.3 节同时按 u 类做样本过采样让每个 batch 里至少有一个 u 类框。我一般先从权重入手效果不够再加过采样因为过采样容易让模型在 u 类上过拟合底片数据的样本重复率太高不一定是好事。4.2 标注文件和图片文件名相同内容却对不上现象训练时 loss 掉到一定值就震荡把预测框画到原图上发现框的位置和底片上的缺陷肉眼可见地错位。原因jpg 和 txt 虽然同名但来源不是同一轮导出。常见于数据集被二次打包、或有人只替换了图片没换标注。压缩包里的说明文件没法保证每个文件的内部一致性必须自己验证。解决跑第二章 2.3 节的对齐脚本它会告诉你哪些文件存在非法行号、哪些坐标越界。出现错位样本时我的处理原则是直接删除该文件而不是手工修正——2647 张的训练规模少几张图影响不大但手工改错一个框模型就会学到一条错误线索。4.3 直接套用自带的 train.txt / val.txt验证集分布失衡现象用压缩包自带的划分训练验证集 mAP 很高换到现场底片测试时掉点明显预制件和管件上的表现差异很大。原因自带的划分文件通常只是随机切分没有按类别做分层。p 类占比 31.9%如果验证集里恰好 p 类框特别多指标自然虚高掩盖了小类上的不足。解决不信任自带划分自己按类别分层抽样重新切分。具体做法是读每个 txt 里出现过的类别集合把图片按主导类别归组每组内随机抽 15% 做验证集保证每个类别在训练和验证里都有代表。第五章会给出完整脚本。4.4 把 YOLO 的归一化坐标当像素坐标直接用现象画框或计算 IoU 时框的位置明显超出图像范围loss 出现 NaN。原因labels 里的 txt 是六列归一化值不是像素坐标。有人直接在评估代码里拿它和 XML 的像素坐标做比较单位都不一样结果必然错乱。解决所有用 TXT 的场景都要先乘上图片宽高还原成像素坐标。我在 3.2 节的脚本里已经写好了换算逻辑直接用那个函数输出像素坐标再进训练或评估流程。4.5 解析 XML 时 findall 返回空列表标注读不出来现象root.findall(object)得到空列表代码报错说找不到标注框。原因部分工具生成的 VOC 文件在annotation根节点上带了xmlns命名空间。ElementTree 在带命名空间的 XML 里默认找object这个不带前缀的标签会匹配失败表现就是空列表不报错但数据全丢。解决用root.iter(object)遍历代替findall(object)或者把 tag 里的命名空间前缀剥掉再匹配。我在 3.2 节用的是第一种能同时兼容带命名空间和不带命名空间的 XML建议直接照抄。5. 用 YOLO 格式落地训练配置、参数与去偏 tricks格式解析、数据验证、避坑都做完了这一章进入正题把这份数据跑起来。我会给出从重划分到损失权重的完整配置所有参数都按 2647 张、6 类、4766 框这个规模来设定。5.1 重划分先抛弃自带的划分文件做类别分层抽样先写一个按类别分层抽样的脚本它的输入是 labels 目录里的 txt输出是新的 train.txt、val.txtimport random from pathlib import Path from collections import defaultdict lbl_dir Path(weld_defect_data/labels) random.seed(42) groups defaultdict(list) for txt in lbl_dir.glob(*.txt): classes set() for line in txt.read_text().strip().splitlines(): classes.add(int(line.split()[0])) main_cls sorted(classes)[0] # 取该图出现的最小类别 id 作为分组依据 groups[main_cls].append(txt.stem) train_files, val_files [], [] for cls_id, stems in sorted(groups.items()): random.shuffle(stems) cut max(1, int(len(stems) * 0.15)) # 每类至少留 1 张做验证 val_files.extend(stems[:cut]) train_files.extend(stems[cut:]) Path(mytrain.txt).write_text(\n.join(train_files)) Path(myval.txt).write_text(\n.join(val_files)) print(训练集图片数:, len(train_files), 验证集图片数:, len(val_files))逻辑说明对每张图片统计它包含的类别集合按类别 id 排序后取第一个作为分组依据这样每个类别分组内的图片在语义上更接近。再对每组按 15% 比例随机切分保证小类 u 也能在验证集里出现而不是全被分到训练集。参数说明random.seed(42)固定随机种子保证每次重跑结果一致复现实验时不会被随机性干扰。cut max(1, ...)是为了避免某个类别样本太少时验证集为空的极端情况u 类 176 个框分布在若干张图上如果一张都没进验证集评估就没意义了。5.2 数据组织与训练配置目录结构决定脚本要不要改主流的检测框架通常要求数据按 images 和 labels 两个子目录组织。我一般会做软链接而不是复制图片省硬盘而且重划分时不用搬文件mkdir -p dataset/images dataset/labels for split in train val; do while read img; do ln -sf $(pwd)/JPEGImages/${img}.jpg dataset/images/${split}/${img}.jpg ln -sf $(pwd)/labels/${img}.txt dataset/labels/${split}/${img}.txt done my${split}.txt done逻辑说明把 JPEGImages 里的图片和 labels 里的标注按划分结果绑定到 dataset 目录。训练集和验证集用独立的子目录这样主流框架的 data 配置可以直接指向目录不用自己写读取逻辑。软链接的好处是重跑划分时不用等文件拷贝。参数说明-sf里的-s是软链接-f是覆盖已存在的同名链接。$(pwd)是为了生成绝对路径避免相对路径在跨目录执行时断链。数据集配置文件按这个格式写path: dataset train: images/train val: images/val names: 0: c # Crack 裂纹 1: lof # Lack of Fusion 未熔合 2: lop # Lack of Penetration 未渗透 3: p # Porosity 空隙 4: s # Slag Inclusion 夹渣 5: u # unfused 未融化逻辑说明names 的顺序不是随便写的必须与 labels 里 txt 的 class id 完全对应。摘要里标签名称数组是[c,lof,lop,p,s,u]那么 YOLO 的 id 就是 c0、lof1、lop2、p3、s4、u5这一点写错训练出来的模型所有类都是错位的。参数说明path是数据集的根目录train和val是相对路径。如果你沿用压缩包里自带的train.txt也可以把train指向那个 txt 文件但我还是建议用 5.1 节重划分的结果理由在 4.3 讲过。训练启动命令给一组我的默认值python train.py --data weld.yaml --imgsz 640 --batch 16 --epochs 300 --patience 40逻辑说明imgsz 640对底片这种长宽比不太极端的图像够用太大增加显存压力但对小缺陷的检出提升有限——焊缝缺陷里的裂纹和未熔合本质是细长条纹理640 分辨率下卷积感受野能覆盖到再往上收益不明显。batch 16是因为 2647 张图的规模不大大 batch 会让 BN 统计量不稳定。patience 40是早停耐心值底片数据的类别不平衡容易让验证指标波动耐心值太小会在小类还没收敛时就停掉。5.3 损失权重怎么给先按频率算再手工微调类别不平衡的常规解法是在损失函数里对每个类别乘一个权重。权重的基础值按逆频率算再对 u 类这种低置信度类别做一次手工放大from collections import Counter counter Counter() for txt in label_dir.glob(*.txt): for line in txt.read_text().strip().splitlines(): counter[int(line.split()[0])] 1 total sum(counter.values()) num_cls 6 base_weight {i: total / (num_cls * cnt) for i, cnt in counter.items()} base_weight[5] * 2.5 # 对 u 类做手工放大 print(base_weight)逻辑说明total / (num_cls * cnt)是典型的逆频率权重公式每个类别的权重与该类框数成反比让总体损失里每个类别的贡献接近均衡。u 类只有 176 框加权后在 loss 里的占比仍然有限所以我会额外乘 2.5 倍再试。参数说明cnt是各类别的框数这份数据里 p 类 1519、c 类 1176、s 类 704、lof 类 629、lop 类 562、u 类 176。权重不是越大越好u 类权重超过 10 倍时模型会在 u 类的少数样本上过拟合验证集的精确率掉得很快我的经验值是围绕 5~8 倍区间来回试。6. 置信度阈值与类别专属阈值从小类别里榨出可用精度的两个习惯训练结束后大多数人会直接用默认的置信度阈值 0.5 去评估或部署这是小类别发挥不出来的最常见原因。u 类样本少、与 lof 视觉接近模型输出它的置信度普遍偏低且分布偏斜统一用 0.5 阈值过滤u 类的召回率会被压到几乎没有意义。正确做法是先统计每个类别在验证集上的置信度分布再分别定阈值。# 假设 preds 是验证集预测结果格式为 (cls_id, conf, xmin, ymin, xmax, ymax) # 统计每个类别的最佳 F1 阈值替代统一阈值 from collections import defaultdict preds_by_cls defaultdict(list) for cls_id, conf, *_ in preds: preds_by_cls[cls_id].append(conf) best_thr {} for cls_id, confs in preds_by_cls.items(): best_f1, best_t 0.0, 0.5 for t in [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]: keep sum(1 for c in confs if c t) if keep 0: continue precision keep / len(confs) recall keep / max(1, gt_counts[cls_id]) f1 2 * precision * recall / max(precision recall, 1e-6) if f1 best_f1: best_f1, best_t f1, t best_thr[cls_id] best_t print(按类最佳阈值:, best_thr)逻辑说明对每个类别遍历一组候选置信度阈值用验证集上的预测框计算 F1F1 最大的阈值即为该类别的最优阈值。u 类的阈值通常会落在 0.15 到 0.25 区间而 p 类可能到 0.5 还是偏高。部署时按这个字典做后处理而不是一刀切。参数说明gt_counts是验证集里每个类别的真实框数用来算召回率这个值可以在 5.1 节的重划分脚本里顺手统计出来。内存受限时preds可以只保留cls_id和conf两列不必存完整框坐标。从那以后我每次拿到新数据集都会强制走一遍分布统计、对齐校验、分层重划分三步流程数据没理顺之前绝不开训练。底片焊缝缺陷这种小样本、强不平衡的数据集决定模型上限的往往不是网络结构而是数据准备的仔细程度和阈值后处理策略。希望这一套流程能帮你在同样类型的数据上少走几步弯路也少翻几次车。本文还有配套的精品资源点击获取