☰
焊接图像语义分割数据集实战:6k张图从清洗到模型训练
2026/10/1 23:49:02 网站建设 项目流程

简介:一份面向深度学习语义分割任务的焊接图像数据集,专为焊接质量视觉检测场景设计,包含超过六千张经预处理的原图与对应标签,覆盖良好焊缝、夹渣、飞溅等典型类别,适合研究人员与工程师训练分割模型。压缩包内共二千个文件,以PNG和JPG图片为主,并附有类别定义文本与数据处理脚本,整体大小约四百一十八兆。数据已进行对比度拉伸、尺寸调整等增广处理,可提升模型在不同光照和尺度下的泛化能力;标签与类别清单组织清晰,便于直接接入常见的语义分割网络。其中包含典型焊接缺陷样本,对工业场景中的自动质量检测具有直接帮助,目前已有三百六十八人学习,对需要构建焊接缺陷自动识别方案或开展工业视觉实验的学习者,是一份完整、可直接使用的数据资源。

1. 焊接图像语义分割数据集:先弄清楚这 6k 张图能干什么

焊接图像语义分割数据集(超过 6k 张数据和标签)是专门给焊缝、焊道和焊接缺陷做像素级分类的深度学习数据集。它不输出目标框,而是让语义分割算法把图像里的每一个像素归到背景、焊道、缺陷等类别,所以用 YOLO 做目标检测的习惯到这里要先停一下。对做焊接质检、机器人视觉和缺陷检测的工程师来说,这批数据的直接价值是省掉从零标注的几个月时间;但拿到手的关键不是立刻训练,而是先核对标签约定和类别分布,否则 6k 张图也会因为标签不统一变成负资产。

2. 焊接图像语义分割数据集的构成与标签约定:拿到手先做三件事

不管是自己采集的还是别人交付的,焊接图像语义分割数据集的第一关不是模型,而是弄清目录和标签编码。我一般会在解压之后先做三件事:看目录结构、统计 mask 里的类别像素、筛掉明显漏标的图。这三件事做完,后面训练才不会翻车。

2.1 目录与标注格式:先确认是灰度 PNG 还是调色板 PNG

常见的数据集目录会分成 images 和 labels 两个文件夹,外加 train.txt、val.txt 或 class_names.txt。焊接图像的原始图可能是工业相机拍的黑白灰度图,也可能是伪彩色图;标签图通常用 PNG 保存,每个像素值代表一个类别,比如 0 是背景、1 是焊道、2 是气孔、3 是裂纹。类别的具体含义一定要以 class_names.txt 或者标注文档为准,不能靠猜。

拿到数据后先用一条命令看两级目录:

find . -maxdepth 2 -type d find images -type f | head -5 find labels -type f | head -5

第一段命令看数据集的顶层结构,第二段和第三段分别确认图像和标签的文件名是否一一对应。这里最容易出现的问题是文件名对不上:有的数据集图像是 jpg、标签是 png,扩展名不同但主名相同;有的则带 _mask 后缀。如果主名对不上,后面写 Dataset 时会漏掉一批样本,而且报错很隐蔽。

确定文件结构之后,再用 Python 检查标签图的 mode:

from PIL import Image from pathlib import Path label_dir = Path("labels") for p in sorted(label_dir.glob("*.png"))[:10]: img = Image.open(p) print(p.name, img.mode, img.size)

打印结果里如果是 L,说明是单通道灰度图,可以直接作为类别 id 读取;如果是 P,说明是调色板图,读取时要按调色板索引而不是 RGB 值;如果是 RGB,那就要警惕,很多标注软件导出时会顺手把标签存成彩色 PNG,直接np.array()读进来会得到三维数组,训练时大概率直接崩。

注意:mask 的存储格式直接影响训练代码,先花十分钟把 mode 检查完,比训练时到处排查维度错误划算得多。

2.2 统计灰度与类别分布:用一段脚本看清标签

焊接图像里背景往往占了大半张图,真正属于焊道和缺陷的像素非常少。如果不先做类别统计,训练时 loss 很容易被背景主导。下面这段脚本可以统计整个数据集的类别像素数和占比:

