简介:面向目标检测与工业质检方向的研究者和算法工程师,这份数据集聚焦X射线底片焊缝缺陷检测场景,弥补高质量标注数据不足。压缩包约39.56MB,含约2000个文件,主要包含JPEG图片、xml与txt三种类型,分别对应原始图像、VOC格式标注和YOLO格式标注,另附train/val/test划分文件与说明文档,可便捷地接入常见检测框架。数据集覆盖6类焊缝缺陷——裂纹、未熔合、未渗透、空隙、夹渣、未融化,共4766个矩形标注框;图片清晰、未经增强,标注框与类别信息完整,便于直接进行模型训练与评估。已有355人学习下载。资源包聚焦数据本身,不做模型精度保证,适合用作焊缝缺陷检测算法研究、验证集构建及对比实验的数据基础。
1. X射线底片焊缝缺陷检测数据集:2647张图能做什么、够不够用
焊接质检老师傅靠肉眼在X射线底片上逐张评片,要看气孔、夹渣、未熔合、未焊透、裂纹、咬边六类缺陷,一天几百张看下来眼睛发花,标准也难统一。这个目标检测数据集把2647张X射线底片图像连同缺陷标注打包在zip里,正好用来训练自动评片模型,把老师傅的经验变成可批量复用的算法。2647张的量级在目标检测里不算大,但对X射线底片这类获取成本高的工业数据,足够验证YOLO类模型的检测路线,也能积累一套针对灰度底片的预处理和调参方法。适合刚上手目标检测、想拿真实工业数据练手的同学,也适合做无损检测自动评定的工程人员。
2. 拿到zip先做数据体检:类别分布、标注格式与底片特点决定后面一切
很多人拿到zip直接解压开就开始训练,训到一半发现类别名对不上、框位置错位,甚至同一张图既在训练集又在验证集。X射线底片数据集尤其要小心,因为同一焊缝往往被切成多张底片,各张底片的成像条件不同,标注习惯也未必统一。所以我拿到后第一件事不是写训练脚本,而是先把数据和标注体检一遍。
2.1 六类焊缝缺陷先认全:标签名与X射线底片上的形态
先对齐领域常识。焊缝X射线底片评片按无损检测的常见习惯,缺陷类型分六种:
- 气孔(porosity):底片上呈圆形或椭圆形暗斑,中心黑度大、边缘清晰,有时单个出现,有时密集成群。
- 夹渣(slag inclusion):形状不规则的暗色块,边界不如气孔圆润,可以单独出现也可以连成链状。
- 未熔合(lack of fusion):母材与焊缝金属未熔合,底片上多为直线或长条状暗线,常沿坡口边缘分布。
- 未焊透(incomplete penetration):焊缝根部未焊透,呈连续或断续的直线暗带,位置基本固定在焊缝中心根部。
- 裂纹(crack):细长曲折的暗线,端部尖锐,是六类里最危险的缺陷,也最容易漏检。
- 咬边(undercut):底片上沿焊缝边缘出现的条状暗区,和背景有明确分界。
打开数据集后,先找到类别名配置文件,无论是data.yaml里的names还是标注工具的classes.txt,都对着这六类过一遍名字映射。常见命名可能是英文全称,也可能是porosity、slag这类缩写或中文拼音。如果names里冒出一个other或defect,多半是标注时把不好归类的框收进了杂项,训练前要想清楚这类要不要保留。保留会让模型把这些框当成独立类别来学,不保留则要在转换脚本里跳过。
2.2 解压与结构判断:先看目录树,再判断标注是VOC还是COCO还是YOLO
拿到zip先别急着在图形界面里双击解压,直接在终端里解出来看结构:
unzip 目标检测6类X射线底片焊缝缺陷检测数据集2647张.zip -d weld_defect cd weld_defect find . -maxdepth 2 -type d这样做能看到顶层目录布局。如果出现JPEGImages和Annotations两个目录,说明是VOC格式,标注是xml;出现annotations目录且里面是单个json文件,说明是COCO格式;出现images和labels两个平级目录,且labels下每个图像同名txt,说明已经直接是YOLO格式。无论用LabelImg存VOC、Labelme转COCO还是其他工具导出YOLO格式,目录结构能对上,后续转换思路都一样。
图像是jpg还是png还是tif也很关键。TIF格式的底片图像往往尺寸很大,动辄几千乘几千像素,直接整图训练后面一定会遇到显存问题,需要切块训练。再用文件数量对账,防止解压漏文件:
find . -type f \( -name "*.jpg" -o -name "*.png" -o -name "*.tif" \) | wc -l find . -name "*.xml" | wc -l图像数量应该接近2647,标注文件数量应该等于图像数量。对不上时先查是不是有的底片图没有对应标注,这在工业数据集里常见——某些底片确实没有缺陷,标注时直接跳过了。但目标检测训练要求每张图都有标注文件,要么补空标注,要么把这类图从训练集剔除,不要硬留。
2.3 用Python做数据体检:类别频次、框面积分布和灰度统计
格式和数量确认后,写个小脚本做三类统计。这决定了后续要不要加权、要不要切块、要不要做灰度归一化:
import os import numpy as np import xml.etree.ElementTree as ET # VOC格式示例;COCO/YOLO换相应解析器,统计口径相同 xml_dir = "Annotations" img_dir = "JPEGImages" cls_count = {} box_areas = [] img_gray_mean = [] for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() for obj in root.iter("object"): name = obj.findtext("name").strip() bnd = obj.find("bndbox") x1 = float(bnd.findtext("xmin")); y1 = float(bnd.findtext("ymin")) x2 = float(bnd.findtext("xmax")); y2 = float(bnd.findtext("ymax")) cls_count[name] = cls_count.get(name, 0) + 1 box_areas.append((x2 - x1) * (y2 - y1)) # 顺便统计灰度均值,抽样前200张即可 img_name = root.findtext("filename") img_path = os.path.join(img_dir, img_name) if img_name and os.path.exists(img_path) and len(img_gray_mean) < 200: import cv2 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img_gray_mean.append(img.mean()) print("类别框数量分布:", cls_count) print("框面积百分位 p10/p50/p90:", np.percentile(box_areas, [10, 50, 90]).astype(int)) print("抽样图像灰度均值:", np.mean(img_gray_mean).round(1))逻辑说明:脚本遍历VOC xml,逐框累计每个类别的框数量,统计框面积分位数,并抽样200张图像算灰度均值。三个输出分别对应三个决策点。
参数说明:类别分布如果出现极端不平衡,比如气孔三千多框而裂纹只有几十框,训练时必须设置类别权重或对少样本做复制增强;框面积p90如果仍然很小,说明整个数据集偏小目标,训练时的imgsz要加大或采用切块训练;灰度均值如果波动大,预处理时要做按图的标准化,而不是固定除以255。
3. 把标注转成YOLO格式并跑通训练:从XML/JSON到yolov8落地
体检完就可以动手了。常见做法是以YOLOv8为主线训练,它在中小规模数据集上收敛快,预训练权重好找,一条命令行就能完成训练和评估。下面按数据划分、格式转换、训练参数三步走。
3.1 按底片编号划分数据:防止同一焊缝的切片串到验证集
X射线底片数据集的隐藏陷阱在数据划分。一张环形焊缝可能拍多条底片,每条底片的图像之间存在连续过渡;如果把图像随机分到train和val,模型在验证集上看到的可能是训练集里同一焊缝的相邻段,表现好的原因只是记住了上下文,下现场就露馅。所以划分要按底片编号分组。
import os, random from collections import defaultdict img_files = [f for f in os.listdir("images") if f.lower().endswith((".jpg", ".png", ".tif"))] # 假设文件名像 weld001_p1.jpg,weld001是底片编号 groups = defaultdict(list) for f in img_files: prefix = f.split("_")[0] groups[prefix].append(f) keys = list(groups.keys()) random.seed(42) random.shuffle(keys) n = len(keys) train_keys = set(keys[:int(n*0.8)]) val_keys = set(keys[int(n*0.8):int(n*0.9)]) test_keys = set(keys[int(n*0.9):]) with open("train.txt", "w") as f: for k in train_keys: for img in groups[k]: f.write(os.path.abspath(os.path.join("images", img)) + "\n") # val.txt / test.txt 同理逻辑说明:分组键是底片编号而不是单张图,确保同一底片的所有切片进同一个集合。随机种子固定是方便别人复现你的划分,否则每次运行结果都不同,训练对比就没有意义。
参数说明:8:1:1是目标检测常见比例,类别极不平衡或底片数量少时可以改成7:2:1,把验证集留足。划分完后用wc -l train.txt val.txt test.txt核对总数,应该和图像总量一致,多一行少一行都要回头查。
3.2 编写XML转YOLO的转换脚本:坐标归一化与类别映射
YOLO训练需要的是每个标注框归一化的中心点坐标和宽高,VOC的xml里存的是像素角点,格式要对齐:
import os import xml.etree.ElementTree as ET CLASSES = ["porosity", "slag", "lack_of_fusion", "incomplete_penetration", "crack", "undercut"] # 这个顺序必须和data.yaml里names的顺序完全一致 def voc_to_yolo(xml_path, out_dir, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in CLASSES: print(f"跳过未知类别: {name} in {xml_path}") continue cls_id = CLASSES.index(name) bnd = obj.find("bndbox") x1 = float(bnd.findtext("xmin")); y1 = float(bnd.findtext("ymin")) x2 = float(bnd.findtext("xmax")); y2 = float(bnd.findtext("ymax")) x1 = max(0, x1); y1 = max(0, y1) x2 = min(img_w, x2); y2 = min(img_h, y2) if x2 - x1 < 1 or y2 - y1 < 1: continue cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {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))逻辑说明:把类名先映射成索引,再将左上角右下角坐标转换为归一化的中心点和宽高。两个保护细节是关键:坐标裁剪处理的是底片数字化时常见的标注越界;过滤小框处理的是误标注,这两类脏数据不清理,训练时会出现难解释的NaN或loss突刺。
参数说明:如果数据集是COCO格式,改法也简单——用pycocotools把ann中的bbox读出来,bbox是[x, y, w, h],同样除图像宽高归一化后写txt,类别映射改为从JSON的categories数组里取顺序,其余逻辑不变。若是原本就带YOLO txt,反而要检查txt是否干净,尤其是坐标值有没有跑到0到1区间之外。
3.3 用YOLOv8跑通训练:预训练权重和几个关键参数
转换完成后把文件整理成YOLO期望的目录结构:
weld_defect/ images/train/ images/val/ images/test/ labels/train/ labels/val/ labels/test/再写data.yaml。path指向数据集根目录,train和val填相对路径,names按CLASSES逐字对应:
path: /home/user/weld_defect train: images/train val: images/val nc: 6 names: ["porosity", "slag", "lack_of_fusion", "incomplete_penetration", "crack", "undercut"]训练命令:
yolo detect train \ model=yolov8n.pt \ data=weld_defect.yaml \ imgsz=1280 \ epochs=100 \ batch=16 \ patience=20 \ device=0 \ project=run_weld \ name=baseline参数说明逐个说:
- model=yolov8n.pt:指定COCO预训练权重做迁移学习。焊缝缺陷和COCO里物体形态差异大,但预训练骨干网络提取的边缘、纹理能力对灰度底片同样有效,收敛速度远快于随机初始化。
- imgsz=1280:必须上调。X射线底片分辨率高且缺陷是小目标,用默认640会把很多小缺陷压到几个像素,直接漏检。显存不够时先减batch,不动imgsz。
- batch=16:单卡24G跑yolov8n可以接受;显存小就减到8或4,欠拟合概率远低于imgsz不够导致的漏检。
- patience=20:连续20个epoch验证集mAP不升就早停,2647张图通常40到60轮就收敛,不需要把100轮跑满。
训练结束后看run_weld/baseline/weights下生成的best.pt和last.pt,best按验证集mAP挑选,后面推理默认用best.pt。
4. 训练后怎么评估:mAP、混淆矩阵与缺陷漏检回放
训练完不要只看loss曲线收工。X射线底片缺陷检测的目标是漏检率,精度数字好看不代表现场可用,所以评估要多走三步。
4.1 先查数据泄漏再谈指标:哈希查重加results曲线
评估之前先做一次终检。虽然划分时按底片编号做了分组,但有些数据集里会出现重复图像或切块重叠,用md5把三套集合比对一次最稳:
import hashlib, os from collections import defaultdict def md5(path): h = hashlib.md5() with open(path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): h.update(chunk) return h.hexdigest() hash_map = defaultdict(list) for root, _, files in os.walk("weld_defect"): for fn in files: if fn.lower().endswith((".jpg", ".png", ".tif")): p = os.path.join(root, fn) hash_map[md5(p)].append(p) dups = {k: v for k, v in hash_map.items() if len(v) > 1} for k, v in dups.items(): print("重复图像:", v)逻辑说明:md5相同的文件如果出现在不同集合,说明划分有泄漏,后面所有指标都不可信。输出为空再往下读指标。
指标建议读yolo训练目录里的results.csv,而不是只看控制台打印的最后一屏。用pandas画曲线更清楚:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("run_weld/baseline/results.csv") plt.plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") plt.plot(df["epoch"], df["metrics/mAP50-95(B)"], label="mAP50-95") plt.plot(df["epoch"], df["metrics/recall(B)"], label="recall") plt.plot(df["epoch"], df["metrics/precision(B)"], label="precision") plt.legend() plt.savefig("weld_curve.png")X射线底片缺陷数据正常的参考区间:mAP50在0.6到0.85,mAP50-95在0.3到0.5。如果mAP50有0.8而mAP50-95只有0.2,说明框边界预测不稳,能套住缺陷但套不准。这种情况先查标注框是不是画得过松,再考虑加大imgsz。
4.2 混淆矩阵比整体mAP重要:相似形态缺陷间的误判要单独看
焊缝缺陷里,裂纹和未熔合在底片上都是线性暗区,夹渣和气孔都是圆形暗斑,模型很容易在相似形态之间犯错。训练完打开val批次的混淆矩阵图,重点看对角线外的高值区域。如果crack被大量识别成lack_of_fusion,先别急着改模型,回到数据集里对比这两类缺陷的底片裁图,确认标注阶段对边界形态的判断是否统一。工业上这两类缺陷的返修策略完全不同,混淆代价很高,所以要看每类各自的precision和recall,不能只看总体mAP。
这里我通常把每个类别单独输出一列指标,方法是对每类构造一个只含正反例的评估子集,用YOLO的val接口按类别统计。如果裂纹类recall低于0.9,这轮训练不能算通过,需要回到数据增强或阈值策略上继续调。
4.3 用预测回放定位漏检:置信度阈值和切片推理要配套
指标之外还有一个直观手段:把验证集和测试集的预测画出来,按置信度从低到高逐个翻图。重点看置信度0.2到0.5之间被滤掉的框,里面有多少是真缺陷,这是漏检的主要来源。推理时建议先降阈值试一轮:
yolo detect predict \ model=run_weld/baseline/weights/best.pt \ source=test_images \ conf=0.15 \ imgsz=1280 \ save=True参数说明:conf从默认0.25降到0.15,代价是假阳性变多。在评片场景,多标一个气孔让师傅复核的成本,远低于漏掉一个裂纹的代价,所以阈值可以向recall倾斜。调整后要对测试集重算一遍每类recall,目标参考:裂纹和未熔合单类recall到0.95以上,气孔可以稍微宽松,用后续人工复核兜底。
5. 常见问题与避坑:X射线底片缺陷检测训练的五个大坑
同一份2647张的数据,不同人跑出来mAP能差20个点,差别大多不在模型结构,而在数据处理和参数取舍。下面五条是我在这个方向上反复遇到的坑,按现象、原因、解决的顺序说。
5.1 一放大imgsz就爆显存:不是显存不够,是整图输入的策略不对
现象:imgsz从640调到1280后,训练直接OOM,batch减到2还在报错。
原因:X射线底片数字化后多为大尺寸灰度图,整图resize到1280平方,GPU显存消耗和CPU预处理压力同时上涨,模型根本装不下。
解决:不要整图硬塞。常见做法是先把底片按滑窗切成若干块,每块约800x800到1280x1280,再以块为单位训练。切块要有重叠,步长取窗宽一半即可,避免一个缺陷正好被切成两半。切出来的块在划分时保持同一底片编号进同一集合,否则又会引入数据泄漏。
5.2 训练loss正常但验证集mAP异常低:names顺序张冠李戴
现象:loss正常下降,验证集mAP却一直贴着地走,画出来的预测框类别全乱。
原因:data.yaml里的names顺序和转换脚本里的CLASSES不一致。转换脚本里气孔是索引0,yaml里却把裂纹排在了第0位,模型学习的标签映射是错位的,指标自然崩。
解决:把CLASSES定义成唯一来源,data.yaml由它生成,不要手敲两遍。训练前抽查几个txt标注的第一列:用head -3 labels/val/某图.txt,对照yaml names数组逐行数,第一行第一列是0就对应names[0],对不上马上改。
5.3 气孔类recall低得离谱:小目标缺陷靠整图训练必漏
现象:总体mAP还不错,单独看气孔recall只有0.4,大量几毫米的针孔都没框出来。
原因:气孔在3000x3000的底片上可能只占20x20像素,直接resize到1280后只剩10个像素不到,特征在缩放中丢光。这不是模型不努力,是预处理把目标抹掉了。
解决:切块训练是正路,推理端也要配套。推理时用原图分辨率滑窗,把所有切片上的预测框映射回整图坐标,再做一次跨窗NMS合并。跨窗NMS的IOU阈值取0.3而不是默认0.5,因为相邻切片会重复预测同一个缺陷,IOU偏高,阈值取太大反而会把正确框合并删除。
5.4 底片噪声被识别成缺陷:灰度不均与胶片颗粒的干扰
现象:验证集误检集中在底片暗角、灰度跳变区,框住的内容看起来像一片糊而不是明确缺陷。
原因:X射线底片有胶片粒度噪声和曝光不均,模型把噪声纹理学成了特征,灰度底片上这种误检特别隐蔽。
解决:在预处理阶段做灰度归一化和背景扣除。常见做法是先把整图灰度按均值方差标准化,再用形态学顶帽滤掉大范围亮度不均,让局部暗区缺陷凸显出来:
import cv2 img = cv2.imread("weld.tif", cv2.IMREAD_GRAYSCALE) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31, 31)) tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel) tophat = cv2.normalize(tophat, None, 0, 255, cv2.NORM_MINMAX)逻辑说明:顶帽操作是原图减去形态学开运算的结果,能把大范围的亮度和背景变化滤掉,只保留局部小尺度暗区,也就是缺陷候选区域。核大小31在几千像素的底片上是经验起点,底片分辨率更高时可以适当加大。预处理后重新训练,误检会显著下降。
5.5 换一个底片厂的数据mAP直接崩:不同射线机灰度分布差异太大
现象:训练集mAP50有0.85,换到另一台射线机或另一批底片,直接掉到0.3。
原因:不同射线机、不同胶片冲洗条件下,底片的灰度直方图差异非常大。模型学到的灰度质感只适配训练来源,换设备后输入分布变了,检测能力随之塌陷,这也是所谓“训练集效果好、现场翻车”最常见的来源。
解决:在预处理里加入随机gamma、对比度、灰度偏移作为数据增强,增加模型对灰度波动的耐受。更实用的做法是给每个新底片来源保留20张左右底片,部署前做一次小样本微调,通常几十轮就能拉回来,比重新收集标注数据便宜得多。
6. 让模型真正参与评片:整图切片推理与部署前灰度检查
训练完成不等于可以交付。现场输入往往是一整张数字化底片,不是训练时的切片,所以推理端要和训练端保持一致:滑窗推理、坐标映射回原图、跨窗框合并。我习惯把它固化成脚本,同时把前面的验证项整理成清单,避免换个环境复现又回到“离线mAP高、上线就翻车”的状态。
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("run_weld/baseline/weights/best.pt") img = cv2.imread("field_imgs/weld_03.tif", cv2.IMREAD_GRAYSCALE) h, w = img.shape[:2] sw = sh = 1024 # 滑窗大小 stride = 512 # 步长取窗宽一半,缺陷跨窗也能被完整覆盖 boxes = [] for y in range(0, h - sh + 1, stride): for x in range(0, w - sw + 1, stride): patch = img[y:y+sh, x:x+sw] patch = cv2.cvtColor(patch, cv2.COLOR_GRAY2BGR) res = model.predict(patch, conf=0.15, imgsz=1024, verbose=False)[0] for box in res.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() boxes.append([x + x1, y + y1, x + x2, y + y2, float(box.conf), int(box.cls)]) boxes = np.array(boxes) keep = cv2.dnn.NMSBoxes( boxes[:, :4].tolist(), [float(b) for b in boxes[:, 4]], score_threshold=0.0, nms_threshold=0.3 )这段代码展开来说:外层循环负责在整张底片上滑窗,内层把每个切片喂给模型,返回的框坐标加上切片原点偏移后回到整图坐标系;最后用NMS合并跨窗产生的重复框,阈值0.3配合0.5步长,能保留同一缺陷在不同切片里的最高置信度框。
部署前还有一个常被我忽略的灰度检查:推理前统计整图灰度均值,如果和训练集均值相差超过两倍标准差,直接输出“低置信度,请人工复核”,而不是把一堆不可信框交给现场。这个习惯是我拿底片做误检回放时养成的——有一批新底片灰度整体偏暗,模型把底片边缘全部识别成夹渣,加了灰度检查后才挡住。做缺陷检测,模型置信度只代表“这里像缺陷”,不代表现场一定可信,守住这个边界,才能在质检场景里长期用下去。希望帮到你。
本文还有配套的精品资源,点击获取