☰
焊接图像语义分割实战:6k级标注数据集与训练避坑指南
2026/10/1 6:14:05 网站建设 项目流程

简介:面向焊接质量检测的深度学习语义分割数据集,适合计算机视觉研究者、工业质检算法工程师及相关专业学生。资源围绕超过6000张焊接图像构建,涵盖合格焊点、飞溅、夹渣等多种典型状态,数据统一做过对比度拉伸、resize等增广,有助于提升模型在不同光照与尺度下的泛化能力。压缩包共2000个文件,以png和jpg图像为主,另有类别说明txt与辅助脚本py,整体约418MB,目录结构清晰,方便直接训练与复现。目前已有368人学习浏览,既可以用于训练焊接缺陷自动识别模型,也能作为分割算法对比与工业视觉课题的基准数据。配套的类别文件与预处理脚本,能帮助快速理解数据格式并开展对比试验,尤其适合需要自定义训练流程的进阶使用者。

1. 焊接图像语义分割为什么值得用带标签的6k级数据集

产线上的焊接质检,最难受的不是模型选型,而是手里没有一套能和现场工况对得上的数据。通用分割数据集里几乎找不到焊缝、飞溅、母材烧损这类目标,而真实焊件又全是强反光、烟尘和随机纹理,随便拿个预训练模型上去测,mIoU能掉到不敢看。焊接图像语义分割数据集的价值就在这:超过6k张的真实焊接图像,每张都带像素级标签,够把网络从「见过很多图」拉到「见过你家产线的图」。适合三类人:做焊缝缺陷检测的算法工程师、搞机器人视觉引导的调试人员,以及想用语义分割切入工业视觉却苦于没有干净数据的研究生。它解决的不是算法创新问题,而是落地前最贵的那一步——数据集闭环。

2. 拆开这套数据集:标注形式、类别设计与划分策略

2.1 标签的三种常见形态:mask、contour 与 json/png 组合

拿到带标签的焊接图像数据集,第一步不是急着训练,而是先搞清楚标签到底以什么形式存在。工业界最常见的语义分割标注交付形式其实就三种:一是单通道的 PNG mask,每个像素存一个类别 id,背景通常是 0,视觉上看起来是黑的;二是用 OpenCV 的 findContours 从 mask 里抽出来的 contour 点集,存在 json 里,方便转成多边形;三是同一张图同时给原图、二值/多值 mask 和 json 三类文件,即 mask+json 双写。

我一般会先写个脚本扫描标签目录,统计每个 PNG 的通道数和唯一像素值。这一步能避开很多后续麻烦:如果 mask 是 8-bit 单通道,直接当索引图读;如果它是 24-bit 的 RGB 调色板图,就得先做颜色到类别 id 的映射,否则torch.Tensor一加载就把类别数乘以 3,loss 计算直接崩。常见做法是先按像素值聚类,人工核对三个颜色,再生成一份class_dict.json存映射关系。

import os import numpy as np from PIL import Image label_dir = "labels/train" class_dict = {} for name in sorted(os.listdir(label_dir)): if not name.endswith(".png"): continue mask = np.array(Image.open(os.path.join(label_dir, name))) if mask.ndim == 3: mask = mask[:, :, 0] # 三通道调色板图取第一通道做索引 unique = np.unique(mask) for v in unique.tolist(): class_dict.setdefault(v, 0) print(f"{name}: shape={mask.shape}, unique={unique}, max={mask.max()}") print("类别数(含背景):", len(class_dict))

这段代码做的事是盘点标签的实际类别构成。mask.ndim == 3的检查非常关键,很多调色板 PNG 读进来是三维,直接unique会得到一组奇怪的三通道组合,而不是干净的类别 id。这里先取第一通道只是临时手段,正式映射一定要按调色板表转。另外,max的值能帮你快速判断类别 id 是否从 0 连续编号,如果出现 0 和 2 但没有 1,说明数据集里某个类在训练标签中缺失,训练时类别数要按全集定义而不是按缺失后的子集。