from PIL import Image import numpy as np from pathlib import Path label_dir = Path("labels") class_counts = {} for p in label_dir.glob("*.png"): img = Image.open(p) if img.mode not in ("L", "P"): print(f"{p.name} 不是灰度/调色板标签,跳过") continue mask = np.array(img) values, counts = np.unique(mask, return_counts=True) for v, c in zip(values, counts): class_counts[int(v)] = class_counts.get(int(v), 0) + int(c) for cls_id, total in sorted(class_counts.items()): print(f"class {cls_id}: {total} pixels")

这段脚本的关键是np.unique(mask),如果标签是 P 模式,np.array(img)拿到的是调色板索引,恰好对应类别 id;如果是 L 模式,拿到的就是灰度值。统计完成后,重点看两类值:一是有没有 255,二是缺陷类的像素占比是不是低到离谱。

255 在很多语义分割任务里代表 ignore 区域,也就是不参与 loss 计算的像素。如果焊接图像里有过曝、反光或者工件之外的区域被标成 255,训练时要在损失函数里设置ignore_index=255,否则模型会强行学这些不可见区域。缺陷类像素占比如果不到 1%,不要急着换模型,先把类别权重或者 Dice loss 加上,否则前几十个 epoch 模型可能一直输出全背景。

2.3 按掩膜质量筛图:哪些图该留、哪些图该删

6k 张带标签的图不是每一张都值得送进训练集。焊接图像经常出现整张图都是背景的情况,可能是采集到了焊前或焊后的间隙,也可能是标注员漏标了。漏标图放进训练集,相当于给模型喂了错误标签,比少一张图危害更大。

写一个简单的漏标检查脚本:

from PIL import Image import numpy as np from pathlib import Path label_dir = Path("labels") empty_samples = [] for p in label_dir.glob("*.png"): img = Image.open(p) if img.mode in ("L", "P"): mask = np.array(img) if len(np.unique(mask)) == 1: empty_samples.append(p.name) print(f"空标签数量: {len(empty_samples)}") for name in empty_samples[:20]: print(name)

如果一个样本的 mask 里只有一个像素值,说明它要么是全背景,要么是整张图都被标成了同一个类别。全背景图是否要删除取决于业务定义:如果将来在线检测时会遇到大量没有焊缝的帧,保留一些全背景图有利于降低误检;但如果这些图只是采集时机不对,建议直接剔除。

除了空标签,还要看边缘质量。把 mask 和原图叠加显示,如果发现标注边缘粗糙得像刷子画出来的,训练出的模型边缘也会同样粗糙。对这种图,我更倾向于在标注工具里用多边形修一遍,或者直接把标注质量差的样本从训练集里隔离出来,等后期做半自动标注时再处理。

3. 用语义分割模型训练焊接图像:从数据加载到收敛

标签和目录确认之后,才算真正进入训练阶段。焊接图像和自然场景图不一样,焊道边缘细、缺陷小、背景纹理单一,所以模型选择和数据加载都要针对这个特点来调。

3.1 选模型:U-Net 还是 DeepLabV3+,以及焊接图像为什么不吃大 backbone

焊接图像语义分割的难点不是区分类别,而是不要漏掉小缺陷。常见的语义分割算法里,U-Net 结构对小目标更友好,因为它的跳跃连接能把浅层边缘信息送到深层;DeepLabV3+ 的优势是感受野大,适合轮廓相对规则的场景。如果数据量只有 6k 张,我一般会先用 U-Net 或 DeepLabV3+ 的轻量骨干跑一版,而不是直接上很大的 backbone。

原因很简单:焊接图像的背景结构简单,焊道区域在图像里通常是连续条带,不需要特别深的网络去理解复杂语义。大 backbone 带来的提升往往被背景像素占比拉平,反而把训练时间拖长。选择模型时还要考虑部署环境,如果是工业相机旁边的工控机,推理速度和显存占用比 mIoU 上零点几个点更重要。

3.2 一个能直接改的 Dataset 类:crop、resize 和 mask 插值

