ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于YOLOv10的交通信号检测与UI界面部署实战

基于YOLOv10的交通信号检测与UI界面部署实战

1. 项目概述与方案选型

1.1 这个项目到底解决什么问题

道路交通信号检测,说白了就是让计算机在一张图片或者一段视频里,把红绿灯、交通标志这类目标找出来,并且标出它们的位置和类别。你看到的这个项目标题里最核心的几个字是“YOLOv10”,也就是说整套检测方案是建立在新一代YOLO系列模型之上的。

我在实际做这个项目之前也纠结过:明明YOLOv8已经很好用了,社区生态也成熟,为什么还要折腾YOLOv10?后来把官方文档和源码翻了一遍,发现YOLOv10在结构上做了几处关键改动,其中最有价值的一点是它采用了无锚框(anchor-free)的设计,并且把训练过程中的非极大值抑制(NMS)环节彻底拿掉了。这意味着推理阶段少了一步后处理操作,检测速度更快,部署到端侧设备或者做实时视频流分析时优势非常明显。

这个项目适合谁来参考呢?如果你正在做毕业设计、课程设计,或者想入门目标检测但不想从零开始写网络结构,那么这个项目就是很好的“脚手架”。它把数据集准备、模型训练、模型导出、UI界面展示整条链路都串起来了,你拿到之后不是看一堆零散代码,而是能直接看到“数据进、结果出”的完整闭环。另外,如果你只是想快速验证一下YOLOv10在自己数据集上的效果,这个项目的工程结构也值得借鉴。

1.2 为什么选择YOLOv10而不是其他检测方案

我最初严肃考虑过的方案有三个:传统图像处理、两阶段检测器(比如Faster R-CNN系列)、单阶段检测器(YOLO系列)。

传统图像处理这条路,比如用颜色阈值分割红灯绿灯、用霍夫变换检测圆形标志,在实验室的几张干净图片上表现还行,一旦遇到光照变化、雨雾天气、摄像头角度倾斜,效果就迅速崩盘。它本质上是靠人工设计特征,泛化能力太弱,不适合作为通用解决方案。

Faster R-CNN这类两阶段检测器精度高,但速度偏慢,一张图跑下来要好几十毫秒甚至上百毫秒。在道路交通信号检测这个场景里,视频流是连续帧,如果每帧都要等这么久,实时性就无从谈起。而YOLOv10在保持高精度的同时,推理速度能在普通显卡上跑到几十毫秒一帧,满足实时检测的需求。

YOLOv10还有个很实用的特性是模型规模可伸缩。同一个项目里,你可以根据硬件条件选择n、s、m、l、x几个不同尺寸,模型参数从两三百万到几千万不等。我的实际做法是:先用nano版本快速跑通整个流程,确认数据集没问题之后,再切到small或者medium版本追求更高精度。这种“先通后优”的策略帮我省了大量排查问题的时间。

2. 数据集构建与预处理细节

2.1 交通信号数据集从哪里来、怎么组织

做目标检测项目,数据集的质量直接决定了模型的天花板。这个项目里使用的是YOLO格式的数据集,目录结构非常标准,分成两个子目录:图片存在images目录下,对应的标注文件存在labels目录下,而且推理阶段必须确保图片和标注文件的文件名一一对应。

标注文件是纯文本格式,每一行代表一个目标,格式是“类别ID 中心点x 中心点y 宽度w 高度h”,注意这里的坐标都是相对于图片宽高的归一化数值。比如一张1920x1080的图片里,一个交通信号灯的中心点像素坐标是(960, 540),宽高是(100, 200),那么标注行就是“0 0.5 0.5 0.052 0.185”,其中宽高分别除以图片宽度和高度得到。

项目里的数据集通常包含以下几类目标:红绿灯(按颜色和形状可以细分)、限速标志、警告标志、禁令标志等。为了演示方便,我一般把类别控制在四到六个之间,太少体现不出检测的难度,太多又会在有限的标注样本上造成类别混淆。用labelimg或labelme软件标注时,输出格式选择YOLO即可。

