
简介一套面向X射线底片焊缝缺陷检测的目标检测数据集适用于工业无损检测、焊接质量评估与算法研究场景也适合目标检测工程师用于模型训练、调优和效果验证。压缩包约39.56MB按JPEGImages、Annotations、labels三目录组织分别存放2647张清晰底片、2647个XML标注文件和2647个TXT标签文件另附5个TXT划分清单三目录文件一一对应便于按文件名批量读取VOC与YOLO两种格式可无缝衔接主流训练流程。每张底片分辨率清晰目标边界明确矩形框标注涵盖裂纹、未熔合、未渗透、孔隙、夹渣、未融化共6类焊接缺陷总计4766个目标框其中裂纹1176个、未熔合629个、未渗透562个、孔隙1519个、夹渣704个、未融化176个各类别框数统计清晰便于分析样本均衡性。目前已有358人浏览学习作为焊缝缺陷检测的基础数据可直接用于模型训练、评估和对比实验也可作为私有基准集与公共数据集交叉验证数据集仅保证标注准确不对训练精度作承诺使用时可根据实际任务灵活处理。1. 目标检测6类X射线底片焊缝缺陷检测数据集2647张图足够把痛点打穿“目标检测6类X射线底片焊缝缺陷检测数据集2647张.zip”这个名字信息量其实比很多标着“工业缺陷数据集”的大礼包都多2647张X射线底片6类焊缝缺陷而且是目标检测格式不是分类格式。焊接质检行业最不缺的是“我知道焊缝有缺陷但想让算法替我看片子”最缺的是有标注、能训练、能互相复现对标的数据。这套数据适合三类人刚转工业视觉的算法工程师、焊管和压力容器制造企业里想上自动评片的人、拿工业小目标做迁移学习的学生。真动手会发现X射线底片的坑不在网络结构也不在算力而在对比度低、目标小、长宽比极端、类别不平衡这四件事。后面所有章节都照这条主线展开。2. 打开zip前先读懂标签6类缺陷、格式与采样分布2.1 先建文件清单2647张底片本身不等于2647个训练样本拿到这个压缩包第一步不是解压后立刻建训练集而是先做一件事把图片尺寸、通道数、标签文件三样东西全部列出来。2647张是原始底片数量但底片可能是一张几千像素宽的长条图有的数据集会直接给原图有的会切成patch再标注还有的会同一张图附带多张不同对比度的副本。如果不先摸底后面训练时图片尺寸忽大忽小collate就会出各种问题。我一般会写一个最笨的扫描脚本先把每张图的尺寸和位深打出来from pathlib import Path from PIL import Image root Path(焊缝缺陷数据集) for img_path in sorted(root.rglob(*)): if img_path.suffix.lower() in {.png, .jpg, .jpeg, .bmp, .tif, .tiff}: im Image.open(img_path) print(img_path, im.mode, im.size) if getattr(im, n_frames, 0) 1: print( 多帧TIFF注意取帧)这段代码逻辑很简单但能筛出很多隐藏问题。im.mode是L说明是8位灰度I;16是16位灰度RGB是伪彩色图。X射线底片行业内常用16位TIFF保存为的是保留高动态范围里的焊缝细节如果你发现数据里有I;16而你不处理直接送进模型绝大多数框架会把它当成异常通道或直接截断。另外如果图片尺寸差异超过一个数量级比如有 512x512 也有 4000x600就说明这批数据大概率是“原图切块”混着放的必须分开处理。2.2 YOLO标签、VOC标签与X射线6类缺陷的映射X射线焊缝缺陷检测的6类业内最常见的是气孔、夹渣、未焊透、未熔合、裂纹、咬边这六种。具体到这个压缩包里是哪六类、顺序怎么样一定要以解压后的label文件为准不能拿常识去猜。标签格式通常三种YOLO的txt、VOC的xml、COCO的json。YOLO格式每一行是“类别序号 x_center y_center width height”坐标都是归一化到0到1之间的浮点数VOC格式是一个xml文件对一张图COCO则是一个大json。大多数焊缝缺陷数据集会优先给YOLO格式因为现在训练工具链里YOLO系最主流。拿到标注后第一件事是统计每个类别的框数量和框尺寸分布不要只看图数。X射线底片里气孔、夹渣这类缺陷密集且小裂纹细长未焊透往往是大片区域这几类在尺寸和数量上天然不均衡。下面这段脚本可以用几秒钟把不平衡程度量化出来import numpy as np from pathlib import Path from collections import Counter label_dir Path(labels) stats Counter() area_list [] for txt in label_dir.glob(*.txt): for line in txt.read_text().strip().splitlines(): parts line.split() if len(parts) 5: print(f跳过坏行: {txt.name}: {line!r}) continue cls int(float(parts[0])) w float(parts[3]) h float(parts[4]) stats[cls] 1 area_list.append([cls, w * h, w, h]) print(各类别框数量:, stats) arr np.array(area_list) print(框面积中位数:, np.median(arr[:, 1])) print(框宽度中位数:, np.median(arr[:, 2])) print(框高度中位数:, np.median(arr[:, 3]))这一段的价值在于它能直接告诉你该用什么尺度训练如果框面积中位数不到整图面积的0.5%那就是标准小目标场景imgsz640很可能不够如果裂纹类的宽度中位数只有高度中位数的五分之一说明长宽比极端的框很多数据增强里的随机缩放就要保守。统计完把结果存下来之后调参时每改一次都回头对照不然你根本分不清模型变好是因为超参数还是因为换了某批数据。2.3 标签坏行和小样本类别先清理再训练X射线底片的标注不是天然干净的。射线底片对比度低标注员经常把框画到焊缝边缘的铅字码、甚至把两道相邻焊缝画进同一个框。还有一类更容易翻车的情况标注工具导出时框的坐标出现负值或超过1.0或者因为原图做了裁剪标签没有跟着裁。这样脏标签混进训练集模型会在某个类别上反复震荡。我处理这类数据有一个固定习惯先不训练先把YOLO标签里所有异常行打印出来。凡是长度不等于5的行、坐标小于0的行、宽高小于0.01的行一律单独归到一个bad_labels文件夹不直接删原文件。0.01这个阈值要注意不是随便取的如果框太小下采样后连一个像素都占不到模型根本学不到特征留下只会增加loss抖动。清理之后重新统计类别数量往往还会发现某一类只有几十个框这种情况下必须做类别重采样或者增强否则即使训练出来那一类的mAP也会低到像没训过。提示不要用全局归一化坐标的“平均值”来判断长宽比分布要看分位数。极端长条的框会被平均值平滑掉但正是这些极端框决定了裂纹类的检出难度。3. 把X射线底片转成能训练的样本目录划分、灰度增强与切patch3.1 统一目录结构给标签留好后悔药数据集解压后第一时间把原始文件复制到一个只读目录里然后在此基础上重新组织一份“干净目录”不要直接在原始目录里动手。我常用的结构是这样的dataset/ original_images/ # 原始底片只读备份 original_labels/ # 原始标注备份 images/ # 统一尺寸、统一格式后的图 labels/ # 与images一一对应的yolo标签 train.txt val.txt这样做的原因很简单训练过程中你会发现某张图本身有问题或者某个标签的类别序号标错了如果原始文件已经被预处理脚本覆盖你连后悔药都没有。train.txt和val.txt不是必须的但建议写出来因为后续用YOLO训练时指定这两个文件比指定目录省很多事。目录划分有一个细节必须按“底片”分组不能按“图片文件”随机分。很多数据集提供的是同一张大底片切好的patchpatch的文件名会带同一段底片编号。如果你直接随机把patch分成train和val同一段焊缝的信息会同时出现在训练集和验证集里mAP虚高得一塌糊涂。要按文件名里的底片ID先分组再整组划分import random from pathlib import Path from collections import defaultdict images list(Path(images).glob(*.png)) groups defaultdict(list) for p in images: # 假设底片ID是文件名前8位实际按你压缩包里的命名规则来 film_id p.stem[:8] groups[film_id].append(p) random.seed(42) all_ids list(groups.keys()) random.shuffle(all_ids) split int(len(all_ids) * 0.85) train_lines, val_lines [], [] for film_id in all_ids[:split]: for p in groups[film_id]: train_lines.append(f{p.resolve()} {p.with_suffix(.txt).name}) for film_id in all_ids[split:]: for p in groups[film_id]: val_lines.append(f{p.resolve()} {p.with_suffix(.txt).name}) Path(train.txt).write_text(\n.join(train_lines)) Path(val.txt).write_text(\n.join(val_lines))这段脚本最后生成的train.txt里每一行是“图片路径 对应标签txt文件名”。用这个方式训练能保证验证集评估结果不掺水。很多人在自然图像数据集上养成了随机划分的习惯到了工业数据上继续沿用结果现场漏检率比验证集高出一大截这就是典型的数据泄漏。3.2 16位X射线底片的灰度归一化与CLAHE增强X射线底片和普通相机照片最大的区别是动态范围。一张16位TIFF里焊缝区域的灰阶可能只集中在很小的一段范围内直接转8位会导致对比度严重不足缺陷和背景糊成一片。新手最容易踩的坑是用cv2.imread直接读TIFFOpenCV默认会把它当成8位图读16位信息直接截断。我常用的预处理流程是先用IMREAD_UNCHANGED读回原始位深做线性灰度拉伸再叠加CLAHE增强。线性拉伸把动态范围铺满CLAHE负责把局部对比度提上来import cv2 import numpy as np raw cv2.imread(film.tiff, cv2.IMREAD_UNCHANGED) if raw.dtype np.uint16: min_val, max_val raw.min(), raw.max() img8 ((raw.astype(np.float32) - min_val) / (max_val - min_val 1e-6) * 255).astype(np.uint8) else: img8 raw clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) img_clahe clahe.apply(img8) # 三通道复制方便直接用预训练模型的RGB权重 img_bgr cv2.cvtColor(img_clahe, cv2.COLOR_GRAY2BGR) cv2.imwrite(film_prep.png, img_bgr)clipLimit2.0是CLAHE的核心参数控制对比度放大幅度tileGridSize(8,8)决定局部区域尺寸。焊缝缺陷通常很小tile太大等于全局直方图均衡化tile太小会把焊缝纹路放大成伪缺陷。2.0这个值是常见起点如果预处理后图像边缘出现明显噪声颗粒就降一点如果缺陷和背景还是分不开就升到3.0。要注意灰度拉伸用最大最小值如果某张底片的边缘有极亮或极暗的屏蔽铅字拉伸会失效这时候需要先做分位数裁剪比如用1%和99%分位作为上下限。如果压缩包里给的是伪彩色底片那就不要强行转灰度直接保留RGB。伪彩色图里往往包含调色板映射的灰度层次转灰度反而会丢掉信息。检测脚本里看到mode RGB走单独一条分支就好。3.3 高分辨率焊片切patch先切成640的砖再送进目标检测模型切patch是工业质检里绕不开的一步。焊接底片通常很长有的宽高比夸张到1比8直接把整张图缩放到640x640那些只有几十个像素的小缺陷会变成两三个像素模型就是有天大的本事也学不出来。遥感目标检测、水下管道裂缝这类场景遇到同样的问题时解法也一致先切块再训练。切patch的关键是overlap不能省。缺陷刚好落在两块的拼接缝处时没有重叠区的切法会把一个框切成两半导致两边都只有残缺目标。我一般用patch640、overlap64也就是步长576。切的时候要把边界框坐标同步平移并且只保留中心点落在patch内的框import cv2 import numpy as np from pathlib import Path PATCH 640 OVERLAP 64 STEP PATCH - OVERLAP def load_yolo_boxes(label_path, img_w, img_h): boxes [] with open(label_path) as f: for line in f: parts line.split() if len(parts) 5: continue cls, cx, cy, bw, bh map(float, parts[:5]) x1 (cx - bw / 2) * img_w y1 (cy - bh / 2) * img_h x2 (cx bw / 2) * img_w y2 (cy bh / 2) * img_h boxes.append((int(cls), x1, y1, x2, y2)) return boxes img cv2.imread(film_prep.png, cv2.IMREAD_UNCHANGED) h, w img.shape[:2] boxes load_yolo_boxes(film_prep.txt, w, h) patch_idx 0 for y in range(0, h, STEP): for x in range(0, w, STEP): x2 min(x PATCH, w) y2 min(y PATCH, h) chip img[y:y2, x:x2] new_boxes [] for cls, x1, y1, bx2, by2 in boxes: cx, cy (x1 bx2) / 2, (y1 by2) / 2 if x cx x2 and y cy y2: nx1 max(x1, x) - x ny1 max(y1, y) - y nx2 min(bx2, x2) - x ny2 min(by2, y2) - y if nx2 - nx1 5 or ny2 - ny1 5: continue new_boxes.append((cls, nx1, ny1, nx2, ny2)) # 写入新标签 if new_boxes: save_name fchip_{patch_idx:05d} cv2.imwrite(fimages/{save_name}.png, chip) with open(flabels/{save_name}.txt, w) as f: for cls, nx1, ny1, nx2, ny2 in new_boxes: nw (nx2 - nx1) / chip.shape[1] nh (ny2 - ny1) / chip.shape[0] ncx (nx1 nx2) / 2 / chip.shape[1] ncy (ny1 ny2) / 2 / chip.shape[0] f.write(f{cls} {ncx:.6f} {ncy:.6f} {nw:.6f} {nh:.6f}\n) patch_idx 1这段代码看起来长但每一个分支都有实际意义。中心点判断是避免重复框的最省力方案丢弃小于5像素的框是避免切patch把框切剩一个角后模型学到错误形状。注意切patch会大幅增加图片数量2647张底片切成640块之后可能变成两三万张这时候train.txt的生成逻辑也要跟着改成“按原始底片分组”不能直接对所有patch随机划分。提示切patch之后训练集和验证集必须还是按底片ID隔离。同一张底片切出的几十个patch如果一部分进train一部分进val数据泄漏比不切patch还严重。4. 用YOLOv8训练自己的焊缝缺陷检测模型超参数和损失配置4.1 先写data.yaml再调imgsz小目标缺陷不吃通用配置把数据准备好后训练目标检测模型最通用的做法是用YOLOv8这类工具链。第一步是写data.yaml。YOLO格式里names的序号必须和标签txt里的第一个数字一一对应。焊缝缺陷检测里常见的六类我会这样占位path: D:/datasets/weld_xray train: train.txt val: val.txt names: 0: gas_pore 1: slag_inclusion 2: incomplete_penetration 3: lack_of_fusion 4: crack 5: undercut如果压缩包里的labels标注顺序不是这个只需要改names里的名字数字序号不要动。train.txt和val.txt的路径可以用绝对路径也可以都放在path目录下用相对路径。YOLOv8训练命令里最重要的超参数是imgsz。很多人拿COCO预训练模型直接跑640但焊缝缺陷里裂纹、夹渣这类目标面积占比可能只有0.1%到0.5%640分辨率下这些小目标在骨干网络里只剩低层特征。实际操作中我会先用上一章统计的框面积中位数做判断如果中位数小于整图面积的1%imgsz至少提到960显存足够就上1280。yolo detect train datadefect.yaml modelyolov8n.pt imgsz960 batch16 epochs150 patience30 flipud0.0 mosaic1.0 copy_paste0.3 valTrue几个参数的逻辑要说明白modelyolov8n.pt是加载预训练权重对于X射线底片这种和自然图像差异很大的输入预训练权重依然有帮助它学过的底层纹理特征能加速收敛。如果现场环境完全离线可以在第一次联网时把权重准备好否则就用yolov8n.yaml从头训练收敛会明显变慢。batch16是常见起步值显存不够时先降到8不要在训练中途为了省显存把imgsz悄悄降了那会让整个实验不复现。epochs150配patience30是给早停留出足够观察区间。4.2 类别不平衡时靠copy-paste和翻转策略而非硬调损失六类缺陷的数量极少均衡。气孔可能占了一半的框裂纹只有几十个框。遇到这种情况很多人的第一反应是改损失函数给少数类加权重。但YOLOv8的损失本身是box、cls、dfl三项混合的单纯在损失层加类别权重往往不好控制调过头会让少数类的框面积集体漂移。我在焊缝检测上更常用的做法是用数据增强去补少数类。训练命令里的copy_paste0.3就是干这个的把同一张图里剪裁出的目标框随机粘贴到别的图上。这个增强对气孔、夹渣这类离散型缺陷非常有效因为它们的背景依赖不强。但裂纹、未焊透这类缺陷通常和焊缝走向强相关copy_paste会把裂纹贴到没有焊缝纹理的位置模型学到的是“任何地方有裂纹形纹理就是裂纹”误检率立刻上去。所以用了copy_paste之后一定要单独看每类的precision和recall不能只看整体mAP。还有翻转策略。自然图像训练默认会开fliplr1.0也就是水平翻转。焊缝底片里如果所有原始图的焊缝都是从左到右水平走向水平翻转会制造大量方向冲突的训练信号垂直翻转更是要慎重。我一般会设fliplr0.5、flipud0.0把垂直翻转直接关掉因为底片上下方向往往有铅字编号等强干扰信息。另外类别不平衡严重时最简单的兜底方案是直接过滤训练集里的背景图。2647张底片不可能每张都有缺陷可能有大量无缺陷负样本。YOLO训练中如果负样本占比过高模型会倾向于保守漏检增加。常见做法是控制负样本比例在30%以内把多余的负样本从train.txt里拿掉验证集里保留完整负样本用于评估误报率。5. X射线焊缝缺陷检测的5条避坑记录现象、原因与解决5.1 验证集mAP虚高现场漏检却打脸第一次跑通训练后验证集mAP可能到了0.85感觉很好但拿到现场真实底片上一测漏检率直接没法看。这个翻车我见过不止一次原因九成是划分泄漏同一张大底片切出来的patch一部分进了训练集一部分进了验证集。模型等于提前看过验证目标的上下文纹理评估分数自然失真。解决方法是回到第3.1节那种按底片ID分组的划分方式。每个patch的文件名要保留原始底片编号划分逻辑先按编号分组再整体切分而不是对所有patch文件直接random split。还记得有个项目这样改完验证mAP从0.85掉到0.68这才是真实水平现场漏检才对得上数。5.2 裂纹这种细长缺陷训练和推理都被下采样抹掉裂纹类缺陷的recall总是上不去。看训练集标注框数量不少尺寸也不小但模型就是学不到。把所有图片缩放到固定尺寸训练时细长裂纹如果方向接近水平在多次下采样后特征会在深层特征图里被拉成一条线响应值被周围背景淹没。原因是裂纹长宽比太极端普遍在10比1以上普通缩放到640x640会把宽度压到两三像素以内。解决方法是先切patch训练训练和推理保持同一切片逻辑不能训练切了推理不切。如果有条件可以针对裂纹这种类别单独做方向增强把裂纹旋转30度以内的变体补充进训练集但要注意垂直方向别转太多。5.3 16位TIFF直接喂模型训练loss怎么都压不下去训练loss在高位震荡十几个epoch都不怎么下降第一件事不要调超参数回头检查数据通道。X射线底片如果是16位TIFF而预处理只用cv2.imread默认读法图像会被截断成8位且灰阶严重压缩很多底片读出来几乎是一张全黑图。模型等于在学一张信息丢失的图floss降不动是正常的。解决方法是统一走第3.2节的读取流程IMREAD_UNCHANGED读回原始位深线性拉伸到8位再做CLAHE。这个流程必须在训练脚本里固定下来预处理和训练不能分两套脚本否则训练时用的图和你手动查看的图完全不是一回事。5.4 标签里有越界框或0尺寸框训练时warning不断训练log里频繁出现negative box、empty label之类的warning时通常不是框架bug是标签本身有问题。标注框坐标可能超出了图片宽高范围也可能某个框的宽或高被标成了0还有可能某张图的标签txt文件是空的但文件名对得上。解决方法是写一个标签清洗流程在训练前把每一行都校验一遍坐标必须是非负浮点数宽高必须大于0.01框中心点必须在图片范围内。越界框直接裁剪到边界宽高小于阈值的框删掉空标签文件对应图片也一起从训练列表里移除。不要怕丢数据脏标签留下的负作用远比删掉那几张图严重。5.5 底片边缘的字符印痕成为顽固误报模型训练完误检基本集中在底片边缘的焊缝编号、铅字码、拍片日期这些印刷痕迹上缺陷没漏多少误报却把自动化评片的可信度拉垮了。原因是这些字符在CLAHE增强后边缘锐利、内部空洞形状上很像夹渣或气孔模型在低层特征上直接把它们认成了同一类。解决思路分两步如果标签里有明确的ROI区域可以把字符区域作为ignore区域在推理后过滤如果没有ignore机制就单独给“字符标记”增加一类背景负样例不归入六大缺陷类推理后再删掉这一类。还有一个更简单但有效的方法检查误报框是否集中在距底片边缘固定像素范围内如果是直接在推理结果里对边缘区域降权。6. 切片推理与预处理版本锁定高分辨率X光片落地前的最后两步训练阶段做了切patch推理阶段也必须一样否则训练和推理的输入分布对不上。整张X射线底片直接送进模型和训练时切块喂进去看到的视野完全不同小目标缺陷会被下采样抹掉。我现在的做法是写一个统一推理函数把整张底片切成和训练时一致的640x640块带64像素重叠逐块预测再把框坐标映射回原图最后用NMS合并重叠框。import torch import numpy as np from torchvision.ops import nms def infer_film(model, img, patch640, overlap64, conf0.3, iou0.5): h, w img.shape[:2] step patch - overlap dets [] for y in range(0, h, step): for x in range(0, w, step): x2, y2 min(x patch, w), min(y patch, h) chip img[y:y2, x:x2] result model.predict(chip, imgszpatch, confconf, verboseFalse)[0] for box in result.boxes.data: x1, y1, bx2, by2, score, cls box[:6].tolist() dets.append([x x1, y y1, x bx2, y by2, score, cls]) if not dets: return [] dets torch.tensor(dets) keep nms(dets[:, :4], dets[:, 4], iou) return dets[keep].tolist()这个函数有三个关键点坐标映射时x和y的偏移量必须加在预测框的起始坐标上overlap设64是为了防止目标正好落在切口上最后一步NMS用模型置信度作为排序依据否则边缘重叠框会输出多个重复结果。注意model.predict里不能再传缩放参数chip本身已经固定成patch尺寸。还有一个比推理更隐蔽的坑预处理脚本的版本管理。灰度拉伸、CLAHE参数、切patch的patch和overlap任何一个数值被手滑改过训练集和推理输入就对不上模型效果会莫名其妙变差。我现在每次开始一轮训练都会把预处理脚本复制一份带时间戳放到项目目录下同时把关键参数写进一个preprocess_config.md训练完把模型的mAP和这个配置放一起存。过两个月回来重跑实验时不用靠记忆猜当时到底用了clipLimit2.0还是2.5。这类X射线底片数据的边际收益往往不在换个更大的模型而在于把输入分布稳定下来。模型可以换增强参数可以试但数据划分和预处理必须像实验记录一样锁定。我自己在这上面吃过亏同一套底片第一次训练mAP0.72隔一周再跑只剩0.61查了半天发现是CLAHE的tileGridSize被同事从8x8改成了16x16。希望帮到你。本文还有配套的精品资源点击获取