简介:面向工业焊缝缺陷检测任务的目标检测数据集,包含2647张X射线底片清晰原图,对裂纹、未熔合、未渗透、空隙、夹渣及未融化6类缺陷进行矩形框标注,累计4766个目标框。数据采用VOC与YOLO双格式存储,图片、XML标注与TXT标签一一对应,可直接用于YOLO系列、Faster R-CNN等主流检测器训练,省去格式转换步骤。压缩包约39.56MB,共2000个文件,以XML标注文件为主,并附有训练/验证/测试集划分列表及说明文档,目录结构规整。数据集标注准确合理,未经图像增强,适合作为工业质检算法研究的训练基准,也可用于高校教学与项目演示。目前已有355人浏览学习,适用于开展X射线底片焊缝缺陷识别实验或构建基线模型。
1. 2647张X射线底片焊缝缺陷数据集,算不算够用
焊缝X射线底片的缺陷检测,是目标检测里少有的“图像完全不像自然图像”的场景:没有颜色、对比度平、缺陷面积小,标注还得靠有探伤资质的人来做。标题里最有信息量的不是“目标检测”,也不是“6类”,而是“2647张”——这个量级刚好卡在“随便跑跑”和“认真做数据”的分界线上。直接解压训练多半能出个还看得过去的mAP,但分数上不去时,问题通常不在模型,而在数据组织方式。下面按一个工业视觉工程师拿到这个zip包之后会走的路径来写:先盘结构,再进yolo流程,随后针对灰度、小目标、类别混淆做专项调优,最后落到导出和阈值选择。2647张不需要数据中心的大规模GPU,一张消费级显卡甚至纯CPU都能跑完,只是时间长短的区别。
2. 数据集结构盘点:把zip压缩包变成可训练的标注目录
2.1 解压前先验包,再解压
拿到任何zip格式的数据集,第一步不是双击解压,而是先看压缩包内部结构。这个习惯能省掉处理错误格式的半天时间,也能在训练前就发现文件不完整的问题。
unzip -l weld_defect_6cls_2647.zip | head -60 unzip -t weld_defect_6cls_2647.zip | tail -5-l只列出压缩包内的文件清单,不解压,用来快速确认目录层级;-t会逐个校验内部文件的CRC,最后输出类似No errors detected in compressed data的提示。如果下载中断过,文件列表可能完整,但个别图片已经损坏,所以-t这一步建议保留。若看到End-of-central-directory signature not found这类报错,说明zip本身不完整,重新下载比尝试修复更可靠。
常见的数据集压缩包顶层结构有三种,解压后第一眼就要判断自己拿到的是哪一种:
| 顶层结构 | 说明 | 后续处理 |
|---|---|---|
| images/ 与 labels/ | 未按训练/验证划分,标签为YOLO txt | 需自行切分 |
| train/ 与 val/ | 已划分,子目录里各含images和labels | 直接写yaml |
| JPEGImages/ 与 Annotations/ | VOC风格,xml标注 | 转成YOLO格式 |
| images/ 与 annotations/ | COCO风格,一个json文件 | 解析json并转成txt |
确认没有问题后,建立一个独立工作目录再解压,比解压后到处移动文件更省事:
mkdir -p weld6_raw && unzip -q weld_defect_6cls_2647.zip -d weld6_raw-q是安静模式,只输出错误信息,-d指定解压目标目录。需要注意解压路径里不要带中文和空格,Windows下尤其容易在后续yaml路径解析时出问题。
2.2 识别标注格式:YOLO txt、VOC xml 还是 COCO json
判断标注格式最直接的方法是看文件后缀,三条命令分别列出三种可能的标注文件:
find weld6_raw -type f -name "*.txt" | head find weld6_raw -type f -name "*.xml" | head find weld6_raw -type f -name "*.json" | headYOLO格式每一行是class_id center_x center_y width height,坐标归一化到0到1之间,一张图对应一个同名txt;VOC xml里能找到<bndbox><xmin>这样的像素坐标节点;COCO json则包含"images"和"annotations"两个顶层数组。
YOLO格式最省事,目录摆对就能训练。VOC xml需要转换,因为yolo训练器读不了xml。最常见做法是把xml解析成YOLO txt:
import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path: Path, out_dir: Path, class_names: list) -> None: tree = ET.parse(xml_path) root = tree.getroot() img_w = float(root.find('size/width').text) img_h = float(root.find('size/height').text) lines = [] for obj in root.iter('object'): difficult = obj.find('difficult') if difficult is not None and int(difficult.text) == 1: continue cls = class_names.index(obj.find('name').text) x1 = float(obj.find('bndbox/xmin').text) y1 = float(obj.find('bndbox/ymin').text) x2 = float(obj.find('bndbox/xmax').text) y2 = float(obj.find('bndbox/ymax').text) cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h lines.append(f"{cls} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") (out_dir / (xml_path.stem + '.txt')).write_text("\n".join(lines))脚本逻辑是把VOC的像素坐标系bndbox换算成归一化值,difficult标签直接跳过,避免把模糊样本送进训练。两个容易踩的坑:class_names的顺序必须与yaml里的names完全一致;转换前确认xml里的size就是原图尺寸,否则所有坐标都会错位。
2.3 用脚本统计分辨率、类别分布与框面积
训练前做三项统计:分辨率分布、每个类别的框数量、标注框相对面积。X射线焊缝底片可能混合不同设备来源,常见的分辨率有1024×1024、2048×2048,也可能有长条形扫描图。分辨率差异大时,统一resize会让小分辨率图像里的缺陷变得更小。
from pathlib import Path from collections import Counter from PIL import Image img_dir = Path('images') label_dir = Path('labels') exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff'} size_counter = Counter() cls_counter = Counter() tiny_boxes = [] for img_path in img_dir.iterdir(): if img_path.suffix.lower() not in exts: continue with Image.open(img_path) as im: size_counter[(im.width, im.height)] += 1 label_path = label_dir / f"{img_path.stem}.txt" if not label_path.exists(): print("missing label:", img_path.name) continue for line in label_path.read_text().splitlines(): parts = line.split() if len(parts) != 5: continue clsid, cx, cy, bw, bh = map(float, parts) cls_counter[int(clsid)] += 1 if bw * bh < 0.01: tiny_boxes.append((img_path.name, int(clsid))) print("分辨率 Top10:", size_counter.most_common(10)) print("各类别框数量:", sorted(cls_counter.items())) print("小框样例数:", len(tiny_boxes))这段代码遍历所有图片,用PIL只读尺寸不解码像素,比cv2更快;标签缺失会直接打印文件名,这是训练时报No labels found的常见根源。bw * bh < 0.01表示框面积占整图不到1%,在1024×1024的图上对应边长不足100像素,属于典型小目标,数量一多就要考虑切图训练。
如果统计出的类别数不是6,或者class id不是从0开始,先检查是不是混入了背景类或标注从1编号。YOLO约定必须从0开始,从1开始不会报错,但预测时所有类别会整体错一位。
2.4 没有现成验证集时怎么切分
压缩包里只有images和labels时,需要自己划分train/val。常见错误是按文件名随机切分,导致同一批底片的不同帧同时出现在训练集和验证集,验证分数虚高。工业质检数据应尽量按工件或底片编号分组划分。
import random from pathlib import Path exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff'} images = sorted([ p for p in Path('images').iterdir() if p.suffix.lower() in exts ]) random.seed(42) random.shuffle(images) val_count = int(len(images) * 0.2) val_imgs = set(images[:val_count]) for img in images: dst = 'val' if img in val_imgs else 'train' (Path(dst) / 'images').mkdir(parents=True, exist_ok=True) img.rename(Path(dst) / 'images' / img.name) lab = Path('labels') / f"{img.stem}.txt" if lab.exists(): (Path(dst) / 'labels').mkdir(parents=True, exist_ok=True) lab.rename(Path(dst) / 'labels' / lab.name)固定随机种子保证每次切分结果一致,rename在同一文件系统里只改路径不复制数据。如果文件名里包含日期、设备号或工件批次字段,优先按这些字段分组,而不是按单张随机切。
3. yolov8训练自己的焊缝缺陷数据集:从yaml到指标解读
3.1 目录重组与dataset.yaml的五个关键字段
yolo训练器只认约定好的目录布局:每个split下分别有images和labels文件夹,图片和标签同名同后缀(标签后缀是txt)。
weld6/ ├── train/ │ ├── images/ │ │ └── 0001.png │ └── labels/ │ └── 0001.txt └── val/ ├── images/ └── labels/如果前面已经从xml或json转出txt,用软链接把目录指过去,避免复制2647张图占双倍磁盘:
cd /data mkdir -p weld6/train/images weld6/train/labels weld6/val/images weld6/val/labels ln -s /data/train_images weld6/train/imagesln -s创建软链接,训练时按绝对路径递归找图,省磁盘空间。随后写weld6.yaml:
path: /data/weld6 train: train/images val: val/images nc: 6 names: 0: crack 1: porosity 2: slag 3: lack_of_fusion 4: incomplete_penetration 5: undercutpath是数据根目录,train和val相对path指定。nc必须等于最大class id加1,names只影响报告显示,但顺序要和转换脚本里的class_names一致。上面names是焊缝六分类的通用命名,具体以你解压后labels里class id对应的说明为准,这里只保证id数量对得上。
验证class id是否连续,一行命令即可:
cut -d' ' -f1 $(find . -name '*.txt') | sort -u输出如果是0 1 2 3 4 5,nc: 6就没有问题。
3.2 训练命令与五个核心参数
yolo detect train \ data=weld6.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=1024 \ batch=16 \ patience=15 \ cache=True逐个说明参数为什么这么设:
model=yolov8s.pt:用COCO预训练权重做迁移学习。2647张的数据量不足以从随机初始化收敛,s版在参数量和容量之间最均衡;yolov8n更快但容量偏小,yolov8m以上在这个量级容易过拟合。imgsz=1024:输入边长。原图2048×2048时设1024等于下采样一倍;原图本身1024时设640或1024都行,不要设得比原图还大,letterbox不会凭空增加信息。batch=16:受显存限制。12G显存跑1024输入的s模型,16是安全值;显存不足时优先降batch而不是降imgsz。epochs=100配patience=15:焊缝缺陷数据通常在40到60轮进入平台期,15轮mAP不涨就提前停。cache=True:2647张图完整缓存进内存,每个epoch省一次磁盘IO,32G内存足够。
训练核心参数速查:
| 参数 | 建议值 | 原因 |
|---|---|---|
| model | yolov8s.pt | 小数据量下s表现最稳 |
| imgsz | 1024 | 保留小目标细节 |
| batch | 16 | 12G显存安全线 |
| patience | 15 | 避免无效训练 |
| cache | True | 2647张量级缓存性价比高 |
3.3 训练输出与指标解读
训练结束后,runs/detect/train目录里有四个东西重点看:weights/best.pt、weights/last.pt、results.csv、val_batch0_pred.jpg。best模型按验证集整体mAP保存,loss最小的epoch不代表mAP最大。
用pandas快速看训练曲线末尾数据:
import pandas as pd df = pd.read_csv('runs/detect/train/results.csv') last = df.iloc[-1] print(df.columns.tolist()) print(last['metrics/mAP50(B)'], last['metrics/mAP50-95(B)'])列名形如train/box_loss和metrics/mAP50(B)。mAP50是IoU阈值0.5下的各类别平均精度,mAP50-95则从0.5到0.95每0.05取一次平均。焊缝缺陷形状不规则,裂纹和未熔合的框很难达到高IoU,所以mAP50-95比自然图像数据集低0.1到0.2是正常现象,不必焦虑。
提示:打开
val_batch0_pred.jpg看错检位置比看数字更快。工业场景里漏检比误检代价更大,把漏检图单独拷出来分析。
训练过程若出现No labels found,回第2.3节查图片和标签是否同名,再查yaml路径拼接是否正确。
4. X射线底片专项调优:灰度输入、tile切图与类别级指标
4.1 灰度图像怎么进目标检测网络
X射线底片本质是单通道灰度图,而大多数预训练网络吃RGB三通道。直接送单通道图,ultralytics会在内部自动复制成三通道,但16位灰度图需要先做位深转换,否则信息丢失或归一化方式不可控。
import cv2 from pathlib import Path for p in Path('images').glob('*.png'): gray = cv2.imread(str(p), cv2.IMREAD_GRAYSCALE | cv2.IMREAD_ANYDEPTH) if gray.dtype != 'uint8': gray = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype('uint8') rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) cv2.imwrite(str(p), rgb)IMREAD_ANYDEPTH让16位图以原始位深读入,cv2.normalize按最值线性拉伸到0到255。注意:如果图像里大面积是均匀黑背景,线性拉伸会把微弱噪声也放大。低对比度焊缝图更常见的是叠加CLAHE:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray)CLAHE对局部区域做直方图均衡,clipLimit=2.0限制对比度放大量,tileGridSize=(8,8)把图分成64个局部块。参数过大时灰度渐变区域会出现带状纹理,模型可能把纹理误判成夹渣。做增强前固定某个缺陷区域,对比处理前后的差异,确认缺陷边缘没有被改变。
4.2 小目标与密集气孔:tile切图加overlap
焊缝底片上的气孔、夹渣经常只有几十像素宽,整图输入时缺陷在深层feature map上只剩几个像素,特征几乎丢失。对2647张这种中等规模数据,常见做法是把大图切成小块,让每个缺陷在训练输入中占更大比例。
def tile_images(img, boxes, tile=1024, overlap=128): h, w = img.shape[:2] step = tile - overlap tiles = [] for y in range(0, h, step): for x in range(0, w, step): cut = img[y:y + tile, x:x + tile] kept = [] for cls, cx, cy, bw, bh in boxes: # 把归一化坐标还原成原图像素坐标 x1 = (cx - bw / 2) * w y1 = (cy - bh / 2) * h x2 = (cx + bw / 2) * w y2 = (cy + bh / 2) * h # 计算在当前tile内的像素范围 nx1, ny1 = max(0, x1 - x), max(0, y1 - y) nx2, ny2 = min(tile, x2 - x), min(tile, y2 - y) # 被切断后剩余不足8像素的框直接丢弃 if nx2 - nx1 < 8 or ny2 - ny1 < 8: continue kept.append((cls, (nx1 + nx2) / 2 / tile, (ny1 + ny2) / 2 / tile, (nx2 - nx1) / tile, (ny2 - ny1) / tile)) if kept: tiles.append((cut, kept)) return tilesoverlap=128让边界处的缺陷不会被拦腰切断,nx2 - nx1 < 8丢弃只剩一点点的框,避免模型学习不完整目标。切图后train和val必须用同一套参数,否则对比失去意义。如果tile总数已经超过5000张,epoch可以从100降到60,靠mosaic增强补多样性。
4.3 漏检定位:从类别AP到标注审查
整体mAP不错,不代表每个类都可用。用best模型重新跑一次验证:
yolo detect val data=weld6.yaml model=runs/detect/train/weights/best.pt命令行末尾会打印每类的Images Instances Box P R mAP50。如果气孔mAP50只有0.3而夹渣有0.8,问题大概率不在模型,而在标注。焊缝X光底片里三类缺陷容易互相混淆:
| 易混类别 | 主要区别 | 标注建议 |
|---|---|---|
| 气孔 vs 夹渣 | 气孔边缘圆滑,夹渣形状不规则 | 按边缘形态归类 |
| 未熔合 vs 未焊透 | 未熔合在坡口侧壁,未焊透在焊缝根部 | 按位置归属 |
| 裂纹 vs 冷隔 | 裂纹有分支,冷隔边缘较钝 | 两组标注人员交叉确认 |
此时先做三步再调参:把漏检最多的类别标签可视化,确认是否大量误标;统计该类别框数量,若不足总数5%,考虑小目标复制粘贴增强;若标注本身不一致,修正标签的收益远大于任何数据增强。
还有一个YOLO特有的坑:默认mosaic增强会把多张X光底片拼在一起,灰度背景的接缝可能被模型学成伪缺陷。若验证时发现图片边缘出现横向或纵向误检,降低mosaic概率并在最后10轮完全关闭:
yolo detect train data=weld6.yaml model=yolov8s.pt \ mosaic=0.5 close_mosaic=10 epochs=100mosaic=0.5让一半batch不做拼接,close_mosaic=10最后10个epoch完全关闭,让模型在接近真实分布的数据上收尾。
5. 从离线模型到产线:ONNX导出、阈值扫描与回归验证
5.1 用ONNX导出并对比推理精度
确定best模型可用后,导出为ONNX格式:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=1024导出后写一小段onnxruntime代码验证输出与原始库一致:
import cv2 import numpy as np import onnxruntime as ort sess = ort.InferenceSession('best.onnx', providers=['CPUExecutionProvider']) img = cv2.imread('val_sample.png', cv2.IMREAD_GRAYSCALE) img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) img = cv2.resize(img, (1024, 1024)).astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1))[None] out = sess.run(None, {sess.get_inputs()[0].name: img}) print(out[0].shape)这里两个细节必须注意:灰度图要显式复制成三通道,否则输入shape对不上;ONNX输出的第二个维度是4+nc,第三个维度是预测框候选数,后处理需要解码坐标再做NMS,生产环境建议直接用OpenVINO或TensorRT的推理库,不要自己写解码。
5.2 用置信度扫描代替拍脑袋定阈值
产线通常更关心漏检率,所以置信度阈值不能只看mAP。用验证集跑多组阈值:
for conf in 0.1 0.2 0.3 0.4 0.5; do yolo predict model=best.pt source=val/images conf=$conf save_txt=True done每组预测结果会输出到runs/detect/predict/labels,写脚本和标注对比,IoU大于0.5且类别相同算命中。统计每个阈值下的漏检数和误检数,选一个漏检满足现场要求的最小阈值。这个值通常比默认的0.25低,因为焊缝缺陷的预测置信度普遍不如自然图像高。
5.3 回归测试集要独立
模型迭代时,不要把整个val集反复用于调参,否则会逐渐把val过拟合进去。严格做法是从2647张里再固定留出100张,只在每次改动后跑一次回归,其余时间不碰。焊接底片标注成本高,这一步不能省。
提示:把解压后的原始包、转换脚本、dataset.yaml和第一次训练的results.csv一起归档,写成一个
data_build.sh脚本。三个月后重新翻新模型时,一条命令就能还原全部数据流程,比任何权重文件都值钱。
本文还有配套的精品资源,点击获取