这里有一个硬性要求要提醒:图片尺寸不要五花八门,最好是统一分辨率。我实际遇到过数据集里有竖版手机拍的图片、横版监控截图、还有方形缩略图,训练时模型输入会被统一resize到640x640,但原始标注坐标是相对原图的,比例不一致时会导致目标框位置偏移。所以拿到数据后第一件事就是统一尺寸,或者至少保证宽高比接近。

2.2 数据划分与配置文件编写

数据集准备好后,要把图片分成训练集、验证集和测试集。我习惯用8:1:1的比例,而且这个划分动作不是手动复制文件,而是通过一个简单的Python脚本生成三个txt文件,记录图片路径列表。YOLOv10在训练时通过data.yaml来确定数据位置,配置文件内容大概长这样:

path: ./datasets/traffic_signal train: images/train val: images/val test: images/test nc: 5 names: ['traffic_light_red', 'traffic_light_green', 'traffic_light_yellow', 'speed_limit', 'warning_sign']

这里有个很多人容易忽略的细节:path字段要写数据集的根目录,然后train和val是相对这个根目录的子路径。如果你把路径写错,训练启动时会直接报AssertionError: Dataset not found。我一开始图省事,写了绝对路径,结果换了一台电脑跑就报错,后来改成相对路径加环境变量拼接,彻底解决了迁移问题。

类别顺序names列表要和标注文件里的类别ID一一对应,这个顺序一旦在训练中途修改,会导致之前训练的模型权重全部作废。所以建议在项目启动第一天就把类别清单定死,做好记录,不要今天调一下顺序明天加一个类别。

2.3 数据增强策略:不要盲目堆量

YOLOv10训练时默认会开启一系列数据增强策略,包括随机翻转、缩放、色彩抖动、马赛克增强等。马赛克增强很有意思,它会把四张训练图片拼接成一张,让模型在小目标检测上表现更好,因为每张子图的物体都缩小了,等于强迫模型去学习更小的目标特征。

但是增强不是越多越好。我实测过在交通信号数据集上开启全部默认增强,训练出来的模型在晴天白天场景效果不错,一遇到阴雨天就明显变差。原因在于色彩抖动把色相偏移拉得过大,让模型对红色和绿色的区分变得不稳定。红绿灯检测本质上对颜色非常敏感,所以调低hsv_h、hsv_s这两个增强参数是必要的。

我的建议是,第一轮训练沿用默认增强参数跑通流程,然后根据验证集上的表现针对性调整。如果你发现模型对特定类别召回率低,比如黄色指示灯经常漏检,就可以稍微降低马赛克增强的概率,因为马赛克会让黄色目标被裁切或遮挡的概率变大。

3. 模型训练核心流程与参数调优

3.1 环境配置的常见坑

在训练之前要把环境准备好。项目要求Python 3.8以上、PyTorch 1.8以上,CUDA版本建议用11.8或者12.1。我踩过最深的坑是PyTorch和CUDA版本不匹配,训练时直接报CUDA error: no kernel image is available,折磨了一个晚上才意识到是GPU驱动支持的最高CUDA版本低于PyTorch编译时的版本。

安装项目依赖时,我建议用虚拟环境,不要直接往系统Python里装。命令很简单:

python -m venv yolov10_env source yolov10_env/bin/activate # Windows下是 yolov10_env\Scripts\activate pip install -r requirements.txt

如果条件允许,直接使用官方提供的预训练权重作为初始权重会省很多时间。比如用yolov10n.pt作为起始点,在自己的数据集上微调,通常几十个epoch就能达到理想效果。从零训练不是不可以,但需要更多数据和更久的训练时间,而且效果未必更好。

3.2 关键训练参数怎么定

训练命令看起来很简单:

yolo train data=data.yaml model=yolov10n.pt epochs=100 batch=16 imgsz=640

但每个参数背后都有讲究。首先是imgsz,模型输入尺寸,默认是640。如果你专门做小目标检测,可以考虑提升到960或1280,精度会上去,但显存占用和推理时间也会同步上升。交通信号灯在监控画面里通常占比较小,如果硬件条件允许,用imgsz=960训练是值得的。

