焊接质量这事,干过结构件、压力容器或者汽车零部件的人心里都清楚,焊缝表面那几道纹路底下藏着多少讲究。以前老师傅拿着放大镜、靠着经验看焊缝,现在产线上恨不得每一道弧光都数字化。我去年接手了一个焊接缺陷检测系统的模拟项目X,核心任务就是基于深度学习,把焊接缺陷识别从"人眼盯防"往"算法自动判别"上推一把,代码用Python实现。这段时间折腾下来,从数据集标注到模型落地的全套链路都走了一遍,踩了不少坑,也攒了些干货,今天就完整复盘一下这个项目,给打算上手类似方向的朋友做个参考。
1. 项目整体拆解:焊接缺陷检测到底在解决什么问题
1.1 传统检测手段的痛点和深度学习的切入点
先说背景。焊接缺陷检测在工业现场一直是刚需,常见的缺陷类型包括气孔、夹渣、未熔合、未焊透、裂纹、咬边等等。传统做法基本分两类:一类是人工目视检测,靠质检员拿着标准图谱对照焊缝成形,效率低不说,漏检率还跟疲劳程度直接挂钩;另一类是基于规则的机器视觉方案,用阈值分割、边缘检测之类的传统图像处理方法去提取缺陷特征,这类方案对光照变化、焊接飞溅、工件表面反光特别敏感,换个工况就得重新调参数,推广成本很高。
深度学习切入这个场景的逻辑其实很直白:把缺陷检测当成一个目标检测或者图像分割任务,用卷积神经网络自动提特征,把"什么位置有什么类型的缺陷"直接学出来。相比传统方法,深度模型的泛化能力要强很多,同一套框架在不同光照、不同材质的焊缝图像上往往都能有不错的表现。这个模拟项目X最终选用的路线就是经典的"检测头+分类头"架构,具体来说就是用YOLO系列的检测框架去框出缺陷位置并给出类别,兼顾速度和精度。
1.2 系统整体架构与核心功能模块
整个系统按照工业落地的思路分成四个模块:图像采集与预处理、缺陷检测模型、结果后处理与可视化、模型管理接口。图像采集这块项目里用的是公开的焊接缺陷数据集做模拟,但架构上预留了接工业相机和光源控制的接口。缺陷检测模型是核心,用Python的深度学习框架实现,训练和推理我都做了封装。结果后处理负责把模型输出的检测框、置信度、类别信息转成质检报告需要的格式,同时支持把检测结果标注在原图上输出。模型管理接口主要做了模型权重保存、加载和推理脚本的独立化,方便后续接Web服务或者桌面端。
选择这个架构的时候主要考虑了三点。第一,模块之间尽量解耦,训练代码和推理代码分开,避免后面换数据集或者换模型结构的时候牵一发动全身。第二,推理链路上尽量减少不必要的依赖,生产环境里不需要训练框架那套复杂的计算图,只需要前向传播。第三,结果输出格式对齐工业常见的JSON和图片标注,方便跟现有的MES系统对接。
项目结构(模拟项目X) ├── data │ ├── raw_images # 原始焊缝图像 │ └── annotations # 标注文件(YOLO格式txt / COCO格式json) ├── configs │ └── train_config.yaml # 训练超参数配置 ├── models │ └── detector.py # 检测模型定义 ├── utils │ ├── dataset.py # 数据加载与增强 │ ├── metrics.py # mAP、Precision、Recall计算 │ └── visualization.py # 结果可视化 ├── train.py # 训练入口脚本 ├── infer.py # 推理入口脚本 └── requirements.txt1.3 为什么选择YOLO系列:速度与精度的平衡
焊接缺陷检测这个场景有一个比较特殊的约束:产线上往往是连续拍摄,一张构件可能几十道焊缝,每道焊缝又拍好几张图,检测速度如果跟不上就只能抽检,抽检就有漏网风险。所以在算法选型阶段,我把目标锁定在单阶段检测器上,YOLO系列自然就成了首选。两阶段的Faster R-CNN虽然精度上限可能更高,但推理速度在CPU或低端GPU上实在撑不起实时流水线。
当时我对比了YOLOv5、YOLOv8和RT-DETR这几个方案。YOLOv8在工程性上做得最好,训练脚本、导出脚本、文档都齐全,而且它的C2f结构和Anchor-Free检测头在小目标缺陷上的表现比v5要稳一些。RT-DETR虽然有Transformer的全局建模能力,但训练收敛对数据量要求更高,模拟项目X的数据量只有几千张图,用RT-DETR容易过拟合。最终选了YOLOv8n作为基线模型,参数量最小,推理速度快,后面如果精度不够再往YOLOv8s升级也方便。
2. 核心准备环节:焊接缺陷数据集的采集、标注与增强
2.1 焊接缺陷类型的界定与标注规范
数据是深度学习的粮食,焊接缺陷检测更是如此。拿到项目的第一件事不是写模型,而是把缺陷类型定义清楚。我参考了国标和行业常见的缺陷分类,把模拟项目X的检测目标定义成六类:气孔、夹渣、未熔合、未焊透、裂纹、咬边。这里面有个细节,不同类型缺陷的形态差异非常大:气孔是小圆点,夹渣是不规则块状,裂纹是细长线条,未熔合和未焊透则是沿焊缝方向的条状区域。这些形态差异直接影响到标注框的尺寸分布,也决定了后面锚框或者标签分配策略要怎么调。
标注规范上我踩过一次坑。一开始标注的时候没有统一规则,有人沿着缺陷边缘仔细描,有人直接拉一个大框把整段焊缝框进去,导致同一个缺陷的标注框面积差好几倍。后来定了一条硬规矩:标注框必须紧贴缺陷可见区域的最大外接矩形,并且类别判定以缺陷最显著的特征为准。比如一块区域既有气孔又有夹渣,要看哪种缺陷占据主导面积,不许一个目标打两个标签。这些规则看起来琐碎,但对最终模型性能的影响比想象中大得多。
2.2 数据集规模、划分策略和标签分布处理
模拟项目X最终整理出大约2800张标注好的焊缝图像,每张图上缺陷数量从0到8个不等。这个规模在工业视觉里算比较小的,所以数据划分格外讲究。训练集、验证集、测试集按7:2:1划分,但划分之前先按图像来源做了分组,保证同一个焊缝的不同照片不会同时出现在训练集和测试集里,否则会引入数据泄露,评估结果虚高。
标签分布方面,气孔和夹渣这两类出现频率最高,占了大约60%,裂纹和咬边占比最小,总共不到10%。这种长尾分布直接训练的话,模型会对小样本类别严重不敏感。我试过两种处理方式:一是对少样本类别做过采样,复制样本图像(配合增强变换)放进训练集;二是在损失函数里给少样本类别加权重。实际测试下来,过采样的效果更直接,因为焊接缺陷的图像特征相对固定,复制样本不会像自然图像那样容易导致过拟合。最后少样本类别的召回率从不到50%提升到了78%左右。
2.3 数据增强策略:针对焊接图像特点的定制化方案
通用目标检测里的数据增强三板斧是翻转、缩放、颜色抖动,但直接搬到焊接图像上会出问题。焊接图像有很明显的工业特征:暗背景、高亮弧光区域、焊缝纹理方向性强。我调了一套适合这个场景的增强组合:
- 几何增强:随机水平翻转、随机旋转(±15度以内),不做垂直翻转,因为焊缝方向在垂直翻转后文本标注和纹理方向会错乱。
- 光学增强:亮度对比度微调、高斯噪声、少量运动模糊,模拟相机曝光不稳定和产线震动的情况。
- 特殊增强:随机裁剪焊缝区域并放缩,强制模型学习局部细节;Mosaic增强只在训练后期降低概率使用,前期用太多会让小缺陷被过度缩小。
这套增强策略在验证集上大概提升了4个点的mAP。不过要注意,增强参数不能拍脑袋定,我建议先用一小批数据跑几个快速实验,观察增强前后模型在验证集上的表现再最终确定。Mosaic增强尤其要小心,焊接图像里小缺陷很多,四个图拼一起之后缺陷尺寸会被压缩,如果模型本身对小目标就不敏感,效果反而会变差。
3. 模型选择与训练细节:从YOLOv8结构解析到超参调优
3.1 YOLOv8网络结构与焊接缺陷检测的适配点
YOLOv8的结构可以拆成三块:Backbone负责提特征,Neck负责多尺度融合,Head负责最终的分类和回归。Backbone用的是CSPDarknet的改进版,核心模块是C2f结构,相比之前的C3模块多了更多的梯度流分支,特征复用能力更强。Neck部分延续了FPN+PAN的结构,自顶向下传语义信息,自底向上传空间信息,这样对不同尺寸的缺陷都能有响应。Head部分从Anchor-Based换成了Anchor-Free,直接预测目标中心点和宽高,省去了聚类锚框的过程,对缺陷这种尺寸分布不规则的场景反而更友好。
焊接缺陷里最怕的是两类目标:一类是细长的裂纹,宽可能只有两三个像素,长却有几十个像素;另一类是很小的气孔,面积占比可能不到整张图的千分之一。YOLOv8的Anchor-Free设计让模型可以直接回归任意长宽比的目标,比固定锚框的方式灵活很多。配合多尺度特征图(P3、P4、P5),小目标在P3层(大特征图)上有更多像素可以学习,这样气孔这类小缺陷就不容易丢。
3.2 训练超参数配置与关键计算
训练配置我用了一张消费级GPU(12GB显存),batch size设为16,输入分辨率640×640,初始学习率0.01,使用SGD优化器搭配余弦退火调度。训练总轮数200轮,但实际在第120轮左右验证集mAP就基本不再涨了,后面主要是让损失曲线进一步平稳。
这里有个必看的参数:学习率。YOLOv8默认的初始学习率是按ImageNet预训练权重设计的,用在焊接图像这种小数据集上偏大。我尝试过直接训,前几个epoch损失直接爆炸,梯度更新幅度太大导致预训练权重被破坏。后来把初始学习率降到0.005,warmup轮数从默认的3轮提到5轮,让模型先在低学习率下适应工业图像的分布,再慢慢加快更新步伐。
还有个容易被忽略的参数是epoch的batch数量。数据量2800张、batch size 16,每个epoch大概175个batch。warmup设5轮意味着前875个batch都在热身,这个比例对迁移学习是合理的。如果数据量更小,比如只有1000张,warmup轮数要降到2-3轮,不然模型预热时间太长,反而浪费训练资源。
3.3 损失函数构成与训练过程监控
YOLOv8的损失函数包括三部分:分类损失(BCE)、定位损失(CIoU)和分布聚焦损失(DFL)。分类损失评估每个候选区域的类别预测是否正确,定位损失衡量预测框和真实框的重合程度,DFL用来精细化边界框回归。训练过程中我重点盯三个指标:训练损失曲线、验证集mAP曲线、以及每个类别的Precision和Recall。
实际操作中我发现一个现象:训练损失一直降,验证集mAP却开始震荡,这说明模型开始过拟合训练集了。解决方法是加早停机制,patience设为30轮,验证集mAP连续30轮没有刷新最好记录就停止训练,然后回滚到表现最好的权重。这套机制在模拟项目X里最后把最佳模型停在了第146轮,省了后面54轮的无效训练时间。
以下是训练日志的关键片段,可以看到loss下降趋势和mAP变化:
# 训练日志示例(每10轮打印一次) Epoch [10/200] train_loss: 0.0837 val_mAP@0.5: 0.312 Epoch [20/200] train_loss: 0.0721 val_mAP@0.5: 0.458 Epoch [30/200] train_loss: 0.0643 val_mAP@0.5: 0.552 Epoch [50/200] train_loss: 0.0512 val_mAP@0.5: 0.693 Epoch [80/200] train_loss: 0.0398 val_mAP@0.5: 0.751 Epoch [110/200] train_loss: 0.0312 val_mAP@0.5: 0.783 Epoch [140/200] train_loss: 0.0268 val_mAP@0.5: 0.794 <- 最佳权重保存点 Epoch [160/200] train_loss: 0.0221 val_mAP@0.5: 0.781 <- 开始过拟合4. Python代码实现:训练脚本、推理脚本与关键封装
4.1 基于现有框架的训练入口编写
项目代码全部基于Python 3.9实现,深度学习框架用的PyTorch 2.0配合Ultralytics库快速迭代。训练入口脚本我把关键参数集中到了一个YAML配置文件中,这样换数据集或者调参的时候不需要改代码逻辑。
# configs/train_config.yaml task: detect mode: train model: yolov8n.pt # 预训练权重 data: dataset/weld_defect.yaml # 数据集配置文件 epochs: 200 patience: 30 batch: 16 imgsz: 640 optimizer: SGD lr0: 0.005 warmup_epochs: 5 weight_decay: 0.0005 seed: 42 device: 0 # GPU设备编号训练入口代码如下,基本就是加载配置、调用模型、开始训练、保存结果:
# train.py from ultralytics import YOLO import yaml import torch def load_config(config_path): with open(config_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) def main(): config = load_config('configs/train_config.yaml') # 加载预训练模型,这里使用yolov8n的权重作为起点 model = YOLO(config['model']) # 开始训练 results = model.train( data=config['data'], epochs=config['epochs'], patience=config['patience'], batch=config['batch'], imgsz=config['imgsz'], optimizer=config['optimizer'], lr0=config['lr0'], warmup_epochs=config['warmup_epochs'], weight_decay=config['weight_decay'], seed=config['seed'], device=config['device'], project='runs/weld_detect', name='exp_weld_v1' ) # 保存最优权重路径,后续推理直接加载 best_model_path = f"runs/weld_detect/exp_weld_v1/weights/best.pt" print(f"训练完成,最优模型保存路径: {best_model_path}") if __name__ == '__main__': main()实际跑起来大约花了两个半小时完成全部训练。Ultralytics框架在训练过程会自动在验证集上评估并保存best.pt和last.pt两个权重文件,best.pt对应mAP最高的那次训练,last.pt对应最后一轮。部署的时候一定用best.pt,别用last.pt,我见过有人图省事直接加载last.pt,推理效果明显差一截。
4.2 数据加载与标注格式转换
焊接缺陷数据集的标注我统一转成了YOLO格式,也就是每个图像对应一个txt文件,每一行是"class_id x_center y_center width height",坐标值都归一化到0到1之间。这种格式的好处是跟Ultralytics框架原生兼容,不需要写额外的数据加载器。
数据加载这一层需要注意编码和图像读取方式。工业相机拍出来的图片有可能是16位深度或者带特殊色彩空间的TIFF格式,但深度学习框架的通用预处理管线默认都是针对8位RGB设计的。我引入了一个预处理步骤,先把图像统一转成8位RGB再喂给模型:
# utils/image_loader.py import cv2 import numpy as np def load_weld_image(image_path): # 读取图像时保留原始深度信息 img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) # 如果是16位深度,转到8位 if img.dtype == np.uint16: # 线性缩放到0-255范围 img = ((img.astype(np.float32) - img.min()) / (img.max() - img.min() + 1e-6) * 255.0).astype(np.uint8) elif img.dtype != np.uint8: img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # 如果是单通道灰度图,转到三通道适配预训练模型输入要求 if len(img.shape) == 2: img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) return img这个函数在项目里非常重要,因为它保证了训练和推理阶段数据分布的一致性。漏写这个步骤的结果就是训练时读图正常,推理时遇到16位图直接程序崩溃,或者模型输出的置信度整体偏低。
4.3 推理脚本封装与结果后处理
推理脚本的设计目标是让非算法人员也能直接用。输入可以是单张图片、一个文件夹,或者是视频流。输出除了标注框的可视化图像,还有结构化的检测结果JSON文件,方便MES系统直接解析。
# infer.py import cv2 import json import argparse from pathlib import Path from ultralytics import YOLO def parse_args(): parser = argparse.ArgumentParser(description="焊接缺陷检测推理脚本") parser.add_argument("--weights", type=str, default="runs/weld_detect/exp_weld_v1/weights/best.pt") parser.add_argument("--source", type=str, required=True, help="图片路径、文件夹路径或视频文件路径") parser.add_argument("--conf-thres", type=float, default=0.25, help="置信度阈值") parser.add_argument("--iou-thres", type=float, default=0.45, help="NMS IoU阈值") parser.add_argument("--save-dir", type=str, default="infer_results", help="结果保存目录") return parser.parse_args() def run_inference(args): # 加载模型 model = YOLO(args.weights) # 推理参数配置 results = model.predict( source=args.source, conf=args.conf_thres, iou=args.iou_thres, imgsz=640, save=True, save_dir=args.save_dir, project=args.save_dir, name="predictions", exist_ok=True ) # 结构化输出 output_data = [] for result in results: boxes = result.boxes if boxes is None: continue for box in boxes: cls_id = int(box.cls[0]) confidence = float(box.conf[0]) xyxy = box.xyxy[0].tolist() output_data.append({ "class_id": cls_id, "class_name": model.names[cls_id], "confidence": round(confidence, 4), "bbox": [round(coord, 2) for coord in xyxy] }) # 保存结构化结果 save_path = Path(args.save_dir) / "detections.json" with open(save_path, "w", encoding="utf-8") as f: json.dump(output_data, f, ensure_ascii=False, indent=2) print(f"检测完成,共检测到 {len(output_data)} 个缺陷目标") print(f"可视化结果保存在: {args.save_dir}/predictions") print(f"结构化结果保存在: {save_path}") if __name__ == "__main__": args = parse_args() run_inference(args)这里有个参数需要重点解释:conf_thres和iou_thres。置信度阈值决定了什么样的检测结果会被保留,设得太低会输出大量假阳性框,设得太高又会漏检。工业场景下我建议初始值设为0.25,然后看实际检测效果微调。IoU阈值是NMS用的,控制重叠框的合并程度,默认0.45在焊接缺陷场景下表现够用,因为同一缺陷不会被重复标注两个框。
5. 模型评估与工业落地:从指标到实际部署
5.1 评估指标体系:mAP、Precision、Recall与F1
模型训练完之后不能光看loss,要用一套完整的指标来评估。我重点用这几个指标:mAP@0.5(IoU阈值为0.5时的平均精度均值)、mAP@0.5:0.95(更严格的评估标准)、每个类别的Precision和Recall、以及F1-score。这些指标能全面反映模型在不同置信度阈值下的表现。
模拟项目X最终在测试集上的表现:mAP@0.5达到0.814,mAP@0.5:0.95为0.526。分类别看,气孔的Precision最高,因为特征最明确,圆形的、暗色的点状缺陷很容易和背景区分。裂纹的Recall最低,很多细短裂纹在640×640的输入分辨率下只有十几个像素宽,特征不够显著。这个现象也在意料之中,后续如果要优化,应该针对裂纹做专门的图像切片推理或者增加P2层特征图。
5.2 推理速度与模型轻量化
模型一直在消费级GPU上跑推理,YOLOv8n的参数量大约310万,单张640×640图像推理耗时约4毫秒(GPU)和约120毫秒(CPU)。如果后续要部署到工控机或者边缘盒子,还可以走TensorRT加速或ONNX导出:
# 导出ONNX格式 from ultralytics import YOLO model = YOLO("runs/weld_detect/exp_weld_v1/weights/best.pt") model.export(format="onnx", imgsz=640, opset=12) # 导出TensorRT格式(需NVIDIA GPU环境) model.export(format="engine", imgsz=640, half=True)ONNX导出之后可以用ONNX Runtime做CPU推理,速度比PyTorch原生快20%-30%。TensorRT导出后GPU推理可以进一步压到2毫秒以内,不过TensorRT的部署环境要求较严格,需要匹配的CUDA版本和显卡型号,适合产线固定的设备。
5.3 工业场景的部署方案建议
部署方案取决于产线实际环境。如果是离线抽检,一个工控机装Python环境和ONNX Runtime就能跑起来,检测结果存本地日志。如果要在产线上做实时检测,建议把推理服务用FastAPI封装成HTTP接口,相机拍照后通过接口上传图片,返回检测结果。封装接口的时候要注意线程安全问题,PyTorch模型在多线程并发推理时存在GIL竞争,应该用进程池或者装模型时做好锁处理。
另外一个很有用的部署技巧是设置ROI区域。焊接检测往往只需要关注焊缝区域,图像里其他部分(母材、背景)会引入干扰。推理环节可以先用传统图像处理简单裁剪出焊缝ROI,再送进模型,既可以减少误检也能提升速度。这个技巧在模拟项目X的实测中让误检率降低了大概15%左右。
6. 常见问题与排查技巧实录
6.1 训练不收敛或损失爆炸
焊接图像数据量小,如果学习率设置不合理,经常出现训练损失前期不降反升的现象。我遇到过一次前10个epoch损失从0.08涨到0.15,原因就是初始学习率太高。排查思路很简单:先看warmup阶段是否正常,如果warmup期间loss已经在涨,说明初始学习率偏大,降到0.001再试;如果warmup正常但进入正式训练后loss上升,可能是batch size太小导致梯度噪声过大,增大batch或者降低学习率。
6.2 小目标缺陷(裂纹、小气孔)检测不到
这个问题几乎每个做焊接检测的都会遇到。裂纹只有几个像素宽,在多次下采样之后特征几乎丢光。我试过几种有效的改进思路:一是把输入分辨率从640提到1024或1280,代价是推理速度下降,但小目标Recall明显上升;二是关闭或者降低Mosaic增强,避免小目标被缩得太小;三是用SAHI切片推理库,把原图切成多个小图分别检测再合并结果。最后项目里选了提高分辨率到960的方案,在速度和精度之间取了平衡。
6.3 误检频繁:背景反光被识别为缺陷
焊缝图像里常有弧光反光区域,这些区域的纹理特征和真实缺陷非常接近,模型很容易误判。这个问题的核心原因是训练数据里反光区域的负样本不够。解决的思路也是数据层面为主:收集大量不含缺陷但有反光、飞溅、氧化皮的焊缝图像,标注成背景类或者直接作为无目标样本放进训练集。Ultralytics框架支持配置文件里指定背景图像(背景图不需要标注文件),让模型见过足够多"长得像缺陷但不是缺陷"的样本。
6.4 通用问题排查速查表
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 训练loss为NaN | 学习率过大或数据异常 | 检查输入图像是否有全黑/全白图 | 降低学习率,清洗数据 |
| 验证集mAP振荡不收敛 | 数据集划分泄露 | 检查同源图像是否跨集合 | 按源分组划分数据 |
| 推理速度达不到实时 | 模型过大或硬件不足 | 用脚本测单张推理耗时 | 换YOLOv8n或TensorRT加速 |
| 某一类几乎检测不到 | 该类样本数量过少 | 统计训练集类别分布 | 过采样+针对性增强 |
| 检测框偏移大 | 标注质量差 | 可视化抽查标注 | 统一标注规范后重新标注 |
| 新批次图片效果差 | 数据分布漂移 | 对比新旧图片直方图 | 收集新数据增量训练 |
6.5 独家避坑技巧
最后分享几个我在项目里验证过的经验。第一,焊接检测项目里,标注质量比标注数量重要得多。与其花时间标注5000张粗糙的图,不如集中精力标注2000张规范的图。第二,训练时不要一上来就用完整数据集跑长训练,先拿100张图跑10个epoch,确认loss能降下来、代码链路没bug,再放开全量训练,这能省下大量试错时间。第三,推理阶段的置信度阈值不能一套走天下,不同班组、不同光照条件下采集的图像,最佳置信度阈值可能有明显差异,部署时应保留调整入口。
个人在实际操作中的体会是,焊接缺陷检测这类工业视觉项目,真正的难点往往不在模型结构本身,而在数据工程和场景适配。你花一个月把数据整理得井井有条,抵得上换十次模型结构。我在这套模拟项目X上踩过的坑,总结下来无非就是"别急着训模型,先把数据和评估基线定好"。后续如果想要进一步提升,可以往图像分割方向走(比如用Mask R-CNN直接输出缺陷轮廓),或者引入工业声学信号做多模态融合检测。但那是另一个话题了,先把目标检测这条链路跑通,足够覆盖大多数实际产线的检测需求。