这阵子一直在搞一套铁路轨道缺陷识别的检测系统,从最初只想把YOLOv11模型跑通,到最后做成一个带登录注册、图片/视频/实时检测、结果导出界面的完整桌面工具,前后折腾了差不多两个月。中间踩了不少坑,也攒下不少可以直接照抄的经验。如果你正在做类似的工业视觉检测项目,或者刚接触YOLO系列、想找一个从数据到部署的完整落地案例,这篇应该对你有用。
这个项目最核心的一件事,不是把模型精度刷得多高,而是把所有环节串起来:数据怎么来、标签怎么定、模型怎么改、界面怎么不卡、导出怎么才能让人真正拿去用。我尽量按实际做项目的过程来讲,每一段都是当时遇到的真问题。
1. 项目从哪里来:轨道巡检场景下的真实需求与数据构建
1.1 轨道检测为什么需要视觉方案
轨道缺陷这件事,平时不坐火车的人可能完全无感,但在铁路运维里是实打实的刚需。钢轨长期承受列车碾压,会出现轨面裂纹、剥离掉块、光带异常,还有扣件松动、螺栓缺失这类“小毛病”。传统做法主要靠人工巡检,天窗时间内工务段的人带着仪器在轨道上走,又慢又累,而且细小裂纹肉眼很容易漏。
这个项目的目标很直接:用摄像头代替一部分人眼重复劳动,基于深度学习的YOLOv11模型自动识别缺陷类型并在画面里框出来。表面看是算法Demo,实际上它是个完整的小系统——检测只是核心,外面还套了账号体系、可视化界面、结果保存这些工程化内容,这样才符合真实使用习惯。
1.2 数据集不是一次到位:公开集打底、自采补充、增强兜底
做检测项目第一道坎永远是数据。轨道缺陷不像猫狗数据集那么好找,正样本本来就少,缺陷种类又碎。我采用的是“开源缺陷样本 + 模拟环境补拍 + 针对性增强”的组合方式,总共整理出约1.2万张可用图片。
先说明一下,纯靠公开数据有个问题,公开集大多是实验室转盘拍摄的轨面特写,角度固定、光照单一、背景干净。拿这种数据训练出来的模型,一到现场就露馅。所以我从两个方面做了补强:
- 在模拟轨道模型上架手机和工业相机,用不同角度、远近、顺光逆光拍了一批素材,重点补充“暗光”“反光”“阴影遮挡”这类难例。
- 用Albumentations做在线增强,包括随机仿射、亮度对比度扰动、马赛克拼接、小目标复制粘贴。其中复制粘贴对轨道缺陷特别有效,因为很多裂纹在整张图里只占很小区域,喂给网络的目标太少。
最终数据集里缺陷类别我定了5类:轨面裂纹、轨头剥离、扣件缺失、扣件断裂、螺栓缺失。之所以没有把“光带异常”这种模糊概念加进来,是因为标注一致性太差,模型学到的东西会非常不稳定。类别宁少勿杂,这个原则在缺陷检测里尤其重要。
1.3 标注环节最容易埋雷,我的做法是“多轮校准”
标注是整个项目里最容易被低估的环节。YOLO格式本身很简单——每张图对应一个txt文件,每行是“类别id x_center y_center width height”,坐标做了归一化。但数据量大起来以后,不同人标出来的框风格完全不同,有的喜欢紧贴目标,有的习惯留白,模型会被这种不一致性搞糊涂。
我的处理方法是:标注完成后随机抽几百张图出来做交叉检查,把同一类别的框尺寸分布画出来,如果发现某个类别的框宽高比特别离散,基本就是标注标准不统一。轨面裂纹这种细长目标尤其容易出现这种问题,后来我干脆重新统一了一遍规则:裂纹沿延伸方向包紧、剥离区域按实际掉块轮廓、螺栓类只看可见部分。
还有一个细节值得提:类别不平衡。扣件缺失这种样本好找,但轨头剥离的正样本可能就几百张。训练时如果直接硬跑,模型会严重偏向样本多的类别。我的做法是先按类别数量统计,给样本少的类别在损失计算里加权重,同时用复制粘贴增强把稀缺类目标复制多份,让每一轮训练都能稳定见到它们。
2. YOLOv11选型逻辑,以及针对轨道场景做了哪些改造
2.1 为什么选YOLOv11而不是别的检测模型
YOLO系列发展到现在,能选的版本很多。我自己之前主力用的是YOLOv5和YOLOv8,这次换成YOLOv11,核心原因有三条:推理框架成熟、小目标能力有优化、官方仓库支持直接出模型。
先看一个粗糙的选型对比,这是我当时在几个候选模型之间做的评估:
| 模型 | 推理速度(T4上ms/张) | 小目标效果 | 部署复杂度 | 结论 |
|---|---|---|---|---|
| YOLOv5 | 约8-12ms | 一般 | 低 | 足够用但旧 |
| YOLOv8 | 约7-10ms | 较好 | 低 | 可选项 |
| YOLOv11 | 约6-9ms | 小目标检测层更友好 | 低 | 最终选择 |
| RT-DETR | 约10-15ms | 中上 | 中 | 速度吃亏 |
| 两阶段类Faster R-CNN | 20ms以上 | 强但慢 | 高 | 现场不合适 |
轨道缺陷检测有个特点:缺陷往往小、细长、对比度低,对速度要求又比较高,因为现场希望实时或近实时出结果。YOLOv11在结构上对多尺度特征融合做了进一步优化,搭配合适的分辨率,比上一代在小目标上更容易出效果,而且官方训练推理链路是目前最顺的,后面做界面、导出ONNX都很省事。
不过要注意,选YOLOv11不代表什么都不用改,Detection Head默认配置是为通用目标服务的,轨道场景必须做针对性调整。
2.2 针对细长裂纹和反光表面做的具体修改
第一处修改是输入分辨率。默认训练尺寸一般是640x640,但轨道裂纹很多只有几十个像素宽,缩放以后特征基本消失了。我把训练和推理分辨率提到1280x1280,虽然显存和耗时都有增加,但小目标召回率明显提升。如果你显卡显存小于8G,建议用896或1024,折中一下。
第二处修改是加了更浅层的检测分支。YOLOv11本身有P3、P4、P5这几个检测尺度,对应8倍、16倍、32倍下采样。轨道上的小缺陷在8倍下采样的特征图上仍然太小,我额外加了一个P2检测层,对应4倍下采样,专门用来捕捉高频细节。代价是计算量增加,但实测mAP50-95能涨两三个点,这钱花得值。
第三处是注意力模块。我在Backbone的倒数第二层后面加了一个轻量的注意力模块,帮助网络重点关注轨面区域而不是枕木、道砟等背景。这不算什么创新结构,但确实抑制了把石块反光当裂纹的误检。
2.3 训练超参数怎么定,以及怎么压住过拟合
训练配置是这套系统里最有参考价值的部分。我直接用YOLOv11的官方训练仓库,配置文件里核心参数如下:
train: dataset/train/images val: dataset/val/images nc: 5 names: ['crack', 'spall', 'clip_missing', 'clip_broken', 'bolt_missing']yolo detect train data=rail_defect.yaml model=yolo11m.pt epochs=150 imgsz=1280 batch=8 device=0 optimizer=AdamW lr0=0.0005几个参数背后的考虑:
- 模型用yolo11m,比nano精度高,比large省显存,对于这个任务够用。
- 优化器选AdamW而不是默认的SGD,收敛更快,尤其在这种小数据集上更稳。
- 训练轮数设150但是加了早停,实际跑到第80轮左右验证集指标就不再明显上升,果断停掉避免过拟合。
- 输入尺寸1280意味着batch不能太大,8比较稳,再大容易爆显存。
训练过程中我每10轮导出一轮验证集的可视化预测图,肉眼观察有没有把道砟误判成剥离、把水渍误判成裂纹这类问题。这比只看mAP好用得多,因为mAP是总体指标,掩盖了很多局部问题。
3. 从模型到系统:UI界面和登录注册模块的设计思路
3.1 为什么选PySide6做桌面界面
系统做出来不是给程序员自己玩的,目标使用场景是工务人员或者检测工程师在电脑上操作。Web界面当然也可以,但部署起来麻烦,还得考虑摄像头能否被浏览器正常调用。最后选了PySide6做桌面端,主要原因有三个:Python生态里和模型推理库集成最顺;跨平台,Windows/Linux都能跑;开发速度快,拖控件布局比从零写Web前端省事得多。
用PySide6还有一个隐性优势:和PyTorch/ONNX Runtime在同一个Python进程里,图像数据从QImage转成numpy数组直接喂给模型,少了一层HTTP通信的延迟,实测单张图片从点击到显示结果在1秒以内。
3.2 登录注册模块:账号存储与密码安全处理
这个模块看起来跟“轨道缺陷识别”没什么关系,但真正给用户使用的时候必须有。不同的检测人员要有独立账号,管理员需要知道谁在哪个时间段跑了哪些检测。我设计的是“管理员+普通用户”两级角色:管理员能查看所有检测历史和账号管理,普通用户只能做检测、看自己的记录。
账号存储直接用SQLite,没有引入独立数据库服务,因为单机部署场景不需要。密码绝不存明文,用的是加盐SHA256哈希。下面这段是把注册逻辑抽出来的核心片段:
import hashlib import os def register_user(username, password): salt = os.urandom(16).hex() digest = hashlib.sha256((salt + password).encode()).hexdigest() # 写入SQLite:insert into users (username, salt, password) values (?,?,?) return True def verify_user(username, password, salt, stored_digest): digest = hashlib.sha256((salt + password).encode()).hexdigest() return digest == stored_digest实际项目里我还加了三件事:密码最少8位且包含字母和数字;连续输错5次锁定对应账号15分钟;登录成功后返回一个带过期时间的用户令牌,界面跳转时携带这个令牌。这些细节不复杂,但能让系统看起来像个正经软件而不是课程设计。
3.3 检测主界面怎么布局,用户才不会一头雾水
主界面的布局原则是“打开就会用”。整个窗口分四块区域:
- 左侧是操作面板,包含“打开图片”“打开视频”“打开摄像头”三个入口,以及置信度阈值滑杆。
- 中间是显示区,原图和标注结果叠加显示,检测框旁边实时显示类别名称和置信度。
- 右侧是结果列表,每条检测记录包含缺陷类别、置信度、坐标、时间戳。
- 底部是统计条,显示当前文件的缺陷数量、各类别分布、耗时信息。
另外做了一个“一键导出报告”按钮,把检测结果输出成带图片批注的PDF或Excel,这个功能在实际调研时被反复提到,比想象中刚需。
界面上最需要注意的事情是把“置信度阈值”做成可调滑杆,而不是写死。因为不同场景下要求完全不同:巡检初筛希望宁可错报也别漏报,那就把阈值调低到0.25;出了疑似缺陷后人工复查,希望减少无意义报警,可以调到0.5以上。
4. 训练和部署落地时最折磨人的几个坑
4.1 验证集指标很漂亮,现场却没法用的“假阳性”问题
这是整个项目里最值得写的一段。模型训练完在验证集上mAP50到了0.94,我当时很高兴,结果拿到真实的轨检车拍摄视频里一测,立刻被打脸——连续把钢轨接缝处的阴影、轨枕上的水渍、甚至铁锈色的道砟识别成了“轨面裂纹”。
原因其实很典型:我的验证集和训练集共用同一批采集条件下的数据,背景分布高度一致,模型根本没有学会“轨面”和“非轨面”的边界,它只是记住了“和训练样本纹理相近的东西”。
排查过程我分了三步走:
- 先看混淆矩阵,发现错误主要集中在crack和spall两个类别与background之间,证明是背景误检而不是类别混淆。
- 再抽错样本,把预测置信度前20的错误图拿出来,发现都是暗色背景加局部亮斑。
- 最后做了一组实验,单图输入尺寸从1280降到640,误检反而变多,证明问题不在分辨率而在数据分布。
修复措施是在训练集里加入了大量裁切后的背景负样本,并且对正常样本做了随机仿射和HSV扰动,模拟现场光照。重新训练后误检直接降了七成。
4.2 界面卡顿与摄像头实时性不足的根因
界面做好之后第一次联调,发现打开摄像头预览时拖动窗口就能把程序卡死。原因很清晰:我在主线程里直接做了“取帧-推理-绘制”这一整条链路,PySide6的界面事件循环被推理阻塞,界面自然就不动了。
解决办法是把推理放到单独的QThread里,界面线程只管显示。关键逻辑是:
class DetectThread(QThread): frame_ready = Signal(np.ndarray) result_ready = Signal(dict) def run(self): while self.running: frame = self.cap.read() results = model(frame) # 这个操作放到子线程 self.result_ready.emit(results)除了线程分离,还加了帧率控制:摄像头输入实时帧率可能有几十fps,但模型推理在1280分辨率下只能跑到十几fps,如果每帧都推理会造成队列堆积,画面延迟越来越大。我的做法是丢帧策略,只对最新的一帧推理,旧帧直接丢弃,这样界面看到的永远是接近实时的结果。
4.3 导出ONNX时踩的算子兼容问题
模型训练好以后,我打算导出ONNX然后用ONNX Runtime推理,这样可以摆脱PyTorch环境,部署体积更小、启动更快。第一次导出倒是很顺利,但一跑就发现输出和PyTorch对不上,检测框位置偏了不少。
排查了半天,问题出在“输出尺寸动态化”上。导出的输入尺寸允许动态变化,但我用来解析输出的代码却写死了shape。YOLOv11在非最大尺寸输入时输出的特征图尺寸会变,直接reshape必然出错。后面我把导出参数改成指定固定尺寸:
yolo export model=best.pt format=onnx imgsz=1280 dynamic=False simplify=True并且后处理里的坐标解析改成读取实际输出shape而不是写死。另外,因为ONNX自带的NMS处理起来比较麻烦,我干脆在导出时关闭了模型内NMS,把原始预测输出拿到Python里自己用置信度过滤和非极大值抑制处理,逻辑完全可控,排查问题也容易得多。
5. 实测效果、运行环境与可复现的启动步骤
5.1 验证集指标和真实场景表现
最终模型在自建验证集上的指标如下:
| 缺陷类别 | mAP50 | mAP50-95 | 备注 |
|---|---|---|---|
| 轨面裂纹 | 0.93 | 0.76 | 细长目标,1280输入下表现尚可 |
| 轨头剥离 | 0.91 | 0.72 | 样本量最少,仍有提升空间 |
| 扣件缺失 | 0.96 | 0.83 | 形状规则,最容易识别 |
| 扣件断裂 | 0.94 | 0.79 | 依赖局部细节 |
| 螺栓缺失 | 0.95 | 0.82 | 背景简单,误检少 |
这里多说一句,mAP50-95比mAP50更能反映真实检测质量,因为它对框的位置精度更严格。工业场景一定要同时看这两个指标,只看mAP50容易被“框大概在附近”的假精度骗了。
推理速度上,用一块中端显卡跑1280分辨率单张图片大约耗时18-25ms,视频流大约12-15fps,跟预期一致。如果换成TensorRT做FP16加速,还有至少30%以上的提升空间,但这个坑如果不熟的话不建议第一版就上。
5.2 源码拿到手后怎么跑起来
整个工程目录大致是这样的:
rail_defect_system/ ├── app/ │ ├── main.py # 入口,启动登录界面 │ ├── ui/ # 登录窗口、主窗口代码 │ ├── core/ # 模型推理、数据处理逻辑 │ └── database.db # SQLite数据库 ├── models/ │ ├── best.pt │ └── best.onnx ├── dataset/ │ ├── train/ │ └── val/ ├── requirements.txt └── rail_defect.yaml运行前先建一个Python 3.10的虚拟环境,然后:
pip install -r requirements.txt python app/main.pyrequirements里最重要的几个包是pyqt6或pyside6、torch、onnxruntime-gpu、opencv-python、ultralytics——注意如果只用ONNX推理,PyTorch其实可以不装,但为了方便二次训练脚本,通常还是装上。
第一次启动后先注册一个管理员账号,再重新登录就能进入主界面。检测模型文件默认加载models/best.onnx,如果没有ONNX Runtime环境则自动回退到best.pt。
5.3 这个系统后续还能往哪些方向扩展
做完一版之后,我觉得后面可以扩展的方向至少有这几个:
一是接入视频流和抓拍联动,不只是单张图片和本地视频,可以对接现场的轨检摄像头,做成长时间无人值守的监控模式,异常才抓拍留存。
二是缺陷分级和尺寸估算,现在只输出缺陷类别和位置,后续可以通过标定把像素尺寸换算成实际毫米值,然后按严重程度分为轻、中、重三级,这对用户排检修计划帮助很大。
三是多路并行优化,如果同时检测左右两条钢轨,可以用两个推理线程分别在两张图上并行跑,控制好显存分配,整体吞吐能翻倍。
四是不断用现场数据做增量训练,把每次人工复核后的结果回填到训练集里,定期重训,这样系统会越用越贴合自己现场的工况。这也是我觉得工业检测项目最应该养成的习惯——算法模型不是一次性交付物,它是需要持续喂养和迭代的。
最后再说点实际操作中的体会
如果让我重新做一遍这个项目,我最想一开始就做对的事情是:先把“现场背景长什么样”搞清楚,再去设计数据集和模型。我第一次就是在公开数据集上花了大精力,结果现场测试发现背景根本不匹配,后面补样本重训浪费了很多时间。另一个体会是,深度学习检测系统能不能真正被用起来,关键往往不在模型精度,而在界面交互和部署流程顺不顺手。一个工程师愿意每张图都点开看,和嫌操作麻烦不愿意用,这两个结果对项目价值的评价是完全不一样的。这套系统的模型精度并不是天花板,但它让我把从数据标注到界面交付的整条链路走通了,后面再换场景、换模型,都只是替换中间模块的问题。