其次是batch,这个参数直接决定显存占用。我用的显卡显存是8GB,跑yolov10n时batch=32没问题,但切到yolov10s之后,batch=32直接OOM(显存溢出)。我的经验是先从batch=8试起,用nvidia-smi查看显存利用率,再逐步往上加。YOLOv10在训练时支持自动批量大小调整,但我不太依赖这个功能,手动确认更踏实。

最后是epochs,太多容易过拟合,太少欠拟合。对小数据集(几千张图),100个epoch足够看到收敛趋势。训练过程中每隔10个epoch保存一次权重,我一般通过验证集mAP曲线判断什么时候该停。如果连续20个epoch mAP不再上升,就果断停掉,别让显卡干烧电。

3.3 训练过程的监控与断点恢复

训练日志里有两项指标需要重点盯:一个是box_loss(边界框回归损失),一个是cls_loss(分类损失)。正常情况下,这两个损失值应该是震荡下降的,如果出现某个指标突然抬升,多半是学习率设置过高或者数据里混入了标注错误。

训练中断是很常见的情况,比如断电、显卡驱动崩溃、手动停止。YOLOv10训练过程中会自动保存最近的权重文件,下次训练时加上resume=True参数就能断点续跑:

yolo train resume=True model=runs/detect/train/weights/last.pt

这里有个小技巧:断点续跑之前,确认一下训练日志里的epoch编号,别重复加载一个已经很接近收敛的模型继续跑几百个epoch,那样只会浪费时间。

我在第一轮完整训练中大约用了四十分钟(8000张图,100个epoch,nano模型),最终验证集mAP50达到0.82左右。这个成绩对于演示级项目已经比较体面了。如果你希望进一步提升,可以试着给每个类别增加标注样本,尤其是那些漏检频繁的类别。

3.4 模型验证与性能评估

训练结束后不要急着部署,先用验证集看一眼效果。YOLOv10的验证命令是:

yolo val model=runs/detect/train/weights/best.pt data=data.yaml

输出结果里需要关注mAP50和mAP50-95两个数字。mAP50是指IoU阈值在0.5时的平均精度,反应基础框定位能力;mAP50-95是不同IoU阈值下精度的平均值,对框的精确度更敏感。交通信号检测场景里,因为目标比较小有时还会互相遮挡,mAP50-95偏低是正常的,不必过度焦虑。

除了看数字,还要实际跑几张验证集图片,可视化成检测结果,用肉眼看有没有明显的错检漏检。我遇到过一种情况:模型把红色汽车尾灯误检成红灯,就是因为训练数据里红灯的样本太少,模型没有学够区分特征。这类问题光看mAP数字发现不了,必须肉眼检查。

4. UI界面设计与系统集成

4.1 UI界面的技术选型

模型训练好之后,下一步就是把检测能力包装成一个能交互的界面,这也是很多课程设计或展示项目的硬性要求。标题里“UI界面”四个字看似简单,实际上很多人在这里卡壳:不知道用哪种GUI框架,不知道如何把模型推理和界面刷新整合起来,不知道图片选择、视频流播放、检测结果展示这些模块怎么协同。

我的选择是Python生态里最成熟的PySide6,也就是Qt for Python。相比Tkinter,它的控件更丰富,布局更灵活,支持样式表美化,做出来的界面更像一个正式的产品;相比Kivy或pywebview,它的文档教程更多,遇到问题时也更容易在社区找到答案。

界面整体设计分三块区域:左侧是控制面板,包含打开图片、打开视频、开始摄像头检测的按钮;中间是视觉检测区域,显示原始画面和检测框叠加结果;底部是信息栏,展示检测到的类别、置信度、耗时帧率等实时数据。这种布局方式思路清晰,用户拿到手就知道怎么操作。

4.2 UI推理线程怎么设计才不会卡顿

这可能是整个UI开发里最容易踩坑的地方。很多人会直接在UI主线程里调用模型推理,结果界面一运行就“假死”,按钮点不动,视频画面一卡一卡像幻灯片。原因很直接:模型推理是耗时操作,主线程被阻塞后的事件循环无法响应界面操作。

解决办法是利用QThread把推理任务放到后台线程,推理完成后通过信号把检测结果传回主线程更新界面。我用PySide6的QRunnable加QThreadPool实现了这个逻辑,核心代码大致如下:

class DetectionWorker(QRunnable): def __init__(self, model, frame): super().__init__() self.model = model self.frame = frame self.signals = WorkerSignals() def run(self): results = self.model.predict(self.frame) annotated_frame = results[0].plot() self.signals.result.emit(annotated_frame)

信号槽机制是Qt的看家本领,简单说就是后台线程完成后,把结果通过信号“丢”给主线程的槽函数,主线程收到后再更新界面。这样视频流检测时,界面依然能流畅响应,用户也不会觉得程序卡死了。

4.3 图片、视频、摄像头三种输入方式怎么统一

整个UI项目最容易失控的地方就是输入源不一致。图片是一帧静态图,视频是连续帧流,摄像头是实时帧流,三者各自处理方式略有不同,但归纳起来其实都是“取一帧,送进模型,得到结果”的循环。

我的做法是写一个统一的处理函数:

def process_frame(self, frame): rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = self.model.predict(rgb_frame, conf=0.4, imgsz=640) annotated_frame = results[0].plot() return annotated_frame

图片模式下,读取后调用一次这个函数,把标注结果直接显示出来;视频模式下,通过cv2.VideoCapture循环读取每一帧,喂给这个函数再显示;摄像头模式本质上一样,区别在于VideoCapture的参数从文件路径换成摄像头索引号。

这里注意一个颜色通道问题:OpenCV默认读入的是BGR格式,而YOLOv10内部处理的是RGB格式。如果你不转换就直接推理,检测框位置基本没问题(因为都是归一化坐标),但可视化叠加时颜色会错乱,画面像开了复古滤镜一样偏蓝偏红。加一行cv2.cvtColor就能解决。

4.4 UI界面的几个小细节

检测置信度阈值在UI里要暴露给用户调节。模型训练好后默认的置信度阈值是0.25,但这个值在交通信号场景下常常显得太低,会出现很多低置信度的误检框。我测试下来,阈值调到0.4到0.5之间,误检明显减少,漏检没有显著增加。UI里加一个滑块控件让用户动态调节,是最实用的方案。

另外一个细节是展示帧率。有些初学者不知道,帧率是衡量实时性的关键指标。我在推理循环里记录了每帧耗时,UI底部实时显示“当前帧率:25FPS检测耗时:35ms”。这个数据在演示答辩时很有说服力,能直观展示系统的性能优势。

5. 项目部署与常见问题排查

5.1 常见报错信息速查表

项目做多了总会遇到各种各样的问题。我整理了一张针对这个项目的高频问题速查表,供大家参考:

报错现象可能原因解决办法
torch.cuda.OutOfMemoryErrorbatch过大或模型过大显存不够降低batch或切换更小模型
训练时Dataset not founddata.yaml路径错误用环境变量拼接绝对路径
推理时plot()方法报错模型版本与调用API不一致检查YOLOv10版本,升级到最新
UI界面显示英文检测框模型类别名没有本地化在names列表里用中文替换
摄像头打开后无画面索引错误或权限问题换索引号或检查摄像头授权
视频检测很卡每帧都做不必要的图像变换减少重复转换,用队列缓冲帧

5.2 显存溢出问题的最优解法

我们在batch那部分提到过显存溢出,这里展开说。如果你的显卡显存只有6GB左右,跑yolov10s以上模型时非常容易OOM。除了降低batch之外,还有一个更聪明的办法:在训练配置里开启梯度累积。

梯度累积的思路是,虽然每次只喂4张图(batch=4),但连续累积4个batch的梯度后再更新一次权重,效果上等价于batch=16。这样做训练时间会稍微长一点,但显存压力大幅下降。YOLOv10的配置文件中accumulate参数就是控制这个的,4张图的batch配合accumulate=4,和16张图单batch的训练效果几乎一致。

如果推理阶段想要更低的显存占用,可以把模型导出为TensorRT格式。TensorRT是显卡厂商推出的推理加速引擎,它会对模型做层融合、精度校准等优化,推理速度能提升好几倍,显存占用也同步降低。导出命令不复杂,但会多引入一些依赖,建议在项目完全跑通后再折腾这个。

5.3 模型权重加载失败的解答