训练代码的第一块硬骨头是数据加载。这里必须记住一个原则:图像可以用线性插值,mask 一定不能用线性插值。mask 里的 1、2、3 是类别编号,线性插值会产生 1.7 这种根本不存在的类别。下面这个 Dataset 类是一个可以直接改的模板:

import cv2 import numpy as np import torch from torch.utils.data import Dataset from pathlib import Path class WeldSegDataset(Dataset): def __init__(self, image_dir, label_dir, samples, crop_size=512, aug=False): self.image_dir = Path(image_dir) self.label_dir = Path(label_dir) self.samples = samples self.crop_size = crop_size self.aug = aug def __len__(self): return len(self.samples) def __getitem__(self, idx): sample_id = self.samples[idx] image = cv2.imread(str(self.image_dir / f"{sample_id}.jpg")) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) mask = cv2.imread(str(self.label_dir / f"{sample_id}.png"), cv2.IMREAD_GRAYSCALE) if image.shape[0] < self.crop_size or image.shape[1] < self.crop_size: image = cv2.resize(image, (self.crop_size, self.crop_size), interpolation=cv2.INTER_LINEAR) mask = cv2.resize(mask, (self.crop_size, self.crop_size), interpolation=cv2.INTER_NEAREST) else: h, w = image.shape[:2] top = np.random.randint(0, h - self.crop_size + 1) left = np.random.randint(0, w - self.crop_size + 1) image = image[top:top+self.crop_size, left:left+self.crop_size] mask = mask[top:top+self.crop_size, left:left+self.crop_size] if self.aug: if np.random.rand() < 0.5: image = image[:, ::-1].copy() mask = mask[:, ::-1].copy() image = image * (0.9 + 0.2 * np.random.rand()) image = torch.from_numpy(image.transpose(2, 0, 1)).float() / 255.0 mask = torch.from_numpy(mask).long() return image, mask

这里 crop_size 我一般取 512,焊接缺陷小,切太大反而让背景占比更高;切太小又会把焊道截断。随机水平翻转对大部分焊接件是安全的,但我不会做随机旋转和弹性形变,因为焊缝是刚性几何结构,过度扭曲会让模型学到错误的形态特征。亮度扰动用 0.9 到 1.1 的系数,模拟工业现场光照波动,太强的亮度扰动会掩盖反光缺陷。

3.3 损失函数与训练参数:交叉熵、Dice 和缺陷类别 IoU

焊接语义分割的类别不均衡是必然的。如果不处理,模型会学成“背景预测器”。常见做法是给类别加权,或者把 Dice loss 和交叉熵混合。类别权重的计算可以直接用上一章的统计结果:

class_counts = np.array([1000000, 50000, 3000, 2000]) frequencies = class_counts / class_counts.sum() class_weight = torch.tensor(1.0 / np.sqrt(frequencies), dtype=torch.float) print(class_weight)

这里1 / sqrt(freq)是防止权重差距过于极端。如果直接用1 / freq,背景类的权重可能只有 0.001,小缺陷类权重却超过几百,训练初期梯度会抖动得很厉害。加上 sqrt 之后,背景权重变小、缺陷权重变大,但不会把训练变成缺陷类单独拟合。

训练时可以这样组合损失:

import torch.nn.functional as F ce = F.cross_entropy(logits, mask, weight=class_weight, ignore_index=255) dice = soft_dice_loss(logits, mask, num_classes=4) loss = 0.7 * ce + 0.3 * dice

交叉熵负责让每个像素分类尽可能准确,Dice loss 负责拉高前景和缺陷的重合度。系数 0.7/0.3 是我常用的起点,如果缺陷类 IoU 一直不动,把 Dice 的系数加到 0.4 或 0.5。优化器用 AdamW,初始学习率 1e-4,batch size 8,训练 80 个 epoch。前 30 个 epoch 只看 loss 下降趋势,后 50 个 epoch 要盯着缺陷类别的 IoU,不要只盯整体 mIoU。

4. 焊接图像语义分割的 5 个典型坑:标注、类不平衡与模型失效

