ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv5钢轨表面缺陷检测实战:从数据准备到部署优化

YOLOv5钢轨表面缺陷检测实战:从数据准备到部署优化

1. 项目全貌与系统框架拆解

先交代一下背景。我前阵子接了一个跟轨道检测相关的视觉项目,需求很直接:从轨道巡检车拍回来的高清图像里,自动标出钢轨表面存在的裂纹、擦伤、剥落之类缺陷。数据量不算大,也就几千张标注图,但场景很典型——户外光照变化大、钢轨表面反光强、缺陷形态又碎又小。方案从传统图像处理一路对比到多个检测模型,最后还是定在YOLOv5上。这篇就把整套代码逻辑从头到尾拆一遍:为什么选它、数据怎么组织、训练怎么调、推理怎么做,以及那些文档里不会写的坑。

先说结论:YOLOv5这套框架在工业缺陷检测里确实够稳。社区生态成熟、代码结构清楚、部署链路完整,从训练到导出再到落地推理,几乎每一步都有现成方案可以抄。拿它做钢轨表面缺陷检测,本身就是一条被反复验证过的路径,我这边做的主要工作是把它适配到轨道场景的具体细节上——数据组织、参数配置、后处理策略,以及针对小缺陷的几个关键调整。

这个内容适合谁看?如果你正在做工业质检、缺陷识别相关的视觉项目,或者刚接触YOLOv5想找一个真实场景的完整案例,这篇文章都能给你省不少时间。我会把代码仓库的核心模块、训练流程、推理脚本都过一遍,重点讲清楚那些不跑一遍根本发现不了的问题。

整个系统从功能上可以分为五块:数据准备环节负责把巡检图像转成训练所需的格式;模型训练环节负责拟合缺陷特征;验证评估环节负责衡量模型效果;推理检测环节负责把模型用起来;最后的部署优化环节负责让模型跑得更快。下面按照这个链路逐一展开。

因为YOLOv5的代码生态已经很成熟,仓库结构和基础逻辑大多数人都眼熟,我不会逐行复述源码,而是挑出跟钢轨缺陷检测强相关的部分做深度解析——数据怎么配、anchor怎么调、小目标怎么处理、后处理参数怎么设,这些才是真正决定模型能不能用的地方。

1.1 钢轨缺陷检测的痛点决定了技术路线

钢轨表面缺陷检测这个场景,跟通用目标检测有一个显著区别:缺陷目标极小。典型的钢轨表面裂纹,宽度可能只有两三个像素,长度几十个像素,在整张1920×1080的巡检图像里占比非常低。如果直接用默认参数训练YOLOv5,小目标漏检问题会非常突出。

另一个痛点是背景干扰。钢轨表面本身就有纹理、锈迹、水渍、油污,再加上户外拍摄的光照变化和相机反光,很多背景区域长得比缺陷更像缺陷。用传统视觉方案去处理,比如边缘检测加形态学过滤,面对固定场景还能凑合,一换光照、一换钢轨型号就全线崩溃。

反观深度检测模型,YOLOv5这类单阶段检测器天然就比传统方案抗干扰,因为它学的是高维语义特征而非手工设计的低层特征。在实测对比中,同一个钢轨表面缺陷数据集,传统方案的最好成绩也只有不到六成的召回率,而YOLOv5s在相同数据上能达到九成以上的召回率。这个差距是决定性的。

1.2 为什么最终没有选YOLOv8或其他框架

每次提到选型,总有人问:现在YOLOv8都出来了,为什么还用v5?答案不复杂。YOLOv5的生态积累足够深,文档、教程、预训练权重、部署方案都是最齐全的。对于我们这个场景,模型的绝对精度差异并不大,但调试便利性和可参考案例的丰富程度差别很大。遇到问题想搜解决方案,v5版本基本都能搜到,v8相对少很多。

还有一个实际考量是部署兼容性。钢轨巡检系统的落地环境以工业PC为主,有些还是老旧工控机,对CUDA版本和驱动版本有严格限制。YOLOv5对PyTorch版本的要求更宽泛,ONNX导出也更稳定,在受限的工业环境里不容易翻车。至于最新版本带来的速度提升,在钢轨巡检这个每秒处理一帧就够用的场景里,并不构成核心优势。

2. 数据准备与标注规范——决定模型上限的第一步

