1. 为什么必须用深度学习方法做焊接缺陷检测——传统算法的边界在这里焊接缺陷检测系统听起来像是一个很传统的工业质检话题但真正用Python把深度学习方案完整落地时你会发现这里面有大量容易被忽略的细节。我最近做一个模拟项目X任务是针对射线检测图像中的气孔、夹渣、未焊透、裂纹等缺陷做自动定位与分类要求不仅能画框还要能统计缺陷数量与尺寸最后为等级判定提供依据。项目整体采用基于深度学习的方案目前已经跑通一套可复现的Python实现。如果你正在做类似的课程设计、实验室原型或者打算在企业里搭一套焊缝质检算法验证环境这篇文章应该能省掉不少弯路。先摆结论焊缝X光图像里的缺陷检测深度学习能做的事远比传统机器视觉要多但也不是把模型丢进去就能出结果。数据口径、标注尺度、模型选型、后处理逻辑每一步都有坑。如果不提前想清楚这些东西最后得到的很可能是一个“论文里很漂亮、现场用不了”的Demo。1.1 焊接缺陷检测到底在检什么射线探伤RT得到的数字化图像中焊缝内部的缺陷会以不同的灰度形态呈现。气孔在图像里通常是圆形或近圆形的暗斑中心灰度变化明显直径往往很小。夹渣是不规则块状亮度不均匀有些比背景暗、有些比背景亮。未焊透和未熔合一般表现为沿坡口边界分布的连续线状暗条比较粗。裂纹是最麻烦的一类它细长、低对比度、端部尖锐经常藏在焊缝余高的灰度起伏里肉眼不仔细看都容易漏掉更别说让传统阈值算法自动去找。这些缺陷对结构安全的影响完全不同。裂纹和未焊透往往必须返工气孔如果是个别的、尺寸不超标可能还在允许范围内但成串气孔也要处理。工业上通常依据检测标准来评级所以检测系统最终要输出的不只是“有没有缺陷”而是缺陷的位置、类型、数量、尺寸以及是否越过标准线。这个目标直接决定了后面的任务设计单纯画个框不够还要能测面积、测长度、统计密集度。1.2 传统图像处理方案的局限在哪早些年做这类检测最常见的套路是阈值分割加形态学处理。先做固定阈值或者自适应阈值把明显的暗区提取出来再用开运算、闭运算把碎噪声滤掉接着根据面积、圆度、长宽比、与坡口边缘的距离这些手工规则判断每个区域是气孔、夹渣还是伪缺陷。这套做法在实验室里很顺放到生产现场就容易翻车。焊缝余高的灰度变化、底片本身的颗粒噪声、机械磨痕、甚至工件表面轻微锈迹都可能被当成缺陷。为了压制误检就得不停加规则面积多大的不要、圆度不好不要、离坡口太远的不要。问题是规则本来就是基于有限样本拍脑袋定的换一种焊材、换一条产线、调整一下电压电流灰度特征变了老规则就要重新标定。我见过一个项目组花了大量时间在写这种特征规则最后换了个曝光参数整套逻辑基本失效。深度学习不太一样。它不需要人工预定义“像缺陷”的显式特征而是通过大量标注样本自己学习“看起来像气孔”“看起来像裂纹”这类语义表达。输入图像、输出缺陷区域中间的特征提取过程是自动学出来的。对低对比度、形态多变、背景干扰强的焊缝X光图来说这种方案比手工规则鲁棒得多。1.3 深度学习方案的整体流程基于深度学习的焊接缺陷检测整个流程可以拆成五步。第一步是图像采集与数字化典型来源是DR设备直接输出数字图像或者把传统胶片底片扫描成数字图。第二步是预处理包括灰度归一化、对比度增强、尺寸统一必要时还要做ROI裁剪把焊缝区域单独切出来。第三步是模型推理这也是核心常见做法是目标检测模型或者语义分割模型。第四步是后处理对模型输出做阈值化、连通域分析、面积筛选得到最终缺陷列表。第五步是数据组织与输出把结果写进数据库、生成可视化标注图、推送告警信号给产线执行机构。整个链路里模型结构是最容易被讨论的部分但真正决定系统能不能用的往往是前后两端数据是否覆盖工艺变化、后处理是否能输出业务需要的信息。下面我按这个顺序把每个环节的具体做法和踩坑记录都展开。2. 数据是第一个真正的门槛缺陷数据的收集、标注与预处理很多人以为项目核心是模型实际做下来才会发现数据才是最先卡住你的地方。焊接X光图像不像自然图像那么好找开源数据本来就少标注口径也乱直接用很容易得出自欺欺人的结果。2.1 数据来源公开数据、现场数据与仿真数据网上能下载到的焊接射线检测数据集一般几百张到几千张主要覆盖气孔、夹渣、裂纹这几个常见类别未焊透和未熔合有时候标得不够细。这类公开数据可以拿来验证算法思路证明“这条路能跑通”但我个人不建议把它直接当作品验收指标。原因很简单公开数据的成像条件、工件材质、工艺参数你没得选模型在里面学到的灰度特征未必适配你真实要检测的生产件。最能说明问题的是自建数据。准备一批有缺陷和没缺陷的焊接试板用DR设备拍片再组织人员标注。成本主要花在焊样制作和设备占用上但数据越贴近真实工艺模型落地越顺利。项目早期如果拿不到大量现场数据我建议至少先收集几十块不同工艺、不同板厚、不同曝光参数的小规模样本确定检测任务可解再考虑规模化采集。仿真与增强是一条性价比很高的补充路径。把已有的真实缺陷图做旋转、裁剪、缩放、亮度扰动、叠加噪声可以生成大量形态相似但不完全相同的样本。需要记住一点增强只能模拟“同一类缺陷在不同拍摄条件下的样子”不能创造真实世界里没有的缺陷结构。像裂纹这种细长低对比目标增强时会牵涉到尺度和方向操作起来要比气孔更谨慎。2.2 标注标准缺陷类型决定了任务粒度焊接缺陷标注的颗粒度直接决定了任务形式。如果只想判断“这一块有没有问题”用矩形框标注就够了目标检测模型训练起来快标注成本也低。如果要做等级判定、面积统计、长度测量矩形框是不够的因为框里包含大量正常纹理无法准确计算缺陷的物理尺寸。这时应该用像素级掩膜Mask标注让模型预测每个像素是否属于缺陷。不同缺陷的标注策略可以参考下面这张表缺陷类型典型外观标注建议气孔圆形或近圆形暗斑尺寸小可用分割掩膜便于统计直径和数量夹渣不规则块状灰度不确定分割掩膜重点统计面积与密集度未焊透沿焊缝根部的连续线状暗条分割掩膜关注连续性和长度裂纹细长、低对比、端部尖锐分割掩膜小目标需放大标注实操时建议第一轮先用矩形框做快速验证确定整体精度趋势没问题再补充像素级标注做定版系统。因为像素级标注的成本是矩形框的好几倍等到算法都跑不通的时候再投入容易浪费标注资源。标注工作至少要两个人独立标一遍再合并结果统一边界规则。特别是灰度介于背景和缺陷之间的边缘区域人和人的判断差异很大不定好规则模型学出来的边界就很飘。2.3 预处理、增强与类不平衡处理X光图像作为输入一般先做灰度归一化把像素范围缩放到0到1之间。要不要做CLAHE局部对比度增强我的经验是“对大多数焊缝图像有正收益”因为缺陷区域和背景灰度太接近把局部对比度拉开模型会更容易收敛。但增强强度要控制好太强会突出融合噪声反而增加误检。增强策略不能无脑套自然图像那套。焊接X光图的翻转方向要看着办如果产线工艺方向固定水平翻转通常安全垂直翻转要看你拍的是横焊还是立焊焊道方向变了缺陷形态的空间关系也会变。灰度扰动和对比度扰动是性价比最高的增强方式一张原图可以模拟多种曝光条件。类不平衡是焊接缺陷分割最大的敌人。一张512乘512的图上背景区域经常占99%以上如果只是普通二分类交叉熵损失模型很容易学会“全部预测为背景”因为这样损失已经很小。应对办法有三个方向损失函数层面用加权交叉熵、Dice Loss或Focal Loss数据层面把小缺陷区域裁剪出来放大作为额外训练样本评估层面单独统计缺陷像素上的召回率不要被总体精度迷惑。2.4 到底需要多少数据这个问题没有标准答案我只能给一个比较实的参考区间。几百张清晰标注图就能把模型训起来足够写论文、做课程设计展示。但要达到产线可用的稳定检测至少需要数千张图像而且必须覆盖不同材质、不同板厚、不同曝光参数、不同工艺状态。我在模拟项目里做过对比只用公开数据训练检测率到一定程度就上不去了每加入一批新工艺数据召回率都会有可见的提升。数据量不是线性的但“现场数据比公开数据重要”这一点始终成立。3. 检测模型选型目标检测与分割两条路线怎么选数据问题理清楚之后模型选型反而没那么纠结。当前主流路线就是两条目标检测和语义分割。两者解决的问题有重叠但输出能力和适用场景不同选错了后面会很别扭。3.1 目标检测路线实时选YOLO精度选两阶段目标检测路线把每个缺陷当成一个目标模型输出“缺陷类别置信度矩形框坐标”。单阶段模型以YOLO系列为代表推理速度快工程生态完善适合在线场景配合数据增强和小目标专门策略能在焊缝X光图上取得不错效果。两阶段模型Faster R-CNN这类精度上限高一些但推理速度吃亏对产线实时检测不太友好。用目标检测做焊缝缺陷时最大的问题是小目标。气孔这一类缺陷只有几个像素大小深度网络下采样几轮之后特征图上的信息几乎丢光。单纯堆基础检测头是不够的需要特征金字塔这类多尺度融合结构并且训练时对包含小缺陷的区域做裁剪放大。另外成串气孔落在同一个矩形框里时框内同时包含多个独立缺陷这对后续按点数评级非常不利。如果你知道业务上需要“数缺陷个数”用目标检测框就得专门处理聚团缺陷的拆分问题。3.2 分割路线统计面积和形态时才不可替代语义分割路线的代表是U-Net及其各种变体模型给每个像素输出一个缺陷概率再做阈值化得到掩膜。分割方案的优势在于细长裂纹不会因为矩形框而引入大量背景噪声召回更准确每个缺陷的像素面积、等效直径、长宽比、密集度都可以从掩膜上直接统计出来如果检测标准对缺陷数量和大小有明确的等级限制分割输出几乎是必须的。代价是标注成本高训练和推理速度比轻量目标检测模型要慢一些。但焊缝场景通常没有自动驾驶那么极端单张图推理就算慢个几百毫秒很多工位的节拍也能接受。后处理方面分割输出做连通域分析之后每个连通域的位置、面积、包围盒全部能拿到其实一点也不麻烦。路线输出形式标注成本推理速度主要适用场景目标检测类别矩形框低高实时筛查、批量判片、缺陷定位语义分割逐像素掩膜高中缺陷面积统计、长度测量、等级判定3.3 我的选型思路与路线切换过程我在模拟项目初期先用了目标检测方案做快速验证因为标注成框便宜可以先把算法可行性摸清楚。跑下来发现细长裂纹的召回是最弱的矩形框本身对“细长低对比”目标很吃亏而且后续统计面积时还要从框里把背景像素剥离出来很别扭。后来切换到分割路线用U-Net类模型直接预测像素掩膜再通过连通域分析得到每个缺陷的坐标、面积和包围盒整体流程反而更顺。所以我的建议是如果业务最终要的是“有没有缺陷、在哪个位置”目标检测足够如果业务要的是“缺陷面积多大、长度多长、数量多少、要不要返修”直接上分割不要绕路。如果实在两者都想要也以分割为主线分割结果里的连通域信息完全够你生成矩形框不用单独再训一个检测模型。4. Python核心实现从数据集封装到训练评估全流程选型确定之后就到了动手写代码的阶段。下面我给出一个精简但完整的Python实现路径覆盖数据加载、模型定义、损失函数、训练循环、推理后处理。代码以PyTorch、OpenCV、NumPy为主要依赖突出核心逻辑直接改路径就能跑通原型。4.1 工程目录与依赖项目结构建议按下面这样组织简单清晰后期扩展也方便weld_defect/ ├─ data/ │ ├─ images/ │ └─ masks/ ├─ models/ │ └─ unet_like.py ├─ utils/ │ ├─ dataset.py │ ├─ loss.py │ └─ postprocess.py ├─ train.py └─ infer.py依赖方面需要安装PyTorch、OpenCV、NumPy、tqdm。数据加载用OpenCV读图训练用PyTorch训练后处理用OpenCV的连通域函数这套组合在焊接图像场景下非常顺手。4.2 数据加载与标签处理数据加载很容易踩坑的点是掩膜读取方式。分割掩膜必须用灰度图读取然后在代码里转成0和1的二值标签。resize的时候输入图像可以使用双线性插值掩膜必须使用最近邻插值否则标签边界会因为插值产生假像素导致模型学出模糊边界。import cv2 import numpy as np import torch from torch.utils.data import Dataset from pathlib import Path class WeldDefectDataset(Dataset): def __init__(self, img_dir, mask_dir, size512, augmentFalse): self.img_dir Path(img_dir) self.mask_dir Path(mask_dir) self.size size self.augment augment self.names [p.stem for p in self.img_dir.glob(*.png)] def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img cv2.imread(str(self.img_dir / f{name}.png), cv2.IMREAD_GRAYSCALE) mask cv2.imread(str(self.mask_dir / f{name}.png), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (self.size, self.size)) mask cv2.resize(mask, (self.size, self.size), interpolationcv2.INTER_NEAREST) img img.astype(np.float32) / 255.0 img np.expand_dims(img, axis0) mask (mask 127).astype(np.float32) if self.augment: if np.random.rand() 0.5: img np.flip(img, axis2).copy() mask np.flip(mask, axis1).copy() img * 1.0 0.1 * np.random.randn() return torch.from_numpy(img).float(), torch.from_numpy(mask).float()注意增强里翻转对象是二维的灰度图通道维度上不做处理因为输入只有单通道。4.3 简化分割模型与损失函数一个完整的U-Net对博客来说太长我保留最核心的编码器、解码器、跳跃连接结构做成一个简化版足够说明原理。完整U-Net只是把这个结构加深、加宽每个阶段多加一层卷积而已。import torch import torch.nn as nn class ConvBlock(nn.Module): def __init__(self, in_c, out_c): super().__init__() self.block nn.Sequential( nn.Conv2d(in_c, out_c, kernel_size3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), nn.Conv2d(out_c, out_c, kernel_size3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.block(x) class UNetSimple(nn.Module): def __init__(self, in_ch1, out_ch1, base32): super().__init__() self.enc1 ConvBlock(in_ch, base) self.pool nn.MaxPool2d(2) self.enc2 ConvBlock(base, base * 2) self.enc3 ConvBlock(base * 2, base * 4) self.up1 nn.ConvTranspose2d(base * 4, base * 2, kernel_size2, stride2) self.dec1 ConvBlock(base * 4, base * 2) self.up2 nn.ConvTranspose2d(base * 2, base, kernel_size2, stride2) self.dec2 ConvBlock(base * 2, base) self.out nn.Conv2d(base, out_ch, kernel_size1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) d1 torch.cat([self.up1(e3), e2], dim1) d1 self.dec1(d1) d2 torch.cat([self.up2(d1), e1], dim1) d2 self.dec2(d2) return torch.sigmoid(self.out(d2))损失函数直接用二元交叉熵加Dice Loss的组合这是分割场景里非常稳定的搭配。Dice Loss专门处理缺陷小、背景大的不平衡问题。class BCEDiceLoss(nn.Module): def __init__(self): super().__init__() def forward(self, pred, mask): bce nn.functional.binary_cross_entropy(pred, mask) smooth 1e-6 inter (pred * mask).sum() dice 1 - (2 * inter smooth) / (pred.sum() mask.sum() smooth) return bce dice4.4 训练循环与验证训练循环本身不复杂重点是验证阶段要保存最优模型。焊接缺陷这种极不平衡任务训练损失下降很快但验证Dice涨得很慢不要看前几个epoch损失低就高兴太早。from torch.utils.data import DataLoader from tqdm import tqdm import torch.optim as optim dataset WeldDefectDataset(data/images, data/masks, size512, augmentTrue) train_loader DataLoader(dataset, batch_size8, shuffleTrue, num_workers2) model UNetSimple(in_ch1, out_ch1, base32) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) loss_fn BCEDiceLoss() for epoch in range(50): model.train() total_loss 0.0 for img, mask in tqdm(train_loader): img, mask img.to(device), mask.to(device) pred model(img) loss loss_fn(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() scheduler.step() avg_loss total_loss / len(train_loader) print(fEpoch {epoch 1}/50, Loss: {avg_loss:.4f})训练完成后单独验证每个类别的Dice和召回率不能只看平均数值。4.5 推理、连通域分析与结果输出推理阶段关键是后处理。模型输出的是概率图需要阈值化然后做连通域分析把每个缺陷当成一个独立对象处理。def inference(model, img_path, size512, threshold0.5, min_area100): model.eval() img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) original_size img.shape[:2] img_resized cv2.resize(img, (size, size)).astype(np.float32) / 255.0 img_tensor torch.from_numpy(np.expand_dims(img_resized, axis0)).unsqueeze(0).float() with torch.no_grad(): pred model(img_tensor.to(device)) pred pred.cpu().numpy()[0, 0] pred cv2.resize(pred, original_size[::-1], interpolationcv2.INTER_LINEAR) binary (pred threshold).astype(np.uint8) num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(binary, connectivity8) result_boxes [] for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] if area min_area: continue x, y, w, h stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] result_boxes.append({bbox: [x, y, w, h], area_pixel: area, center: centroids[i]}) return result_boxes这段代码把预测概率图放大回原图尺寸再做阈值化保证输出标注和原图对齐。如果现场图非常大建议先把焊缝ROI区域切出来只在ROI上做推理既能提速又能减少背景干扰。5. 训练与调参中的真实踩坑记录代码跑通只是第一步真正折磨人的是训练和调参阶段的那些奇怪现象。下面这些坑是我在模拟项目里真实撞上的每个都值得提前排雷。5.1 小目标气孔漏检第一次训练完看验证集整体Dice还挺好看但把预测可视化出来发现漏掉的基本全是小气孔。这类缺陷只有十几个像素几次下采样之后特征贡献几乎消失。解决思路要分两层。第一层是模型结构使用特征金字塔式的多尺度融合让浅层的高分辨率特征参与最终预测。第二层是训练数据把小缺陷区域裁剪出来放大单独作为一批样本加入训练。我在项目中做了个小工具跑一个简单的Blob检测找出小缺陷的位置然后把这些位置周围的128乘128窗口额外保存为增强样本下一次训练时漏检明显减少。5.2 背景占比过高导致的“全不检”如果训练日志里损失还在下降但测试集上预测掩膜全是黑色那多半是模型学会了“全输出背景”。原因前面说过缺陷像素占比太低普通交叉熵损失被背景主导。我的排查顺序是先统计训练集中缺陷像素的平均占比如果低于1%直接把损失函数换成BCE加Dice然后看阳性样本上的召回率而不是只看全局Dice。换完损失函数后模型才真正开始学习缺陷区域的特征。Focal Loss也可以但参数要调我就是先从Dice Loss入手旁边保一个BCE项稳定训练效果已经够用。5.3 只看准确率会被骗在焊接缺陷这种极度不平衡的场景里准确率是一个毫无意义的指标。一张正常焊缝图缺陷像素为0模型只要输出全背景准确率就是99%往上。但如果产线每天拍几千张图哪怕有1%的正常图被误检成缺陷每天的误报量也很夸张现场人员很快就不信这个系统了。对应的做法是每次迭代都记录三个数缺陷召回率、背景误检率、以及按连通域统计后的“块级”误报数量。只在像素级别评估还不够因为几个噪点连成的小区域在像素级可能只影响0.1%的Dice但形成连通域之后就是一个实实在在的误报缺陷块。所以必须把评估放在业务单位上也就是“检出了几个真缺陷”“误报了几个假缺陷”。5.4 推理输入尺寸与滑窗拼接的坑训练时图像resize到512乘512推理时如果直接拿原图resize小目标信息会被压缩性能明显下降如果直接输入原图模型会因为尺寸分布不一致而退化。我在项目里遇到过类似问题把推理改成滑窗之后就解决了。做法是把原图切成若干重叠Patch每个Patch按训练时的尺寸输入模型推理完再把概率图拼回原位。重叠区可以用平均融合减少拼缝处的跳变。重叠比例不建议太大10%到20%足够。这个方案对小气孔和细长裂纹都有好处因为局部裁切能保留更多细节。代价是推理总耗时增加但焊缝检测场景通常不是毫秒级可接受。6. 系统落地时可以继续做的几件事原型能跑通之后离真正的产线可部署还有距离。下面这几个方向是我在项目后期验证过的按优先级从高到低排列。6.1 ONNX导出与轻量化部署训练完的PyTorch模型直接给产线用在环境和效率上都不合适。先导出成ONNX格式再交给运行时推理引擎处理是常见做法。焊接缺陷这种低对比小目标场景量化要谨慎。FP16基本无损INT8的精度损失在细小裂纹上会比较明显。我建议先跑FP16保证效果INT8要等场地样本稳定后再做。dummy torch.randn(1, 1, 512, 512, devicedevice) torch.onnx.export( model, dummy, weld_defect.onnx, input_names[input], output_names[output], opset_version11 ) print(ONNX export done.)导出完成后用ONNX Runtime加载测试一遍确认输出和PyTorch版本一致再做部署。6.2 在线产线的触发、缓存与告警链路在线检测场景和离线分析是两回事。产线来的是一段连续图像流不能傻乎乎每帧都推理。通常的做法是传感器检测到工件到位后触发采集当前ROI图像放进队列GPU推理线程从队列取出图像处理结果写入数据库并关联工件条码如果判定为不合格触发声光告警同时保存可疑原图和模型结果图。这个缓冲设计最关键的是不能丢图尤其是缺陷图。我见过因为推理速度跟不上简单地把队列里的旧图像丢掉的方案最后等于把最容易出问题的那批数据全扔了。宁可在告警侧加延迟也不要随意丢帧。6.3 错分样本回注与增量训练我的项目里最有价值的操作是每跑完一批现场数据就把漏检和误检的图像挑出来重新标注再追加到训练集中做增量训练。增量训练时学习率要调低大概是原来的十分之一防止新样本把旧知识冲掉。每次追加的样本量控制在1000张左右跑20到30个epoch效果提升通常很稳定。第一批错分样本回注带来的收益最大越往后越趋近于饱和。跑完回注之后最好单独保存一个固定评估集全程不参与训练确保每次模型变化不会产生回归。不要因为某次回注让旧场景召回率下降而不自知这是我吃过亏后养成的习惯。最后再分享一个体会焊接缺陷检测这个项目技术本身不是最大的难点难的是你愿不愿意把时间花在数据、标准和反复验证上。模型结构大家都能看懂最后拉开差距的往往是这些“脏活累活”。但恰恰是这些脏活能把系统从“论文Demo”变成“车间里真正能用的工具”。