2.2 类别体系决定模型上限:背景稀释是最隐蔽的坑

焊接图像语义分割的类别设计,业内默认的底线是「焊道、飞溅、母材」三个前景类加上背景,共四个类别。焊道要细分成「焊缝成型良好」与「咬边/未熔合」,飞溅要区分「附着飞溅」和「大颗粒飞溅」,这些细化会在标注成本上增加很多,但对后续缺陷检测的帮助是巨大的。类别的选择本质是在定义模型的输出空间,如果只标背景和前景,那模型学到的其实是「哪里是金属反光」,而不是「哪里有缺陷」。

背景稀释是这套 6k 数据集里最容易被忽视的问题。焊接图里母材和暗背景往往占了 60% 以上的像素,训练时模型只要学会预测背景,loss 就能降得很低,焊道这类窄长结构会被完全吃掉。所以类别定义要按像素占比重新审视一遍:如果「母材」占了太大的比例,建议把训练 loss 换成带类别权重的版本,或者把大块母材在 loss 里降权。

这里有个可落地的做法:先拿标签统计各类像素占比,再据此设定 loss 权重。

import numpy as np from PIL import Image import os label_dir = "labels/train" counts = np.zeros(4, dtype=np.int64) # 背景/焊道/飞溅/母材 for name in sorted(os.listdir(label_dir)): if not name.endswith(".png"): continue mask = np.array(Image.open(os.path.join(label_dir, name))) if mask.ndim == 3: mask = mask[:, :, 0] vals, cnts = np.unique(mask, return_counts=True) for v, c in zip(vals, cnts): if v < 4: counts[v] += c weights = 1.0 / (counts + 1e-6) weights = weights / weights.sum() * len(counts) print("像素占比:", counts / counts.sum()) print("建议loss权重:", weights)

权重设为像素占比的倒数,是最直白的类别均衡方式。但要注意飞溅这类目标往往像素占比只有 1% 左右,权重拉太高会让模型把正常金属纹理误判成飞溅,所以weights建议再乘一个上限系数,比如不超过 5。焊道和飞溅的权重可以再乘 1.5 倍硬放大,因为这两个类对后续检测最有价值。

2.3 数据划分:别按文件名随机切,按焊缝接头切

6k 张数据的划分,看起来是个小问题,实际是决定验证分数真实性的关键。很多人直接random_split按文件名切 8:1:1,最后验证集 mIoU 虚高,到了现场却翻车。原因在于焊接图像数据往往存在「同一焊缝的连续帧」现象,同一道焊缝的相邻照片高度相似,如果它们同时出现在训练集和验证集,验证集就变成了「背题测试」,模型见过了几乎一样的图。

正确做法是按拍摄对象分组切分。常见的数据集目录里会有一层「接头编号 / 焊缝编号」的子目录,或者文件名里带编号前缀,比如weld_01_frame_0003.jpg这种。切分时以weld_01为单位,把整个编号的文件全部放进同一边,保证训练集和验证集里没有同一条焊缝的图。

import os import random from collections import defaultdict from shutil import copy2 image_dir = "images/train" train_dir, val_dir, test_dir = "split/train", "split/val", "split/test" groups = defaultdict(list) for name in sorted(os.listdir(image_dir)): group = name.split("_")[0] + "_" + name.split("_")[1] # 提取weld_编号 groups[group].append(name) group_list = list(groups.keys()) random.seed(42) random.shuffle(group_list) n_train = int(len(group_list) * 0.8) n_val = int(len(group_list) * 0.1) train_groups = set(group_list[:n_train]) val_groups = set(group_list[n_train:n_train + n_val]) test_groups = set(group_list[n_train + n_val:]) for group, files in groups.items(): target = train_dir if group in train_groups else val_dir if group in val_groups else test_dir os.makedirs(os.path.join(target, group), exist_ok=True) for f in files: copy2(os.path.join(image_dir, f), os.path.join(target, group, f))