在这个项目里,数据准备占掉了大约六成的工作量。模型结构可以抄现成的,训练参数可以照搬,但数据不行——每个场景的数据都得自己采集、自己清洗、自己标注。而且数据质量直接决定模型上限,后面所有的调参优化都只能在这个上限内做文章。

2.1 缺陷类别定义与标注规范

我这边将钢轨表面缺陷划成了四类:裂纹、擦伤、剥落、压溃。这个分类跟轨道维护人员的习惯保持一致。如果分类粒度太粗,不同形态的缺陷混在一起,模型训练时特征冲突,精度上不去;如果粒度太细,比如把裂纹按长度再细分几个等级,类间差异又太小,标注一致性很难保证,模型也学不出区分度。

标注规范这件事特别重要,也是很多项目翻车的地方。钢轨裂纹形态不规则,标注时最常用的原则是“最小外接矩形”——框住缺陷的主体区域即可,不需要把每一条细小分叉都包进去。但如果多人协作标注,每个人的框选习惯不一样,最终数据里同类缺陷的框大小差异会非常大,直接影响回归头的收敛效果。

我处理这个问题的方法是先做标注规范文档,再对标注结果做一致性抽检。具体做法是计算每个类别的平均宽高比分布,如果某个标注者的结果明显偏离群体平均值,就要回头重新校对。实测下来,这一步能有效避免训练时box_loss震荡的问题。

2.2 钢轨缺陷数据集的目录结构与格式

YOLOv5的数据组织遵循固定的目录规则,钢轨缺陷数据集按照这个规则组织后,训练时不需要额外写数据加载逻辑。核心是images和labels两个主目录,各自再拆成train和val子集。标注文件跟图片一一对应,是同名的txt文件,每行代表一个目标框。

标注文件每行的格式是五个数值:类别索引、归一化中心x坐标、归一化中心y坐标、归一化宽度、归一化高度。

注意:YOLOv5的坐标全部是归一化后的值,归一化基准是图片宽高,而不是边长。如果转换标注时用错基准,模型训练后会出现预测框整体偏移一个方向的问题,而且loss降不下去。

对于钢轨缺陷这种小目标数据,有一个额外建议:原始高清图不要直接缩放到训练尺寸。一张1920×1080的图直接缩到640分辨率,很多细小裂纹会直接消失。更好的做法是先用切片脚本把大图切成若干小块,保留缺陷的原始像素信息,再做缩放。钢轨巡检图像切成四个512×512的小块后,小缺陷的保留效果明显改善。

2.3 数据增强策略在钢轨场景的取舍

YOLOv5内置的Mosaic增强在大多数场景都好用,但钢轨缺陷检测这里要留个心眼。Mosaic会把四张图拼在一起训练,如果四张图的亮度差异太大,模型会学到一些奇怪的亮度特征。我的做法是开启Mosaic,但把mixup概率降到很低,同时强化HSV增强里的饱和度变化,因为钢轨表面缺陷的颜色特征本来就以暗色为主,饱和度扰动有助于模型忽略颜色干扰、专注形状特征。

钢轨场景还需要增加一种平移和旋转的增强强度。巡检图像中缺陷出现的位置随机性强,角度也随机,适度加大仿射变换的幅度可以提升泛化能力。但不要开太大,否则缺陷形变失真,模型反而学到不真实的形态。

3. 核心训练逻辑与代码模块拆解

数据和配置准备好之后,真正进入训练环节。YOLOv5的训练代码虽然看起来庞大,但核心逻辑并不复杂。我按从数据读取到损失计算这条主线来拆,把关键模块和代码逻辑讲清楚。

3.1 数据加载与标签匹配机制

YOLOv5的数据加载响主要实现在datasets.py中,核心类继承自PyTorch的Dataset。读取流程是:按索引取图片路径和对应标注文件,读取后做Mosaic等在线增强,然后做Letterbox缩放统一尺寸,最后归一化并转为Tensor。这里重点是构建了合理的预处理链路。

标签匹配机制是YOLOv5的核心设计之一。模型每个输出位置有多个anchor,输入标注框首先计算与这些anchor的宽高比,匹配度高的分配为正样本参与预测和损失回传,匹配度低的归为背景。这组anchor对钢轨缺陷这种长宽比差异大的场景需要调整策略——钢轨裂纹的长宽比可以到10比1,但默认的anchor分布并没有覆盖这个极端范围。

