前阵子把一个“基于YOLOv8的轨道异物检测系统”从零到一整了下来,算是把数据标注、模型训练、可视化展示、PyQt5界面设计和最后的环境部署全流程都趟了一遍。这套东西听起来是个不小的工程,但拆开看其实就是“数据集 + 模型 + 界面 + 部署”四件事,真正的难点反而不在YOLOv8本身,而在数据质量和工程落地时的各种衔接细节。这篇文章把我在项目里的完整流程和踩坑记录写出来,给正在做类似巡检项目或者拿这个方向当毕设的同学一份能直接参考的实操记录。
我尽量把每个环节的“为什么”也讲清楚,比如为什么YOLO格式的标签长这样、为什么PyQt5里必须开线程做推理、为什么训练时mAP会卡住不动。你照着做至少能跑通一条完整链路,少走几周弯路。
1. 项目整体设计与思路拆解
1.1 这个系统到底要解决什么问题
轨道异物检测,说白了就是盯着铁轨或者轨道周边区域,发现有不该出现的东西就报警。场景很常见,比如轨道上掉了石块、施工工具、树枝,甚至行人闯入。传统方式是人工盯监控画面,但长时间盯屏容易疲劳,漏报风险高,所以用计算机视觉做自动检测是刚需。
这类场景有几个比较棘手的地方:
- 目标普遍偏小,比如一块砖在画面里可能就几十个像素;
- 光照变化大,顺光、逆光、夜间补光差异明显;
- 异物类别杂,同一类物体不同角度长得差异很大;
- 实时性要求高,报警晚几秒钟都可能引发问题。
选YOLOv8作为检测模型,主要是看中它在速度和精度之间的平衡。对轨道巡检这种需要快速响应、又要求能部署到普通工控机上的场景,YOLOv8的系列模型从n到x都有,量级从几兆到几十兆,按硬件水平选就行。而且ultralytics框架把训练、验证、导出封装得很顺手,不用自己造车轮子。
1.2 整体技术栈与系统组成
整个系统按数据流理解很清晰:
- 数据层:摄像头采集视频帧,或者直接读图片文件;
- 标注层:用标注工具生成带标签数据集,格式统一成YOLO的txt;
- 训练层:基于YOLOv8训练检测模型,输出权重;
- 应用层:PyQt5写界面,加载模型做实时推理,把结果画在画面里,配上告警逻辑;
- 部署层:配置好Python环境和依赖,打包成可以双击运行的应用。
这里最容易被忽略的是“数据层”和“标注层”占用的时间。我这个项目里,训练模型只花了两三天调参,但前前后后整理数据、清洗标注、核对类别花了快两周。不要觉得数据处理是杂活就糊弄过去,检测效果天花板其实是数据决定的,模型只是把这个上限尽量逼近而已。
1.3 为什么选择PyQt5做展示界面
有人会问,为什么不用Web前端或者直接用OpenCV的窗口?我自己的理由很直接:
- PyQt5是桌面应用,离线环境运行稳定,不依赖浏览器和网络;
- 后续要接声光告警、串口闸机联动这类硬件逻辑,桌面端处理起来更顺手;
- 界面控件成熟,QThread做多线程比较方便,不像纯OpenCV窗口那样什么都得自己画;
- 打包成exe在工控机上跑,比搭一套Web服务省心得多。
当然PyQt5也有缺点,比如界面样式比较老气,但监控类工具的界面核心是数据清晰、操作简单,不需要花哨。真正让我在项目里花时间调试的不是控件布局,而是“视频解码、模型推理、界面渲染”这三者之间的线程配合。这个问题后面单独用一小节讲。
2. 带标签数据集的制作:从采集到标注
2.1 数据采集的来源与注意事项
我先说结论:数据质量比数据量重要,尤其是异物检测这种小目标场景,样本“像不像真实场景”直接决定检测效果。
数据来源大体分三路:
- 真实场景采集:用现场监控摄像头截取视频帧,覆盖不同时间段、不同天气、不同光照;
- 模拟场景拍摄:在类似轨道环境中放置异物样本,用手机或工业相机拍摄;
- 公开通用数据集预训练:先用通用目标检测数据集训练一遍,拿到预训练权重做迁移学习,再用自己的轨道数据微调。
这里面最容易踩的坑是“样本太单一”。很多项目图省事,只在同一个角度、同一个光照下拍了几百张就开训,结果测试时换个角度就漏检。我的做法是同一类异物至少保证三个角度、两种亮度、两种距离以上,宁可每类只有三四百张高质量图片,也不要一千张全是同一个视角的相似图。
负样本也要收,这是很多新手忽略的。空轨道、只有道砟、只有轨枕的画面也需要一些,让模型知道“没有异物的时候别乱报”。我最后的数据集里放了大概15%的负样本,对降低误报有明显的帮助。
2.2 标注工具与YOLO格式说明
标注工具我用过LabelImg,也试过labelme,最后固定在LabelImg配合YOLO模式直接输出txt文件。LabelImg操作简单,画矩形框后选类别就行,缺点是切到YOLO模式前要先准备好类别名。
YOLO格式的标签是一个txt文件,跟图片同名,放在同一个目录下。每一行代表一个目标,格式是:
class x_center y_center width height注意,x_center、y_center、width、height都是相对于图片宽高的归一化坐标,取值在0到1之间。类别从0开始编号。
举个例子,一张640x480的图片里,有一个石块的检测框左上角是(100, 200),右下角是(200, 300),那么它对应的YOLO标签行就是:
0 0.234375 0.520833 0.156250 0.208333计算过程:
- x_center = (100 + 200) / 2 / 640 = 150 / 640 = 0.234375
- y_center = (200 + 300) / 2 / 480 = 250 / 480 = 0.520833
- width = (200 - 100) / 640 = 0.156250
- height = (300 - 200) / 480 = 0.208333
建议标注完做一次脚本检查,主要查两类问题:一是坐标是否越界,二是某些图片漏标了。我吃过一次亏,训练到一半发现有一批图片的标签文件是空的,模型把空图当成负样本,损失曲线一直下不去,后来用脚本筛出来重新标注才正常。
2.3 数据增强、划分与验证集
数据增强我直接在ultralytics框架里配置。YOLOv8默认自带Mosaic、翻转、HSV变换等增强策略,配置文件里都可以调。我实际用下来,Mosaic对小目标检测帮助明显,但要注意,如果异物场景有很强的尺度固定性,Mosaic之后目标可能缩得很小,反而学不到稳定特征。所以我在Mosaic基础上,没有开太多亮度扰动,因为轨道场景夜间和白天差别太大,我干脆用白天数据做主要训练,夜间单独准备了一批数据做验证。
数据划分我习惯按8:1:1分成训练集、验证集、测试集。目录结构清晰一些,方便后续复用:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml内容是关键,特别是names的顺序一定要跟标注类别数字对应:
train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 3 names: ['stone', 'tool', 'person']我之前犯过一个低级错误,标注时把“tool”当成第2类,但names里写成了第一个,结果模型训练时所有类别错位,置信度看起来很高,检测框位置也对,但类别全乱了。这种问题光看loss曲线根本发现不了,一定要在训练前去验证集上跑一次,肉眼检查预测的类别是否跟实际目标一致。
3. 模型训练与可视化调优
3.1 环境准备与训练脚本
训练环境我用的是conda创建独立环境,避免不同项目之间的包冲突。主要的包是ultralytics、torch、opencv-python。如果你有NVIDIA显卡,先装好CUDA和cuDNN,再用GPU版本的PyTorch。装完后可以用一行代码检查显卡是否可用:
import torch print(torch.cuda.is_available())输出True才能用GPU训练,否则就在默默用CPU硬扛,训练速度会慢到怀疑人生。
训练脚本用ultralytics的Python接口写,比命令行直观一些:
from ultralytics import YOLO model = YOLO("yolov8n.pt") model.train( data="dataset/data.yaml", epochs=100, batch=16, imgsz=640, workers=4, device="0", )这里有个选型问题。YOLOv8有n、s、m、l、x几个版本,我项目里先用了n跑通流程,后面换成s,因为轨道异物检测对精度要求更高,但工控机性能有限。如果你显存充足、推理设备也够强,可以上m。但从工程角度,我建议先小模型跑通,再根据实测效果升级模型,不要一上来就搞l或x,训练和部署都痛苦。
3.2 训练过程指标与可视化结果解读
训练结束后,ultralytics会在run目录下生成一堆可视化文件,其中最常见的是results.png和confusion_matrix.png。新手拿到手最容易蒙圈,我一个个说。
results.png里有几根曲线:
- train/box_loss:边框回归损失,正常是下降趋势;
- train/cls_loss:分类损失,下降且最终收敛;
- val/cls_loss、val/box_loss:验证集上的损失,比训练集损失略高是正常的;
- metrics/precision、metrics/recall:精确率和召回率,越高越好;
- metrics/mAP50、metrics/mAP50-95:核心指标。
我看模型的习惯是先看mAP50,也就是IoU阈值0.5下的平均精度,再看mAP50-95,后者更严格、对框的定位精度更敏感。对于异物检测这类偏“找出目标”的任务,mAP50达到90以上就是可用的状态。mAP50-95受小目标影响大,轨道场景里目标偏小,这个指标如果低于50也不用太慌,重点看实际测试效果。
confusion_matrix.png可以看看哪些类别之间容易混淆。我项目里“tool”和“stone”在远距离时经常互相认错,因为小尺寸下纹理信息丢失,两者轮廓又相似。后来我通过增加远距离样本,以及把置信度阈值调高,误报才降下来。
3.3 调参与常见误区
很多新手喜欢到处调超参,我自己的经验是:大部分参数保持默认就可以,重点处理以下几个:
- imgsz。轨道目标小,我建议至少用640,如果显存允许,试试768或896,小目标检测会有明显提升。但推理速度会下降,要平衡。
- batch。显存不够就调小,比如从16降到8。不要因为batch小就慌了,YOLOv8自动学习率调度会帮你补偿一部分。
- early stopping。ultralytics默认有早停机制,patience设为默认就行。不要一看loss降得慢就手动中断,给训练多留一点耐心。
- 数据增强。只在效果不稳的时候调,不要一开始就开太多花活。
还有一个典型误区是:“训练集损失一直降,但mAP不动”。这通常是数据问题,比如某个类别样本太少,或者标注框不准确。别急着加深模型,先回到数据层面查一下。
3.4 导出模型并测试
训练完的权重文件是best.pt,部署的时候我习惯导出成ONNX格式,因为ONNX在CPU上推理速度比原始PyTorch模型快,而且后续可以转成其他平台格式。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") model.export(format="onnx", imgsz=640)导出后才能拿真实场景测试一波。用一段简单的推理代码,把检测结果画到图片上:
from ultralytics import YOLO model = YOLO("best.onnx") results = model.predict("test_01.jpg", conf=0.25) result = results[0] result.save(filename="output.jpg")这一步看起来简单,却在项目里很关键。用PyQt5做界面时,推理部分其实就是这个predict调用,只不过从摄像头取帧、循环调用、实时绘制。如果你在这里能稳定输出检测结果,那后面做界面就顺了。
4. PyQt5界面设计与可视化展示
4.1 界面布局、交互与组件
PyQt5界面的核心是“控制端”和“显示端”分离。我是这样布局的:
- 左侧主区域:QLabel显示实时画面,检测框和类别文字直接画在帧上;
- 右侧控制面板:
- 模式选择:本地图片、视频文件、摄像头;
- 开始/停止按钮;
- 置信度阈值滑条;
- 告警状态指示灯;
- 日志输出区(显示检测记录、当前帧率、告警信息)。
控件整体用QVBoxLayout和QHBoxLayout组合起来就行,布局代码本身不复杂。真正要注意的是,不要把检测逻辑塞进UI事件里。比如你在“开始”按钮的点击槽函数里直接写一个while循环读摄像头,那么界面会在循环期间完全卡死,窗口拖不动、按钮点不了。
4.2 实时视频推理线程设计
PyQt5的界面运行在主线程,也就是事件循环线程。所有耗时操作,比如摄像头读取和解码、YOLO模型推理,都应该放到子线程,子线程处理完再通过信号把结果传回主线程更新界面。
我用的方案是创建两个QThread子类,也可以合并成一个:
- VideoThread:负责读取视频流/摄像头,把原始帧信号发给DetectThread;
- DetectThread:收到帧后做模型推理,再把绘制了检测框的帧信号发回主线程显示。
主线程收到信号后用setPixmap更新QLabel即可。线程之间传递数据用pyqtSignal,内部用队列缓冲,避免积压。
这里有一个细节:模型第一次推理时需要加载权重、做预热,首帧耗时明显长于后续帧。所以启动时可以先丢一帧给模型“暖机”,不然界面显示的可能是延迟很大的画面。
4.3 画面绘制与检测结果展示
OpenCV默认读出来的图像是BGR格式,而Qt显示要的是RGB,中间需要一个转换。这一行代码我几乎每次都要写,贴出来方便直接抄:
from PyQt5.QtGui import QImage rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qt_image = QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) pixmap = QPixmap.fromImage(qt_image)检测框的绘制我建议直接在OpenCV上完成,然后再转成QImage。不要在Qt侧重新绘制框,那样又要管理坐标映射,容易出偏差。YOLOv8的predict结果里有boxes.xyxy,取出坐标后直接用cv2.rectangle画就行。
告警逻辑可以这样设计:当检测到某类异物且置信度超过阈值时,通过信号让主线程把状态指示灯的样式改为红色,并在日志区写入告警记录。如果项目还要做声光联动,可以在信号触发的位置预留一个硬件控制函数接口,后续接串口或GPIO都方便。
5. 环境配置与常见问题排查实录
5.1 环境部署清单
最后部署阶段,环境配置是个小门槛。我整理一份当前项目里用到的核心依赖清单:
| 依赖 | 版本建议 | 说明 |
|---|---|---|
| Python | 3.10 | 兼容性好,PyQt5和ultralytics都支持 |
| PyQt5 | 5.15.x | 稳定版本,直接用pip装 |
| ultralytics | 8.x | YOLOv8训练和推理框架 |
| torch | 2.x + CUDA | GPU训练必需,CPU部署可降低 |
| opencv-python | 4.8+ | 图像读取、处理、绘制 |
| onnxruntime | 1.16+ | CPU/GPU推理ONNX模型 |
建环境的命令也很简单:
conda create -n rail_env python=3.10 conda activate rail_env pip install pyqt5 ultralytics opencv-python onnxruntime如果要用GPU训练,再单独装对应版本的torch,不然用CPU跑一百个epoch会比较煎熬。
5.2 新手最容易踩的坑
我把实际碰到的几个高频问题整理成一张排查表,基本覆盖了从启动到运行的常见坑:
| 问题现象 | 原因 | 解决办法 |
|---|---|---|
| 程序启动后闪退,无报错 | Qt平台插件异常或OpenCV DLL冲突 | 检查环境变量,避免混装多个OpenCV版本;用纯Python环境重装PyQt5 |
| 摄像头打不开,画面黑屏 | 摄像头被占用或分辨率过高 | 先释放其他进程,设置摄像头分辨率如1280x720,降低采集压力 |
| 模型文件加载失败 | 路径含中文或空格 | 所有路径改成英文,模型文件和数据集路径都不要放桌面 |
| 训练时报CUDA out of memory | batch或imgsz太大 | 调低batch到4或8,imgsz降到640,必要时用GPU显存清理工具 |
| 推理帧率很低 | 模型太大或input尺寸太高 | 换YOLOv8n/s,导出FP16的ONNX,检测间隔加几帧 |
| 画面颜色偏蓝偏绿 | BGR和RGB没有转换 | 在QImage转换之前先做cv2.cvtColor |
| 界面卡死无响应 | 推理写在了主线程 | 用QThread把检测逻辑和UI分离 |
5.3 实测效果与优化心得
我最后用一套模拟轨道现场的视频做测试,包含三个主要类别的异物样本。整体效果是:在1280x720分辨率下,GPU推理能稳定跑到40到50帧每秒,CPU上使用ONNX大约10帧每秒;检测精度上,近处大目标基本全部能框住,远处小目标会有漏检,但置信度阈值从0.25调到0.15之后,召回率明显提升,代价是误报变多。
这里有个经验:不要只看单张图片的效果,连续视频流里的抖动更值得关注。模型在单帧里偶尔漏一两次没关系,但如果是连续性漏报,就要考虑是不是这一帧的图像出现了反光、模糊或者目标严重遮挡。针对这些情况,我加了一个简单的帧间平滑逻辑:同一个目标连续几帧都出现了,才触发告警;如果只出现一帧,则暂时记录不报警。这个逻辑大大降低了现场误报率,实现也不复杂,就是在PyQt5里维护一个目标ID到连续帧数的字典而已。
最后再分享一个小技巧:在界面里除了实时显示画面,我建议顺手加一个“保存告警截图”的功能。每次检测到有异物且置信度超过告警阈值时,就把当前帧保存到单独目录,文件名带上时间戳和类别。这功能写起来只要几行代码,但后续调试模型、复盘漏报误报时,这些截图就是最好的第一手素材。我项目里有一半的调优思路,都是靠翻告警截图找出来的。