这段脚本把每个编号组的文件整体送入同一集合。注意 seed 固定 42,保证复现。如果没有子目录,文件名也一定带着拍摄批次号,按批次号切是最稳妥的中间方案。这个「按组不按文件」的划分习惯,是决定验证集可信度的关键。

3. 用这套 6k 数据集训练语义分割模型:转换、脚本与参数建议

3.1 把原始目录转成 VOC 格式:转换脚本与四种标签形态的取舍

拿到 6k 张图和标签之后,第一件事是把散落的文件整理成训练框架认识的格式。PyTorch 自己的 Dataset 类自然可以由你任意发挥,但工程上为了方便调试、换框架、配合标注工具,直接落成 VOC 格式(JPEGImages、SegmentationClass、ImageSets 三个目录)最省心。

转换要注意几个容易翻车的细节:label 要和原图同名;SegmentationClass必须存成调色板 PNG 而不是cv2.imwrite的默认三通道 JPEG(JPEG 有损压缩会直接毁掉像素 id);另外要检查原图和 mask 的分辨率是否一致,有的数据集标注时做了缩放。转换脚本如下。

import os import numpy as np from PIL import Image import shutil root = "weld_dataset" voc_root = "weld_voc" os.makedirs(f"{voc_root}/JPEGImages", exist_ok=True) os.makedirs(f"{voc_root}/SegmentationClass", exist_ok=True) os.makedirs(f"{voc_root}/ImageSets/Segmentation", exist_ok=True) # 定义类别调色板: 0背景, 1焊道, 2飞溅, 3母材 palette = [0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255] for split in ["train", "val", "test"]: img_dir = f"{root}/images/{split}" mask_dir = f"{root}/labels/{split}" names = [] for name in sorted(os.listdir(img_dir)): shutil.copy(f"{img_dir}/{name}", f"{voc_root}/JPEGImages/{name}") mask = np.array(Image.open(f"{mask_dir}/{name.replace('.jpg', '.png')}")) mask_img = Image.fromarray(mask.astype(np.uint8)) mask_img.putpalette(palette) # 写入调色板 mask_img.save(f"{voc_root}/SegmentationClass/{name.replace('.jpg', '.png')}") names.append(name.replace('.jpg', '')) with open(f"{voc_root}/ImageSets/Segmentation/{split}.txt", "w") as f: f.write("\n".join(names))

参数说明:palette 列表每三个数定义一种颜色,长度必须覆盖所有类别数,否则putpalette会报错或写出错色的图。mask.astype(np.uint8)强制把类别 id 压到 8-bit,超过 255 的类别号会被截断,这是语义分割标签的老坑。另外如果原图是灰度图,shutil.copy没影响,但后续增强要注意保持三通道输入,很多分割模型的第一层卷积是三通道的。

3.2 最小可跑的训练脚本:Dataset 读取 + loss 选择

数据整理好后,训练脚本本身可以很薄。这里用一个最小化但完整的 PyTorch 训练骨架:自定义 Dataset 读图、读 mask,训练循环只负责 forward、loss、backward、验证。模型用 UNet 变体即可,代码里从models里导入预定义的 UNet,聚焦数据与训练逻辑的部分。

import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import numpy as np class WeldSegDataset(Dataset): def __init__(self, img_dir, mask_dir, input_size=(512, 512)): self.img_paths = sorted([f"{img_dir}/{n}" for n in os.listdir(img_dir)]) self.mask_paths = sorted([f"{mask_dir}/{n}" for n in os.listdir(mask_dir)]) self.input_size = input_size def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img = Image.open(self.img_paths[idx]).convert("RGB").resize(self.input_size) mask = Image.open(self.mask_paths[idx]).resize(self.input_size, Image.NEAREST) img_t = transforms.ToTensor()(img) mask_t = torch.from_numpy(np.array(mask)).long() return img_t, mask_t