有同学留言说best.pt加载后推理结果全是空白框,或者直接报AttributeError,仔细一看是加载了错误的权重文件。这里要区分三种文件:训练时的last.pt是最近epoch的权重,适合断点续训;best.pt是验证集表现最优的权重,适合推理部署;而官方预训练权重(比如yolov10n.pt)是在COCO数据集上训练的,类别和交通信号数据集的类别不一致,直接拿来做推理肯定不对。

正确流程是把best.pt作为最终模型文件使用,并且在代码里写清楚路径,不要图方便填了训练过程中临时保存的last.pt。如果是从网上下载别人的模型权重,要确认它的类别列表和自己预设的一样,否则检测框会有,但类别名字全是乱的。

5.4 漏检误检的排查技巧

模型训练好之后如果发现漏检情况比较多,不要立刻怀疑模型结构,先从数据找原因。我的排查顺序是:先在验证集上跑一遍,把漏检图片单独拎出来看,观察目标是不是过小、被遮挡、低光照或者类别样本严重不均衡。

如果是小目标漏检,考虑把训练输入尺寸从640提高到960;如果是低光照场景,考虑在数据增强里加入亮度调整;如果是类别不均衡,最简单的解决方法是给少数类增加复制粘贴的增强。我处理过一个案例:黄色指示灯样本只有绿色的二十分之一,模型专门漏黄色,最后通过在训练集里对黄色样本做随机旋转加翻转复制,把样本量补到接近绿色的一半,漏检率立刻下来了。

5.5 推理性能优化的最后一道工序

性能优化涉及两个层面:显存占用和推理速度。在UI界面模式下,我建议使用半精度推理。YOLOv10加载模型时可以通过参数控制,使用fp16模式,推理速度几乎翻倍,精度损失可以忽略不计。做法很简单:

model = YOLO("best.pt") model.fuse()

融合操作把卷积层和批归一化层合并,减少计算量。这个操作不需要重新训练,加载权重后直接调用即可。融合之后再做个简单的基准测试,你会发现单帧推理时间明显缩短,UI界面的实时性更有保障。

6. 项目完整测试实录与复盘

6.1 一个完整场景的检测效果实录

我用这个项目跑过一段路口的监控视频,长度约两分钟,场景包括晴天日间、树荫遮挡、车辆排队起步等常见情况。使用yolov10s模型和640输入尺寸,在普通显卡上平均帧率能稳定在45FPS左右,红绿灯的检测基本没有漏检,限速标志识别偶有抖动但整体稳定。

印象最深的是视频中间有一段公交车遮挡了红灯的画面。常规情况下被遮挡目标确实无法检测,但模型在目标重新露出来的前三帧内就重新锁定了,这得益于YOLOv10训练时的多尺度特征融合机制,模型对目标被短暂遮挡的情况有一些“记忆”能力。实用中这种表现是合格的。

6.2 项目还能怎么扩展

这个项目的可扩展方向很多。如果你手头有车路协同相关的课题,可以在检测基础上增加目标跟踪模块,比如匹配车辆与信号灯的关系,判断某辆车是否闯红灯;可以接入串口或网络协议,把信号状态输出给外部设备;也可以把模型量化后部署到 Jetson 这类边缘设备上,做成真正的嵌入式路侧感知终端。

如果是在校学生,UI界面这块还可以继续美化,比如加一个统计面板展示每分钟检测到的目标数量曲线,或者增加一个“导出检测报告”功能,把一段视频的检测结果按帧整理成表格,答辩演示效果直接上一个档次。

我个人做完这个项目最大的体会是,一个完整的检测系统,模型训练只占三分之一的精力,剩下的时间都在和数据、代码环境、界面交互做搏斗。很多同学拿着模型训练完就觉得结束了,其实真正拉开差距的是工程化能力——把模型稳稳当当地放进一个能看能点的界面里,让真实用户感受到人工智能确实在干活。

最后分享一个小技巧:在UI界面里加一个“模型版本”标签栏,记录当前加载的权重名称和训练时间。这个细节成本极低,但当你同时调试好几个模型或者过几天再来做演示时,就能快速定位当前用的是哪个模型,不用再靠猜。项目迭代过程中,这种小习惯能省下大量不必要的困扰。

RELATED READING

延伸阅读

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