
简介面向焊接质量检测的深度学习语义分割数据集适合计算机视觉研究者、工业质检算法工程师及相关专业学生。资源围绕超过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 三类文件即 maskjson 双写。我一般会先写个脚本扫描标签目录统计每个 PNG 的通道数和唯一像素值。这一步能避开很多后续麻烦如果 mask 是 8-bit 单通道直接当索引图读如果它是 24-bit 的 RGB 调色板图就得先做颜色到类别 id 的映射否则torch.Tensor一加载就把类别数乘以 3loss 计算直接崩。常见做法是先按像素值聚类人工核对三个颜色再生成一份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, dtypenp.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_countsTrue) 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_okTrue) 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的默认三通道 JPEGJPEG 有损压缩会直接毁掉像素 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_okTrue) os.makedirs(f{voc_root}/SegmentationClass, exist_okTrue) os.makedirs(f{voc_root}/ImageSets/Segmentation, exist_okTrue) # 定义类别调色板: 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_tImage.NEAREST是 mask 缩放的唯一正确选择默认的 BILINEAR 会把 1 和 0 插值成 0.5Softmax 后类别就乱了。Dataset 就这么点训练循环里重点在 loss 的选择上焊接任务建议CrossEntropyLoss(weightclass_weights)起步不推荐单独用 Dice窄焊缝在早期训练阶段会导致梯度不稳定。等模型第一个 epoch 收敛后可以换DiceLoss和CE加权叠加这属于玄学调参靠验证集分数决定。model UNet(in_channels3, num_classes4) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn torch.nn.CrossEntropyLoss(weightclass_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(fepoch {epoch}: loss{loss.item():.4f}, val_iou{val_iou:.4f})参数建议lr1e-3配 Adam 适合 ResNet 类编码器如果换 ViT 类编码器要降到 5e-4batch_size在单卡 12G 显存下建议 8输入 512x51230 个 epoch 对 6k 图够用超过 50 个 epoch 后验证分数通常会进入平台期再练只是过拟合。焊道类别窄input_size不要低于 384否则细裂缝会被 downsampling 吃掉。3.3 增强策略焊接图的方向敏感性焊接图与自然图有个本质区别焊缝的方向是有物理语义的横向焊缝和纵向焊缝的特征完全不同但这不代表旋转增强不能用。绕 z 轴旋转 90 度或 180 度对焊缝检测仍然有效因为焊接方向不变时灰度、纹理特征不变。重点约束的是翻转和锐利变换水平翻转会改变起弧方向在缺陷检测任务里这相当于训练数据自相矛盾。我一般在训练里只做如下增强随机亮度对比度抖动、随机高斯噪声、随机裁剪到 512 附近的尺寸、水平/垂直随机翻转对单帧焊接图像有效对序列数据要关掉垂直翻转。torchvision.transforms里加ColorJitter要克制焊接图像普遍过曝或过暗亮度抖动幅度大了会让模型误学成「靠亮度区分焊道」。一条建议训练阶段如果没加光照归一化就用transforms.Normalize(mean0.5, std0.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(fskip {path}: img{img.size}, mask{mask.size}) continue4.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_norm1.0。查标签里有没有异常像素值用 2.1 节的扫描脚本再看一遍。这个坑多出现在工业图里别急着调学习率先查标签和输入数值范围。5. 验证不只是 mIoU用三类指标把模型推到产线前按焊缝组划分之后验证阶段我习惯跑三组指标而不只看 mIoU。第一组是每一类的 IoU 和 mIoU重点看飞溅类第二组是「缺陷召回率」——把焊道类别里预测出的缺陷区域与人工标定缺陷框做匹配计算召回和误报第三组是稳定性验证连续 10 个 epoch 验证集分数取标准差标准差大说明模型仍然不稳定需要继续训练或调整增强。一个实用的产出是预算一张「单类 IoU 报告」直接决定这个模型能不能上线。这里给出一个快速验证脚本import numpy as np def compute_iou_per_class(pred, target, num_classes4): 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 再谈训练。看清楚你的验证集比换任何网络都值得希望帮到你。本文还有配套的精品资源点击获取