焊接质量这事干过结构件、压力容器或者汽车零部件的人心里都清楚焊缝表面那几道纹路底下藏着多少讲究。以前老师傅拿着放大镜、靠着经验看焊缝现在产线上恨不得每一道弧光都数字化。我去年接手了一个焊接缺陷检测系统的模拟项目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部分延续了FPNPAN的结构自顶向下传语义信息自底向上传空间信息这样对不同尺寸的缺陷都能有响应。Head部分从Anchor-Based换成了Anchor-Free直接预测目标中心点和宽高省去了聚类锚框的过程对缺陷这种尺寸分布不规则的场景反而更友好。焊接缺陷里最怕的是两类目标一类是细长的裂纹宽可能只有两三个像素长却有几十个像素另一类是很小的气孔面积占比可能不到整张图的千分之一。YOLOv8的Anchor-Free设计让模型可以直接回归任意长宽比的目标比固定锚框的方式灵活很多。配合多尺度特征图P3、P4、P5小目标在P3层大特征图上有更多像素可以学习这样气孔这类小缺陷就不容易丢。3.2 训练超参数配置与关键计算训练配置我用了一张消费级GPU12GB显存batch size设为16输入分辨率640×640初始学习率0.01使用SGD优化器搭配余弦退火调度。训练总轮数200轮但实际在第120轮左右验证集mAP就基本不再涨了后面主要是让损失曲线进一步平稳。这里有个必看的参数学习率。YOLOv8默认的初始学习率是按ImageNet预训练权重设计的用在焊接图像这种小数据集上偏大。我尝试过直接训前几个epoch损失直接爆炸梯度更新幅度太大导致预训练权重被破坏。后来把初始学习率降到0.005warmup轮数从默认的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_mAP0.5: 0.312 Epoch [20/200] train_loss: 0.0721 val_mAP0.5: 0.458 Epoch [30/200] train_loss: 0.0643 val_mAP0.5: 0.552 Epoch [50/200] train_loss: 0.0512 val_mAP0.5: 0.693 Epoch [80/200] train_loss: 0.0398 val_mAP0.5: 0.751 Epoch [110/200] train_loss: 0.0312 val_mAP0.5: 0.783 Epoch [140/200] train_loss: 0.0268 val_mAP0.5: 0.794 - 最佳权重保存点 Epoch [160/200] train_loss: 0.0221 val_mAP0.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, encodingutf-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( dataconfig[data], epochsconfig[epochs], patienceconfig[patience], batchconfig[batch], imgszconfig[imgsz], optimizerconfig[optimizer], lr0config[lr0], warmup_epochsconfig[warmup_epochs], weight_decayconfig[weight_decay], seedconfig[seed], deviceconfig[device], projectruns/weld_detect, nameexp_weld_v1 ) # 保存最优权重路径后续推理直接加载 best_model_path fruns/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, typestr, defaultruns/weld_detect/exp_weld_v1/weights/best.pt) parser.add_argument(--source, typestr, requiredTrue, help图片路径、文件夹路径或视频文件路径) parser.add_argument(--conf-thres, typefloat, default0.25, help置信度阈值) parser.add_argument(--iou-thres, typefloat, default0.45, helpNMS IoU阈值) parser.add_argument(--save-dir, typestr, defaultinfer_results, help结果保存目录) return parser.parse_args() def run_inference(args): # 加载模型 model YOLO(args.weights) # 推理参数配置 results model.predict( sourceargs.source, confargs.conf_thres, iouargs.iou_thres, imgsz640, saveTrue, save_dirargs.save_dir, projectargs.save_dir, namepredictions, exist_okTrue ) # 结构化输出 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, encodingutf-8) as f: json.dump(output_data, f, ensure_asciiFalse, indent2) 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要用一套完整的指标来评估。我重点用这几个指标mAP0.5IoU阈值为0.5时的平均精度均值、mAP0.5:0.95更严格的评估标准、每个类别的Precision和Recall、以及F1-score。这些指标能全面反映模型在不同置信度阈值下的表现。模拟项目X最终在测试集上的表现mAP0.5达到0.814mAP0.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(formatonnx, imgsz640, opset12) # 导出TensorRT格式需NVIDIA GPU环境 model.export(formatengine, imgsz640, halfTrue)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直接输出缺陷轮廓或者引入工业声学信号做多模态融合检测。但那是另一个话题了先把目标检测这条链路跑通足够覆盖大多数实际产线的检测需求。