我这边处理方式是:先用自己的数据集跑一次自动聚类,用k-means生成更适合钢轨缺陷的anchor尺寸,然后在模型配置文件中覆盖默认值。这是一个容易被忽视的步骤,但对小缺陷检测效果的提升非常明显。实测中,仅调整anchor这一步就让mAP提升了约3个百分点。

3.2 损失函数设计与关键参数调节

YOLOv5的损失由三部分组成:分类损失(BCE)、目标置信度损失(BCE)和定位损失(CIoU)。在钢轨缺陷场景中,这三部分的表现各有各的问题。

分类损失容易出现过拟合倾向,因为某些缺陷类别样本数量偏多。解决思路是开启类别权重,给样本少的类别提高损失权重。置信度损失在缺陷检测中经常出现“什么都检测成目标”的现象,原因是钢轨表面纹理复杂,大量背景区域被误判为含目标。定位损失对缺陷这类小目标本身就比较严格,因为CIoU会对中心点距离和宽高比差异敏感,小目标的微小像素偏差就会导致损失值大幅波动。

为了解决小目标定位难的问题,我做了两个调整。第一个是增加输入分辨率,把默认的640改成960,让小目标占据更多像素。第二个是开启YOLOv5的P2检测层,专门为小目标增加一层高分辨率特征图输出。这两个调整配合后,细裂纹的召回率提升非常显著。

3.3 训练过程解析——loss曲线的真实含义

YOLOv5训练时终端会实时打印三类损失:box_loss、obj_loss、cls_loss。很多人盯着这三个数看半天也不知道到底正常不正常,这里说下我的判断经验。

健康的loss曲线应该是三线同步下降,在前50个epoch下降明显,之后逐渐平缓。如果box_loss一直高位震荡不下降,多半是标注问题——框选不准确或尺寸不一致,模型怎么拟合都拟合不了。如果obj_loss降得很慢,说明前景和背景的区分度不够,需要回到数据增强上做调整。如果cls_loss降得快但验证集mAP不高,大概率是过拟合了,需要加大增强强度或增加样本量。

我第一版训练时遇到过一个很典型的问题:cls_loss在后期突然反弹。排查下来是因为验证集和训练集有图片泄露——数据增强过程中生成了大量视觉上非常接近的变体,被错误地划到了验证集。修正划分逻辑后,cls_loss曲线立刻恢复健康。

4. 训练调参与性能评估的完整实操

这一节把训练环节的可落地细节完整过一遍。包括硬件基准、超参数设置、训练命令、性能评估指标,以及调参的优先级策略。

4.1 硬件环境与训练超参数配置

训练环境方面,我用的是一张24G显存的显卡。钢轨缺陷数据集不算大,单卡完全够用。如果你只有一张8G显存的卡,也不是不能跑,就是把batch size调小、输入尺寸降到640即可,代价是训练时间变长。

下面是我为钢轨缺陷场景最终采用的训练参数组合:

超参数推荐值说明
输入尺寸960×960比默认640更能保留小缺陷细节
Batch Size32根据显存调节,不宜过小否则BN层不稳定
初始学习率0.01配合余弦退火衰减曲线使用
动量0.937延续默认值
权重衰减0.0005防止过拟合
Epoch300早停机制为patience=50
预训练权重YOLOv5m比s精度高,比l训练快,均衡选择

学习率这块值得多说一句。YOLOv5默认使用SGD加上余弦退火调度。我试过用AdamW,收敛确实更快更稳,但最终精度略低于SGD。工业项目中模型的最终精度比训练速度重要,所以最终用了SGD加余弦退火组合。

4.2 模型规模选型与训练命令示例

YOLOv5的模型规模从n、s、m、l到x共五档。对于钢轨缺陷检测,直接用x级模型是没必要的——缺陷类别少、语义复杂度有限,大模型带来的精度收益有限但推理成本大幅增加。折中方案是YOLOv5m,精度比s有明显提升,速度又比l快很多。

训练时的命令行示例:

cd yolov5 python train.py \ --data rail_surface.yaml \ --weights yolov5m.pt \ --img 960 \ --batch 32 \ --epochs 300 \ --device 0 \ --cache \ --patience 50

其中rail_surface.yaml是数据集配置文件,必须保证类别名称顺序跟标注文件中的索引完全一致,这是一个很容易出错的地方。如果yaml里第一个类别是crack但标注文件中索引0代表scratch,训练不会报错,但评估结果全乱。

4.3 评估指标怎么读——不只是看mAP

模型训练完成后,可以用以下命令跑一轮验证:

python val.py --data rail_surface.yaml --weights runs/train/exp/weights/best.pt --img 960

输出指标中,mAP@0.5是最常用的衡量指标,但在钢轨缺陷场景里,我个人更关注每个类别单独算的召回率。四类缺陷样本量差异较大,平均数据可能把某些类别的劣势掩盖掉。比如整体mAP可能有0.85,但单独看裂纹召回率只有0.7,这个差距必须单独观察。

另一个需要关注的指标是F1分数在0.25到0.5置信度区间的表现。工业场景中,置信度阈值设置直接影响误报率和漏报率。如果模型在0.25置信度下的F1明显高于0.5置信度,说明模型对低置信度的检测结果还有信息量,可以在推理时适当降低阈值并配合更严格的NMS来抑制重复框。

5. 推理代码实现与部署优化

训练完成之后,真正的挑战才开始。模型要在巡检车上跑起来,还要跟现有的轨道巡检软件集成。这一节讲推理侧的代码实现和部署优化,也是项目从技术验证走向产品化的关键环节。

5.1 自定义推理脚本实现

YOLOv5自带的detect.py可以直接用来做单张图片推理或者视频推理,但实际集成场景中,通常需要把检测逻辑嵌入到现有的巡检软件框架里。这时最好写一个自己的推理封装,核心代码逻辑如下:

import cv2 import torch from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.plots import Annotator def load_model(weights_path, device): model = attempt_load(weights_path, device=device) return model def letterbox(img, new_shape=960): # 保持宽高比缩放并填充边缘 shape = img.shape[:2] r = min(new_shape / shape[0], new_shape / shape[1]) new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) dw = (new_shape - new_unpad[1]) / 2 dh = (new_shape - new_unpad[0]) / 2 img_resized = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) img_padded = cv2.copyMakeBorder(img_resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114)) return img_padded def detect(img, model, device, conf_thres=0.25, iou_thres=0.45): img_letterboxed = letterbox(img) img_tensor = torch.from_numpy(img_letterboxed).to(device) img_tensor = img_tensor.permute(2, 0, 1).float().div(255).unsqueeze(0) pred = model(img_tensor)[0] pred = non_max_suppression(pred, conf_thres, iou_thres) return pred

这段代码有两个细节值得注意。一是letterbox的填充逻辑不能省,训练时输入图片是经过letterbox处理的,推理时不处理的话输入分布不一致,检测效果会明显退化。二是NMS的iou阈值在钢轨缺陷场景中不宜设得太低,因为密集裂纹区域会有大量互相重叠的预测框,如果iou_thres设成0.7以上,重叠框会大量输出,后处理时还要再过滤。

5.2 推理坐标换算——最容易出错的环节

推理完成后得到的目标框坐标是基于缩放后图像尺寸的,需要按letterbox的反向逻辑映射回原图坐标。这一步出错的表现是:框的位置偏离真实际目标,通常偏向图像中心方向。

def scale_boxes(orig_shape, pred, letterbox_shape): # pred为NMS后的检测结果,格式为[x1, y1, x2, y2, conf, cls] pred[:, :4] = scale_coords(letterbox_shape, pred[:, :4], orig_shape).round() return pred

YOLOv5的utils里已经封装好了scale_coords函数,直接用就行。但注意传入的letterbox_shape必须是缩放填充后的完整尺寸,而不是原始尺寸。我见过不少人在这一步传错参数,导致所有检测框都偏移。

5.3 模型导出与单帧性能优化

训练好的PyTorch模型可以直接部署,但工业场景中通常建议先导出为ONNX或TensorRT格式。ONNX的跨平台兼容性最好,TensorRT在NVIDIA显卡上有最高推理速度。

python export.py --weights runs/train/exp/weights/best.pt --img 960 --include onnx

导出后可以用ONNX Runtime加载运行,比原生PyTorch推理快不少。实测中同一个模型,PyTorch单帧推理耗时约45ms,转成ONNX后降到28ms,转成TensorRT后进一步降到约15ms。对于钢轨巡检这种对实时性没有极端要求的场景,ONNX格式已经够用。

除了模型格式,推理侧的优化还有两个方向。第一个是预处理优化,把图像缩放方式从逐帧动态计算改成提前分配固定缓存。第二个是批量推理,如果巡检系统同时处理多路相机图像,把多帧打包成一个batch推理能显著提升整体吞吐量。

6. 常见问题排查与调优实录

