恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
YOLOv10道路交通信号检测系统实战:从数据到UI的完整链路
首页
资讯中心
/
YOLOv10道路交通信号检测系统实战:从数据到UI的完整链路
YOLOv10道路交通信号检测系统实战:从数据到UI的完整链路
发布时间:2026/10/17 11:15:01
忙了大半个学期一个基于深度学习YOLOv10的道路交通信号检测系统总算跑完整条链路从数据处理、模型训练到带UI界面的可运行程序没有烂尾。这套系统做的事情不复杂输入一段视频或摄像头画面实时框出红绿灯、箭头灯、倒计时牌这类信号目标标出类别和置信度。数据以开源的交通信号数据集为基础自己补了一批白天、夜间、逆光场景的样本训练好的模型打包进PyQt5界面整套Python工程和权重文件都整理在项目目录里。写这篇文章有两个原因。一是这个项目从选型到落地踩了不少文档里查不到的坑值得记录下来二是最近总有人在群里问道路交通信号检测不是老场景了吗换YOLOv10图什么还有人训练时觉得mAP够了就收工结果一放到真实路段上立刻露馅。我打算把整个项目里关键决策的“为什么”原原本本讲清楚包括YOLOv10的选型逻辑、数据怎么清洗、类别怎么定、训练参数怎么调、界面封装时最容易卡死的地方以及真实测试中模型会翻哪些车。1. 为什么是YOLOv10交通信号检测的几个硬约束1.1 交通信号检测的三个特殊之处做视觉检测的都知道通用目标检测拿COCO预训练权重一跑就能出框但交通信号灯这个方向不是那么简单。它有三个特别明显的约束直接决定了模型选型。第一个约束是实时性。信号灯检测最常见的落地场景是车载辅助驾驶和路口监控分析前者要求毫秒级响应后者往往要同时处理四路甚至八路画面。我早期用某旧版模型做过测试在中等显卡上单帧推理到没问题但接到多路视频流的边缘盒子上帧率立刻掉到十几帧监控场景根本没法用。YOLOv0系列从v5开始就主打高效到v10这一代在速度上又往前走了一步。第二个约束是小目标。一张1920x1080的路口画面远处信号灯在图像里往往只占二三十个像素连整张图的百分之一都不到。常规检测器对这类目标很容易漏检这也是为什么很多通用模型跑得好好的一换到交通场景就失灵。第三个约束是误检代价高。把路灯杆上圆形装饰物误判成红灯或者在夜晚把车尾灯当成红灯在辅助驾驶场景里都可能引发错误提示。这类场景要求模型对“形状颜色发光状态上下文”同时建模不能只靠单一特征。这三个约束叠加起来能选的空间其实不大。YOLOv10这一代最核心的变化恰好就是在实时性和后处理简化上动了刀所以我一上来就把候选方案锁定在它身上。1.2 YOLOv10与常见版本的对比为了说服团队里习惯用老版本的同学我做了一个非常朴素的对比把大家关心的几个维度列在一张表里模型版本推理是否依赖NMS端到端训练参数量单帧推理速度同硬件小目标表现YOLOv5需要否较大s型号约7M基线一般靠大分辨率硬顶YOLOv8需要否中等s型号约11M比v5略快有提升但不彻底YOLOv10不需要是更小n约2.7M、s约7.2M明显更快配合无NMS机制表现更稳表格里参数是官方公开数据单帧速度基于我自己的GTX 3060实测不保证所有人复现一致但趋势是稳定的。YOLOv10的n号模型轻到可以跑CPU部署s号模型在显卡上处理单帧只要个位数毫秒这对需要同时分析多路画面的场景非常友好。1.3 NMS-free到底是什么为什么对信号灯重要YOLOv10最吸引我的不是名字而是它把传统的NMS后处理整个去掉了实现了真正的端到端检测。这事得解释一下因为很多同学只看到“更快”这个结果不知道底层原因。传统的YOLO系列推理时要对同一个目标产生的多个候选框做非极大值抑制NMS简单说就是先让模型“大声说出所有可能”再从一堆框里人工挑一个最准的。这个过程要调阈值阈值高了容易漏框低了容易重复框。而信号灯往往成排出现在横排信号灯上三个灯珠紧挨在一起框与框之间重叠度高NMS一压就可能把中间的灯压丢。YOLOv10的解法是双标签分配训练时用一个分支给模型“喂很多候选框”做监督推理时用另一个一对一匹配分支直接输出唯一框。类比一下就像开会时大家先自由发言充分讨论但最终决策人固定只有一个不会出现三个声音同时拍板的情况。删掉NMS这一步不仅省了后处理时间还让“哪个框才是真正目标”的问题在训练阶段就被学进了网络参数里推理时更干净、更果断。这一点放在信号灯检测里尤其香。我后续测试中对比过YOLOv8和YOLOv10处理同一段夜间路口视频YOLOv8在信号灯密集区域偶尔出现重复框而YOLOv10几乎没有这个问题。2. 数据折腾实录公开数据集、自标注与类别设计的取舍2.1 网上数据集的第一眼类目杂乱与小目标为主模型能不能用七成功夫在数据。我一开始图省事直接把几个开源交通信号数据集下载下来合并结果发现效果奇差后面检查才发现问题不在模型在标签。公开数据集的问题是“看起来多实际很杂”。不同数据集的标注口径完全不同有的只标红绿黄三种圆灯有的把箭头灯单独拆开有的连倒计时牌都算成一类标注框有的沿灯珠边缘框得很紧有的把整个黑色灯壳和部分灯杆都框进去了。最离谱的一份数据里矩形框的面积是灯珠本身的五倍大模型学到的是“一整片黑色区域是红灯”换个路口立刻失效。清洗是第一件正事。我写了个小脚本统计所有标签框的宽高分布发现大量框只有20x20像素甚至更小整张图里信号灯的像素占比中位数不到0.3%。这就解释了一个现象为什么很多人在机器人数据集上随便跑跑就90多mAP放到交通信号场景精确率就掉到一半。解决小目标问题的第一板斧不是换网络而是把分辨率提上去、把框的质量提上来。2.2 类别设计拆得细还是合着用类别设计是决定后续模型上限的关键也是最容易被忽略的环节。我见过有人把所有信号灯统一标成一个“traffic_light”类训练完也能框出灯的位置但完全回答不了“这灯是红的还是绿的”这个问题——对信号检测来说等于白做。我最终按语义拆成了六类red、green、yellow、red_left、green_left、countdown。箭头灯和圆盘灯分开是两个原因一是箭头灯的形状更尖锐和圆盘灯的特征分布差异明显二是实际路口存在“圆盘红灯左转箭头绿灯”同时亮的情况如果都塞进red类模型会困惑到底该输出一个框还是两个框学习信号会互相打架。如果数据量不够我的建议是至少把红绿黄三色拆出来箭头类先并进去后期再补数据拆细。拆太细而每类样本只有几十张模型容易过拟合这个度要自己权衡。还有一个细节标注框要贴灯珠而不是贴灯壳。红灯亮起时红色区域才是有效特征框进黑色外壳相当于给模型注入了大量背景噪声。我用标注工具批量修正了大约两成标签框的边界训练后验证集表现立刻有明显提升。2.3 数据增强组合mosaic之外还得补什么YOLO系训练默认带mosaic、mixup、HSV变换等增强策略但对交通信号灯场景默认参数不够。我用了一个很土但有效的方法小目标复制粘贴。统计出所有灯珠类别的平均尺寸后把训练集中真实的小尺寸信号灯裁剪出来随机粘贴到同一图像的合理位置比如路口上方、杆件附近同时贴上对应标签。这相当于给罕见小目标做了几十倍的过采样极大缓解了远距离漏检问题。白天场景我做得比较多真正难的是晚上和逆光。默认HSV增强只能调颜色饱和度模拟不了夜晚“灯珠自发光、周围漆黑”的对比度关系。我额外加了随机Gamma变换把图像整体亮度压低同时保留灯珠的亮度峰让模型学会在暗背景中靠发光特征找目标。还有一个容易犯错的点HSV的色相增强范围如果调太大红灯会被增强成橙黄色甚至黄色造成类别混淆。我最后把H通道的增强幅度压到原来的三分之一。3. 训练到能用的完整流程参数、命令与监控点3.1 环境版本对照项目环境我记录得很清楚方便复现。训练机是一张12GB显存的显卡系统是UbuntuPython用3.10PyTorch用官方稳定版对应版本CUDA版本和显卡驱动匹配好之后就不再乱动。装好依赖之后直接把YOLOv10官方仓库克隆到本地进入目录装requirements就可以准备训练。这里给一个提醒显卡驱动版本和PyTorch的CUDA编译版本必须对上否则一跑训练直接报“CUDA error: no kernel image”。我自己第一次训练就卡在这上面折腾半天重新装驱动才好。如果只是做推理CPU也能跑但训练阶段强烈建议至少准备一张6GB以上显存的显卡。3.2 data.yaml与标签整理YOLOv10的训练数据格式和YOLOv8一致整理成如下目录结构traffic_light/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/标签文件是txt每一行分别是类别id 归一化中心x 归一化中心y 归一化宽 归一化高。转换脚本这一步必须仔细我见过不少人把坐标除以了图像高度却忘记乘宽度导致所有框画歪。转换完成后我用随机抽样的方式把训练集中的图像和标签叠在一起画框预览肉眼确认没有错位才继续。data.yaml内容很简单核心是给模型交代“有多少类、类别叫什么、数据在哪”path: ../datasets/traffic_light train: images/train val: images/val names: 0: red 1: green 2: yellow 3: red_left 4: green_left 5: countdown3.3 训练命令与超参数我的训练命令大概是这样的yolo detect train \ data../datasets/traffic_light/data.yaml \ modelyolov10s.yaml \ pretrainedyolov10s.pt \ epochs200 \ batch16 \ imgsz640 \ device0几个参数的取舍。imgsz我为什么选640而不是更大因为信号灯是小目标理论上分辨率越高越有利试过1280训练mAP确实涨了2个多点但推理速度掉到没法实时的程度。最后折中选了640用前面说的小目标复制粘贴增强来弥补分辨率不足。epochs设200看起来多但在中等数据集规模下前50轮loss下降很快后面基本是小幅震荡。我会把早停耐心设大一些避免模型还没收敛就被打断。batch要看显存12GB跑yolov10s的640输入batch为16比较稳再往上容易OOM这时候可以顺手开梯度累积等效拉大batch。3.4 训练过程监控的三个重点训练时我基本不看终端刷屏只看三个东西。第一loss曲线是否平滑下降。如果出现明显的断崖式nan十有八九是学习率设大了或标签里出现了异常值。第二验证集上每类别的召回率。只看总体mAP会掩盖“某一类完全没学会”的问题我训练中就发现yellow类别一直只有其他类一半的召回率后来补了一批黄灯样本才算正常。第三每隔二三十轮手动拿一段真实路口视频去测直接看画面里输出稳不稳。这个习惯帮我提前发现了好几次“指标还行但画面乱跳”的问题。训练结束后保存的best.pt放在runs/detect/train/weights/目录下后续所有部署测试都用这个权重。4. mAP 0.93之后我发现模型还在犯低级错误4.1 指标好看与错检并存第一次训练完验证集mAP0.5到了0.93mAP0.5:0.95也有0.81说实话这个指标在目标检测里已经算好看。但把模型接到真实路段视频上时问题立刻暴露它会把白天远处红色车尾灯当成红灯把路边商店的红色发光招牌字当成红灯甚至偶尔把刹车灯串误判成成排的信号灯。原因并不难想。车尾灯和红灯的特征高度相似——都是圆形、红色、发光、位置可能在画面高处比如公交车尾灯。模型在训练集里见过的大量红灯都是黑色底壳包着亮色灯珠它并没有真正学会“灯珠必须被规则外壳包裹”这种高层语义。4.2 难例挖掘的两轮介入指标不错但实测错检对应的手段就是难例挖掘。我把跑视频时所有置信度大于0.3却是误检的帧截取下来总共挑出三千多张重新标注后单独成一个难例集。这里有个关键操作难例不能一股脑全混进原始训练集。我在第二轮训练中把难例集和原始训练集按大约1:3比例混合同时把误检目标标成对应负类别比如把“车尾灯”单独标了一个类叫taillight。这样模型不是简单丢掉这些特征而是学会区分“红灯”和“像红灯的东西”。第二轮微调直接加载第一轮训练出来的best.pt只跑了30轮学习率调低到之前的十分之一。效果非常明显车尾灯误检率下降了大半但随之而来的是对真正红绿灯的召回率稍微掉了一点。于是又做了第三轮把少量原始训练样本重新掺回去拉回召回率。最终权重在保留0.92 mAP的同时误检明显减少。4.3 导出、量化与速度测试功能验证完毕我开始折腾部署格式。YOLOv10官方支持导出ONNX我用下面命令转了一次yolo export modelbest.pt formatonnx opset12导出后在不同后端做了速度对比。原始PyTorch权重在GPU上单帧大概5毫秒转成ONNX后稍微再快一点再往上可以尝试TensorRT的fp16量化帧率还能提升但需要装额外依赖。如果只是做界面演示工具PyTorch权重完全够用如果奔着多路并发去建议至少走到ONNX级别。5. 把模型装进用户界面PyQt5封装中的关键线程设计5.1 界面功能与模块划分模型训练好了只是第一步项目交付还要有能给人点开的界面。我选了PyQt5做桌面端原因很简单Python生态成熟、拖控件方便、和OpenCV/YOLO的集成成本最低。界面按功能划分了三个模块图片检测、视频文件检测、本机摄像头实时检测。图片检测支持单张选择和多张批量选择结果直接展示在右侧画布上左侧显示检测统计。视频检测和摄像头实时检测共用一个播放窗口下方是帧率、日志和“保存当前帧”按钮。没有一开始就堆花哨功能核心是让用户能“不看代码”完成从打开文件到看到检测结果的完整操作。检测结果画框时我按类别分配了固定颜色红灯红色、绿灯绿色、黄灯黄色、倒计时蓝绿色方便一眼识别。5.2 为什么单独拉一个推理线程界面开发里最典型的坑就是直接在按钮回调里跑推理。我第一次就是这么写的界面一拖视频就整个转圈鼠标点哪都没反应因为重活全部卡在UI主线程里事件循环被阻塞了。解决方法是用QThread把推理过程放到独立线程UI线程只负责接收结果并刷新画面。线程之间通过信号槽通信界面按钮点了“开始检测”主线程发一个信号给推理线程推理线程不断读取视频帧、跑模型、把带检测结果的帧通过信号发回主线程主线程只画图。这个设计最核心的价值是推理卡也好、视频读取慢也好界面始终能响应用户可以随时点“停止”而不是强杀程序。5.3 核心代码加载模型、推理与绘图推理线程的核心逻辑大概是这样的class DetectThread(QThread): frame_signal pyqtSignal(object, list) def __init__(self, model_path): super().__init__() self.model YOLO(model_path) self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running: ok, frame cap.read() if not ok: break results self.model.predict( frame, imgsz640, conf0.35, verboseFalse ) dets [] for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cls_id int(box.cls[0]) conf float(box.conf[0]) dets.append((x1, y1, x2, y2, cls_id, conf)) self.frame_signal.emit(frame, dets)主界面收到frame_signal后遍历dets画矩形框和标签。这里有个小细节不要在推理线程里同时做画图的活否则线程里既要跑模型又要画图遇到高分辨率帧会拖慢整个循环。UI线程画图是异步的即便一帧画慢了下一帧还是会继续发过来实际观感更流畅。conf阈值我是动态调的白天场景0.35够用夜晚场景建议调到0.45因为夜间的误检更多稍微牺牲一点召回换来更干净的输出用户会更信任这个系统。6. 真实路段测试的两个典型翻车场景与修复6.1 白天逆光与夜晚过曝真实路段测试和跑数据集视频完全是两码事。第一次拿着笔记本去路口测试遇到的是下午逆光场景信号灯背对太阳灯珠颜色被强光洗成一片白模型十次有八次漏检。这个问题不能完全靠调阈值解决因为信息确实丢失了。我的临时方案是数据补强专门去逆光时段录了一段约二十分钟的视频抽帧后加入训练集做一轮微调效果有一定改善。真正的根治思路是算法之外的事——在实际部署时用偏振镜或改变相机的安装角度避开阳光直射。夜间过曝是另一类问题。路口LED信号灯在暗背景下过曝红色光晕会向周围晕开模型常把这个光晕也当成灯的一部分框比实际大好圈。后处理里加一条约束过滤掉长宽比大于3的细长“红灯框”因为真实圆盘信号灯的长宽比最多接近1而灯晕往往拉成长条形。这方法虽然土但实测对过曝场景很有效。6.2 倒计时牌与黄灯闪烁静态检测搞不定的“时态问题”很多真实路口的倒计时牌和信号灯挨得很近倒计时数字显示红色“5”“3”的时候单帧画面和红灯极度相似。我遇到过连续几十帧把倒计时的“5”识别成红灯的情况这在车载场景里等于连续误报。单帧检测对这个问题无解因为它本质是一个时序问题而不是空间检测问题。我采用了一个极其简单的时序滤波维护一个红灯状态历史队列要求红灯连续出现超过5帧才输出“红灯”警报切换为绿灯时也要求连续3帧确认。这样倒计时数字虽然单帧可能误判但它只有秒级跳变无法维持连续多帧稳定就能被滤掉。黄灯闪烁也是同理。闪烁黄灯在特定瞬间看起来像黄灯常亮单帧无法判别只有拿到连续帧后才能识别闪烁模式。这也说明了一个基本事实检测算法只是系统的眼睛真正的安全决策还得靠时序逻辑过滤。6.3 一点可以继续深入的方向项目做完了能继续挖的方向其实很多。比如倒计时牌可以单独训练一个轻量OCR分支直接读出剩余秒数比单纯把倒计时牌框出来更有价值再比如把单帧检测换成轻量级时序跟踪利用信号灯的周期性变化稳定输出还有一个思路是引入多传感器融合把信号灯的配时方案作为先验信息大幅剪枝掉不可能的检测结果。不过这些方向都需要更大算力或更完整的数据链路支撑对个人项目来说先把当前这套系统做稳定、做好误检抑制比盲目加功能更能让人放心。如果在真实场景部署我会优先把“连续多帧确认”这件事做成可配置项对不同场景保留不同参数。项目收尾那几天我把整条链路又走了一遍从数据清洗脚本到训练命令再到UI界面打包确认不是“能跑就行”而是“换台机器也能复现”。做这种检测项目最深的体会是模型本身只占整个系统的一小部分数据的细致程度、后处理的时序逻辑、界面的交互体验每一环都会决定最终落地效果。如果你也在磕类似的目标检测项目建议在追求指标之前先把自己数据集里的每一条标签都认真看一遍大部分让模型翻车的坑早就在数据里埋下了。