即使模型能收敛,焊接图像数据集的坑也远没结束。下面这 5 个问题是我在类似项目里反复踩过的,每条按现象、原因、解决三个部分说清楚。

4.1 焊道反光造成的标签漂移

现象:训练 loss 正常下降,验证 mIoU 也不错,但一到新现场就出问题,模型把焊道边缘的高亮反光区域预测成了气孔。

原因:反光区域在灰度图上表现为高亮的圆形或条状斑块,和真实气孔在视觉上非常像。如果标注人员只是按亮度圈缺陷,反光就会被标进缺陷类,模型实际学到的是“亮度异常=缺陷”,而不是“形态异常=缺陷”。

解决:先把标签里所有反光区域重新检查一遍,把误标为缺陷的反光擦掉,归到背景或单独的反光类。训练时不要做强直方图均衡,因为均衡化会进一步放大反光和暗区对比,让误判更严重。如果现场反光无法避免,可以在数据增强里加随机高光模拟,但前提是标签本身已经干净。

4.2 类别像素极度不平衡:从 loss 曲线看不出类别崩溃

现象:loss 从 1.2 降到 0.3,训练看起来很成功,但单独看缺陷类 IoU 是 0,模型把所有像素都预测成背景。

原因:背景像素占比过高时,交叉熵 loss 会被背景项主导。即使缺陷类 100% 分错,背景类 95% 以上分对,loss 依然在下降。

解决:先打印每个类别的混淆矩阵,不要只看整体 loss。然后在损失函数里加上类权重或 Dice loss。我的习惯是最低类别占比低于 1% 时,强制把该类的 loss 权重调到其它类的 5 到 10 倍,同时用ignore_index=255排除无效区域。

4.3 边缘标注过粗导致 mIoU 虚低

现象:训练到 50 个 epoch 后验证 mIoU 在 0.85 附近卡住,怎么调学习率都上不去。

原因:焊接掩膜的边缘如果标注得很粗,模型学到的边缘和真实边缘之间永远差几个像素。评估阶段 ground truth 边缘也是错的,所以模型预测越接近真实边缘,和错误标签的 IoU 反而越低。

解决:先放大看标签边缘,确认是不是标注问题。如果是,用形态学腐蚀把 mask 边缘向内收缩 1 到 2 个像素,让标签更贴真实边界。更彻底的方案是在标注工具里把粗画笔改成多边形精修,或者用模型预测结果做边缘微调,再人工确认。

4.4 验证集划分不当:同一工件出现在训练和验证

现象:验证 mIoU 高达 0.95,但换到新的焊接件上效果骤降。

原因:焊接图像经常是按视频帧采集的,同一道焊缝的相邻帧几乎一模一样。如果随机切分 train/val,同一个工件会同时出现在两边,模型等于提前见过验证集。

解决:按工件 ID 或采集时间分组切分。比如文件名是part001_frame010.jpg,就以part001作为分组特征,保证同一个工件的所有帧只进训练集或只进验证集。分组切分的代码在下一章给出。

4.5 误把 RGB 图当作灰度标注

现象:训练脚本没报错,但 loss 涨到几千或输出维度对不上,检查时发现 mask 长成了(H, W, 3)。

原因:标签导出时用了彩色 PNG,0、1、2 被存成 RGB 三元组,而不是单通道类别值。

解决:加载 mask 前先打印mode。如果是 RGB,优先回到标注工具重新导出;如果实在没办法,只能写映射表把 RGB 颜色还原成类别 id,但整个过程非常痛苦。所以拿到数据集的第一件事,永远是用上一章的脚本检查 mask 的 mode,这个习惯能省下一整天的排查时间。

5. 让 6k 数据发挥更大价值的三个方向:分组划分、后处理与半自动标注

如果前面的热身都做完了,模型也已经能跑通,接下来才是把 6k 张图真正变成可用资产的部分。数据质量比数量重要,但好的方法可以让质量一般的 6k 张图翻倍。

5.1 按工件分组划分 train/val/test:别让同一道焊缝两边都出现

焊接视频帧之间存在极强的时序相关性,相邻两帧可能只差一个采集角度。随机切分会让验证集失去意义。用 GroupShuffleSplit 可以按组切分:

from sklearn.model_selection import GroupShuffleSplit samples = ["part001_frame001.jpg", "part001_frame002.jpg", "part002_frame001.jpg"] groups = [name.split("_frame")[0] for name in samples] gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(samples, groups=groups)) train_samples = [samples[i] for i in train_idx] val_samples = [samples[i] for i in val_idx]

这里groups是每个样本所属的工件编号。如果没有现成的工件 ID,可以按文件名前缀或采集时间窗口来构造。test_size 我一般取 0.2,但焊接数据里如果某个工件的缺陷形态特别特殊,把它整组放进测试集就会导致测试指标异常,所以最好再单独留一个包含典型缺陷的固定测试集。

5.2 后处理:连通域过滤比直接调阈值更稳

模型输出的是每个像素的类别概率,直接取 argmax 会得到很多孤立噪声点。尤其是气孔这类小目标,模型经常在焊道中间吐出一连串零散小点。常见做法是加连通域过滤:

import cv2 import numpy as np def remove_small_components(pred_mask, min_area=30): num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(pred_mask, 8) clean = np.zeros_like(pred_mask) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] >= min_area: clean[labels == i] = 1 return clean

min_area是像素面积阈值,需要根据训练时的输入尺寸调整。如果模型输入是 512x512,气孔在图上可能只有 10 到 30 个像素,min_area 建议不超过 20;如果是检测焊瘤或大块飞溅,阈值可以放到 100 以上。这个后处理最大的坑是误删小缺陷,所以上线前一定要可视化检查被过滤掉的区域。

CRF 后处理也能让边界更干净,但推理速度会明显变慢。工业现场如果对单帧延迟有要求,我一般不用 CRF,而是用连通域过滤加边缘平滑。实际效果和 CRF 差距不大,速度却快得多。

5.3 半自动标注:先用干净数据训练,再反推质量差的标签

6k 张图里如果只有 4k 张标注得干净,剩下 2k 张边缘很糙,不要直接删掉。更高效的做法是先用 4k 干净图训练一个初版模型,再对剩余图像做预测,让标注员只修标签,而不是从零画。

挑选哪些图值得修,可以用预测熵来决定。熵越高,说明模型对这个区域越不确定,越可能是标注本身有问题:

import torch def uncertainty_score(model, images): logits = model(images) prob = torch.softmax(logits, dim=1) entropy = -(prob * torch.log(prob + 1e-6)).sum(dim=1) return entropy.mean(dim=(1, 2)).cpu().numpy()

把每张图的平均熵从高到低排序,优先修熵最高的一批。这个流程本质上是把人工标注资源用到模型最不确定的地方。对焊接缺陷这类小目标,模型不确定的区域往往集中在缺陷边界和反光区域,正好是标注最容易出错的位置。

6. 上线前的验证与模型迭代:用 6k 图换一个可用模型

训练完成不等于可以上线。焊接图像分割模型落到现场之前,我会先用一个“现场视角”的验证集做检查,这个集和 train/val 的划分方式完全不同。现场样本应该包含不同光照、不同焊接速度、不同工件批次,而不是从原数据集里再抽一部分相似帧。如果现场数据没有标签,就临时标记 30 到 50 张,只用来做通过性测试。

验证时先跑整体指标,再看三类细节:缺陷有没有漏报、边缘有没有锯齿、反光区域有没有误报。不要只看 mIoU,焊接质检更关心漏报率。缺陷漏掉了,mIoU 再高也没意义;边缘糙一点反而可以通过后处理修。所以我会单独算每个缺陷类别的 recall,如果 recall 低于业务要求,先把后处理阈值调低,再考虑重训。

模型迭代时一次只改一个变量。要么调损失系数,要么调数据增强,要么换 backbone,不要同时全改。我自己的习惯是:任何一次改动,先看缺陷类别的 IoU,再看整体 mIoU;如果缺陷 IoU 没有变化,就回到数据层面检查标签。这个习惯帮我挡掉过很多次看起来 mIoU 很高、实际没法用的模型。希望帮到你。

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

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

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

立即咨询