这部分是实际踩坑的记录。我在钢轨缺陷检测项目里遇到的问题,多数都是YOLOv5在工业数据上很典型的问题。整理成速查表形式,方便遇到问题时快速定位。

6.1 钢轨缺陷检测高频问题速查表

问题现象可能原因解决方案
box_loss高位震荡标注框尺寸不一致、错误标注重新审核标注,统一标注规范
裂纹类目漏检严重小目标在640下像素太少提升输入尺寸至960,开启P2层,调整anchor
误报率高、背景被识别成缺陷光照不均、训练数据背景多样性不足增加HSV饱和度和曝光扰动,采集更多背景负样本
验证集mAP高于训练集数据划分时序错误导致泄露按原始图片切分,而不是按增强后图片切分
导出ONNX后输出shape不一致PyTorch版本与ONNX版本不兼容锁定torch1.x稳定版本,导出时指定opset=12
推理时检测框整体偏移letterbox参数配置错误、scale_coords传参错误检查预处理与坐标映射环节参数

6.2 小目标检测优化的三次迭代实例

这个项目最有代表性的一段优化经历,就是解决细裂纹漏检问题。第一版模型在960输入尺寸下,整体mAP到了0.87,但单独看裂纹类目,召回率只有0.72。这个表现在工业场景里基本不可用,因为漏掉裂纹比多报几个误检严重得多。

我的第一次调整是改anchor。让YOLOv5在数据上聚类生成新anchor,然后重新训练。跑完看结果,裂纹召回率只提高到了0.76,提升有限。主要原因是光调anchor不够,模型提取小目标特征的能力还得靠网络结构配合。

第二次调整是开启P2检测层。在模型配置里增加一层更高分辨率的输出特征层,让小目标对应的特征图从80×80细分成160×160,获得的特征信息更密集。这次效果明显,裂纹召回率提高到0.83。

第三次调整是把推理时的置信度阈值从默认的0.25降到0.15,配合一个专门的NMS后处理逻辑:先保留低置信度但跟高置信度框高度重叠的裂纹候选框。这一步把召回率提上了0.88。代价是误报率增加了,但再通过验证集的置信度校准,最终在误报可控的前提下把裂纹召回率稳定在了0.86左右。

6.3 部署环境适配的几个坑

工业落地场景中,部署机器的软件环境比训练机器复杂得多。我遇到过的典型问题是:巡检电脑的显卡驱动版本太老,只支持CUDA 10.2以下的版本,而训练环境里的PyTorch和CUDA版本都更高。导出的ONNX模型在推理机上直接加载失败。

解决方案是模型侧不做修改,纯粹靠导出时的兼容性参数来适配。把ONNX的opset版本设为12,是兼容性最好的一个版本,同时把动态输入改为固定尺寸输入。这样导出后的模型几乎可以在任何ONNX Runtime版本上运行,不再依赖训练时的CUDA环境。

如果部署机器没有NVIDIA显卡,只能用CPU推理,那YOLOv5m模型跑960输入的单帧时间可能接近300ms,明显偏慢。建议是训练一个YOLOv5s模型专用于CPU环境的轻量部署。小模型在钢轨缺陷上精度差不了太多,但CPU推理速度能快近一倍。有条件的话也可以用OpenVINO工具做推理优化,实测CPU推理性能能有3倍左右的提升。

7. 项目维护与模型迭代的经验总结

项目上线不是终点,模型还需要随着采集数据量的增加做持续迭代。重新训练模型前,要把新采集的图像经过同样的标注流程加入数据集,然后重跑一遍训练、评估、导出流程。这个过程有几个细节要注意:新增数据必须按同样的规范标注,新增数据在随机划分时要保证同一个缺陷不漏泄到验证集里,模型迭代后要做回归测试,确认新模型没有在旧数据上出现性能回退。

我在实际维护过程中还发现了一个提高迭代质量的技巧:每次收集到的新图像,先人工快速筛选一遍,把模糊、过曝、纯背景的丢出去,避免无效数据稀释模型的学习效果。钢轨巡检车跑一天下来的数据量很大,真正含有新缺陷形态的图像占比不高,做好筛选能大幅提升每次迭代的效果。

这个项目后续还可以往两个方向扩展。一是把单帧检测扩展到视频序列检测,利用时序信息过滤误检,这在连续性巡检场景中效果会很好。二是加入缺陷等级分类,让系统不仅能检测缺陷位置,还能输出缺陷严重程度,给维护人员的维修决策提供参考。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进