Image.NEAREST是 mask 缩放的唯一正确选择,默认的 BILINEAR 会把 1 和 0 插值成 0.5,Softmax 后类别就乱了。Dataset 就这么点,训练循环里重点在 loss 的选择上:焊接任务建议CrossEntropyLoss(weight=class_weights)起步,不推荐单独用 Dice,窄焊缝在早期训练阶段会导致梯度不稳定。等模型第一个 epoch 收敛后,可以换DiceLoss和CE加权叠加,这属于玄学调参,靠验证集分数决定。

model = UNet(in_channels=3, num_classes=4) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = torch.nn.CrossEntropyLoss(weight=class_weights) for epoch in range(30): model.train() for imgs, masks in train_loader: imgs, masks = imgs.cuda(), masks.cuda() preds = model(imgs) loss = loss_fn(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() # 每个epoch后跑一次验证 model.eval() val_iou = compute_iou(model, val_loader) print(f"epoch {epoch}: loss={loss.item():.4f}, val_iou={val_iou:.4f}")

参数建议:lr=1e-3配 Adam 适合 ResNet 类编码器,如果换 ViT 类编码器要降到 5e-4;batch_size在单卡 12G 显存下建议 8,输入 512x512;30 个 epoch 对 6k 图够用,超过 50 个 epoch 后验证分数通常会进入平台期,再练只是过拟合。焊道类别窄,input_size不要低于 384,否则细裂缝会被 downsampling 吃掉。

3.3 增强策略:焊接图的方向敏感性

焊接图与自然图有个本质区别:焊缝的方向是有物理语义的,横向焊缝和纵向焊缝的特征完全不同,但这不代表旋转增强不能用。绕 z 轴旋转 90 度或 180 度对焊缝检测仍然有效,因为焊接方向不变时,灰度、纹理特征不变。重点约束的是翻转和锐利变换:水平翻转会改变起弧方向,在缺陷检测任务里这相当于训练数据自相矛盾。

我一般在训练里只做如下增强:随机亮度对比度抖动、随机高斯噪声、随机裁剪到 512 附近的尺寸、水平/垂直随机翻转(对单帧焊接图像有效,对序列数据要关掉垂直翻转)。torchvision.transforms里加ColorJitter要克制,焊接图像普遍过曝或过暗,亮度抖动幅度大了会让模型误学成「靠亮度区分焊道」。一条建议:训练阶段如果没加光照归一化,就用transforms.Normalize(mean=0.5, std=0.5),这对过曝图的稳定作用明显。

4. 焊接语义分割避坑指南:标注、训练与验证的 5 个高频问题

4.1 标签和原图尺寸不一致导致 mask 漂移

现象:训练时 loss 正常下降,但预测结果里焊道的边缘整体错位一截,尤其是图像边缘部分,偏差能到十几个像素。

原因:标注工具导出时关掉了自动缩放,或者某些 mask 是按缩略图画完再映射回原图,比例没对齐。很多数据集的标签是逐张独立标注的,偶尔会有几张漏掉缩放。

解决:数据准备阶段加一段强制校验,逐对检查img.size和mask.size,不一致就跳过并打印文件名。在训练脚本的 Dataset 里不做隐式 resize,而是统一到固定尺寸后再对齐检查一次,避免预测时因输入尺寸变化产生系统性偏移。焊接缺陷的容忍度极低,偏移 5 个像素就会让缺陷定位失效。

img = Image.open(path) mask = Image.open(mask_path) if img.size != mask.size: print(f"skip {path}: img={img.size}, mask={mask.size}") continue

4.2 焊道与飞溅在标签中的混标

现象:模型输出的飞溅区域明显比真实多,连成一片,而且焊道边缘也被染成飞溅色。

原因:焊道熔池边缘的金属光泽与飞溅极相似,标注人员在尺度小的图上难以区分;更隐蔽的是「飞溅搭在焊道上」的情况,有的标注把重叠区标成飞溅,有的标成焊道,类别边界在像素级没有统一规则。

解决:给数据集设一条硬性规则——任何像素如果有重叠,优先级按「焊道 > 飞溅 > 母材」来标,并把这条规则写进class_dict.json的注释里。训练时如果再发现模型混淆这两个类,回来查标签,大概率是标注规则不一致。

4.3 类别不均衡导致的小目标丢失

现象:模型整体 mIoU 还行,但飞溅的 IoU 为 0,查了代码没 bug,训练也没崩。

原因:飞溅像素占比可能低于 1%,交叉熵损失被背景完全淹没,网络把所有像素都预测为背景反而 loss 更低。这不是 bug,是目标函数没按数据分布设计。

解决:用本文 2.2 节的方法统计类别占比,把类别权重放大焊道和飞溅。注意权重不要直接从像素倒数拉满,给个上限 5,然后看验证集 IoU 再调一次。飞溅这类小目标还可以在 loss 上叠加一个针对性的boundary_loss,但先别上来就加,多数时候权重调整就够了。

4.4 验证集 mIoU 虚高:同一焊缝进了训练又进了验证

现象:训练时验证 mIoU 一路走高到 0.85,模型部署后到现场新场景 mIoU 只有 0.6 左右,落差巨大。

原因:划分数据时按文件名随机切,同一条焊缝的前后帧被分到了两边,模型在验证集里见过了几乎相同的图。

解决:按 2.3 节的「组划分」方式重做数据集。核对方式很简单——在验证集里随机抽 20 张图,人工看缩略图,如果里面有连续帧或同一条焊缝的多角度图,说明划分没过关。之后报告 mIoU 时,要注明「按焊缝组划分」,这是衡量模型泛化能力的真实边界。

4.5 训练时偶发 loss 为 NaN

现象:训练到中后期某次迭代 loss 直接变 NaN,重启后可能复现,也可能不复现。

原因:焊接图有强烈的反光,某些样本的像素值接近饱和,经过带 BN 的深层网络后激活值爆炸;或者输入 mask 里有超出类别数的值(比如 255 被当成噪声写进去了),导致 CrossEntropy 收到了越界 target。

解决:给__getitem__里加一句mask = torch.clamp(mask, 0, num_classes - 1)保底;训练时开grad_clip_norm=1.0。查标签里有没有异常像素值,用 2.1 节的扫描脚本再看一遍。这个坑多出现在工业图里,别急着调学习率,先查标签和输入数值范围。

5. 验证不只是 mIoU:用三类指标把模型推到产线前

按焊缝组划分之后,验证阶段我习惯跑三组指标而不只看 mIoU。第一组是每一类的 IoU 和 mIoU,重点看飞溅类;第二组是「缺陷召回率」——把焊道类别里预测出的缺陷区域与人工标定缺陷框做匹配,计算召回和误报;第三组是稳定性验证,连续 10 个 epoch 验证集分数取标准差,标准差大说明模型仍然不稳定,需要继续训练或调整增强。

一个实用的产出是预算一张「单类 IoU 报告」,直接决定这个模型能不能上线。这里给出一个快速验证脚本:

import numpy as np def compute_iou_per_class(pred, target, num_classes=4): ious = [] for cls in range(num_classes): p = (pred == cls) t = (target == cls) inter = (p & t).sum() union = (p | t).sum() ious.append(inter / (union + 1e-6)) return np.array(ious) ious = compute_iou_per_class(pred_mask, true_mask) print("IoU by class:", dict(zip(["bg", "weld", "spatter", "base"], ious))) print("mIoU:", ious.mean())

真正上线前,我会把验证集里最容易让模型翻车的图单独抽出来,做成「困难样本集」。做法是保存每张图的 mIoU 最低的 50 张,人工看一下是标签错还是场景极端。这比堆更多网络结构重要得多,焊接视觉的瓶颈通常不在模型复杂度,而在数据噪声和标签一致性。我吃过一次亏:当年第一版模型验证集 0.82,直接部署后被现场光照环境打到了 0.5,后来回头细查,才发现验证集里混了一半同组连续帧。从那以后我所有分割项目都先把划分策略写进 README 再谈训练。看清楚你的验证集,比换任何网络都值得,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询