接到某智慧交通项目任务时,对方的需求一句话说得挺轻巧:基于YOLOv8训练车辆闯红灯目标检测数据集,把闯红灯的车辆检出来,还要支持小目标检测和类别区分。我当时觉得这不就是跑一个标准目标检测管线的事吗,结果真上手之后,发现从数据集构建、标注口径、小目标优化到闯红灯判定逻辑,每一环都有大坑。这篇文章把我从零到一跑通这套方案的过程完整写出来,重点拆解小目标检测和类别区分两个核心难题,也把我踩过的坑和最终的调优经验一并分享,适合正在做智慧交通、安防监控或类似大场景目标检测的开发者参考。
1. 闯红灯检测真正难在哪里:先看清问题再动手
1.1 单帧目标检测和“闯红灯判定”不是一回事
很多初学者会把闯红灯检测理解成“检测到红灯 + 检测到车 = 闯红灯”,这是最大的认知误区。单帧目标检测的输出只有两个原料:信号灯状态和车辆检测框,而闯红灯是一种跨帧的时序行为,必须结合停车线位置、车辆轨迹、红灯持续时间才能判定。
所以我在方案设计一开始,就把整体架构拆成了两层:感知层和逻辑层。感知层由YOLOv8负责,输出车辆类别、信号灯状态和检测框;逻辑层由后处理规则负责,把多帧信息串联起来,最终产出“某车在红灯期间越线”的事件。这样拆的好处是,检测模型只做它擅长的事情,而闯红灯判定的可解释性和误报控制全部留在规则里,后续迭代时改检测和改规则互不干扰。
这个认知直接决定了后面所有工作怎么布局。如果一开始不管三七二十一,先训一个检测模型出来,再想着用单帧输出硬判定闯红灯,后面必然会被各种误检、漏检折磨到崩溃。
1.2 场景难点拆解:小目标、密集遮挡与光照变化
路口的真实监控场景对目标检测模型相当不友好,我把它归纳成四类核心难点:
- 小目标问题。信号灯在1080p画面里通常只有几十个像素高,黄灯、箭头灯更小;停车线远端驶来的车辆也就二三十像素,车牌、车灯更是典型的极小目标。
- 密集遮挡。早晚高峰多车道排满车,车辆之间相互遮挡严重,尾部特征大量丢失,检测框很容易漂移。
- 光照与天气干扰。逆光、夜间眩光、雨雾天气,信号灯颜色会明显失真,红色和橙色、红色和刹车尾灯之间很容易混淆。
- 类别不平衡。红灯状态在一天里只是部分时段,红色样本远少于绿色样本;大货车、公交车样本数量也比小轿车少得多。
我建议在做任何训练之前,先把你遇到的难点写清楚,形成一张对照表。这样后面每次效果不理想,都能回到表里检查,而不是在超参数上盲目试。
| 场景难点 | 对应策略 |
|---|---|
| 小目标(信号灯、远端车辆) | 提高输入分辨率,切片推理,浅层特征融合 |
| 密集遮挡 | 补充真实遮挡样本,使用copy-paste增强 |
| 光照变化、夜间眩光 | 色彩抖动增强,夜间与白天分开评测 |
| 类别不平衡 | 类别加权损失,对少样本类别重采样 |
1.3 类别体系设计:车辆与信号灯放在一起检,还是分开检
类别区分是标题里的核心之一,而第一个决策就是:车辆和信号灯用一个模型,还是两个模型?
我最终选择了一个模型同时输出车辆大类(轿车、卡车、公交等)和信号灯状态(红灯圆盘、黄灯、绿灯箭头等)。原因有两个:路口设备算力有限,串联两个模型会增加延迟;更重要的是,车辆和信号灯共享同一个特征提取网络,信号灯虽然小,但和车辆在同一条路上出现,合并训练时特征提取器能互相受益。
不过类别真的不能贪多。类别越细,小目标分类难度越高,标注成本也成倍上涨。我当时保留了8个类别:轿车、卡车、公交车、摩托车、行人、红灯圆盘、黄灯、绿灯箭头。具体类别要根据路口信号灯的实际相位数设计,宁可少而准,不要多而杂。
2. 车辆闯红灯目标检测数据集的构建:从抽帧到格式转换
2.1 数据来源与采集节奏
我这个项目的数据主要来自路侧监控视频抽帧。抽帧节奏要覆盖两条线:时间维度和空间维度。时间维度要覆盖白天、夜晚、早高峰、平峰、雨天;空间维度要覆盖路口不同方向的相机视角,因为不同机位下目标尺度差异很大。
初始我拿了约一天的连续监控视频,按每5帧抽1帧,得到约4万张图片。4万张全部标注是不现实的,我先做了一轮自动筛选:剔除模糊帧、重复帧、只有排队车尾无信号灯信息的帧,优先保留信号灯可见、车辆离停车线有一定距离的帧,最后压缩到1.2万张作为第一批训练集。这一步能省下很大一部分标注成本。
2.2 标注口径:小目标一定要标,遮挡只标可见部分
标注工具可选的范围很多,关键不是哪款工具最好用,而是团队里对标注口径有统一约定。我总结了两个非常重要、但容易忽略的细节:
- 信号灯哪怕在画面里只有一个小亮点,也要框出来。小目标样本是训练集里的稀缺资源,漏标一个信号灯,模型就少学一个小目标特征。宁可框比目标略小一圈,也不能整帧漏标。
- 被遮挡车辆只标可见部分,不要凭经验补全整个车身。YOLO回归的是可见区域的边界框,如果强行标成完整车框,模型会学到一个错误的形状分布。
标注完成后,我还会专门抽检信号灯小目标的标注框,确认没有把红灯和刹车尾灯标混。这种细粒度抽检是控制类别区分质量最有效的手段。
2.3 标签格式转换与坐标校验
我的标注工作流产出的是VOC风格的XML文件,训练前需要统一转成YOLO格式的txt,每个文件一行对应一个目标,格式为:类别id、归一化中心x、归一化中心y、归一化宽、归一化高。
这个转换踩过最经典的坑是坐标分母不一致:有人用整张图宽高归一化,有人用局部裁剪宽高归一化,最后训练时损失直接变成nan,排查了很久才发现是标签坐标越界。所以我在转换脚本里加了一组硬校验:坐标范围必须落在[0,1],宽高必须大于0,类别id必须在配置的类别表范围内,任何异常直接打印文件名并中断。
python convert_voc_to_yolo.py --xml_dir ./annotations --out_dir ./labels --img_dir ./images转换完后,我还会统计一次小目标占比:按原图分辨率把像素宽高乘积换算成目标面积,低于32×32像素的目标数量必须占总目标数的30%以上。如果不够,说明训练集会偏向大目标,模型天然对小目标不敏感,这时候再去调模型结构意义不大,要先回去补数据。
2.4 数据集划分:按视频片段切,避免数据穿越
划分训练集、验证集、测试集时,有个常见错误是按图片随机划分。同一辆车连续帧之间高度相似,随机划分会造成数据穿越,验证指标虚高,上线后立刻打回原形。
我按视频时间窗口切段,同一段视频的画面只进一个集合,这样验证结果才可信。验证集里我还特意多放夜间和雨雾样本,因为这些才是模型上线后最容易翻车的区域。训练集、验证集、测试集的比例我控制在约7:2:1,其中验证集大约2000张,测试集1000张。
3. YOLOv8训练的关键配置:参数、类别与损失
3.1 模型选型与预训练权重
YOLOv8有n、s、m、l、x几个型号。我实测后确定把m作为基线,因为小目标检测对特征分辨率非常敏感,n和s参数量太少,在极小目标上的精度会明显偏低;l和x精度确实更高,但训练时间拉得太长,部署和推理成本也高。
我直接使用官方在COCO上的预训练权重做迁移学习,而不是从头训练。COCO预训练已经让模型学会了通用的边缘、纹理、形状特征,微调阶段只需要学习交通场景的语义偏移,收敛速度快很多,也能规避训练初期边界框回归不稳定的问题。这一点在数据集规模有限时尤其重要。
3.2 关键超参数和训练命令
我把一组实测稳定收敛的超参数记录如下:
- 输入尺寸:1280×1280。这是小目标检测最直接的提升手段,640输入时约40像素的信号灯几乎无法稳定召回,换成1280后召回率提升非常明显。代价是显存和推理耗时翻倍。
- 训练轮数:300轮,配合早停策略,验证集mAP@0.5:0.95连续30轮不升就停止。
- 批大小:显存允许范围内尽量大,我用了8。
- 优化器:AdamW,初始学习率0.001,配合余弦退火。实测比SGD收敛更平滑。
- 马赛克增强:前面开启,最后10轮关闭。因为纯马赛克拼接会对小目标定位产生干扰,关闭后可以稳定修正边界框回归。
训练命令非常简单,关键是把数据配置yaml写好,里面包含训练集、验证集路径和类别列表。
yolo detect train data=traffic.yaml model=yolov8m.pt imgsz=1280 epochs=300 batch=8 optimizer=AdamW lr0=0.001 cos=True3.3 训练监控:不只看mAP,更要分桶看小目标AP
训练时只盯整体mAP是远远不够的。小目标检测的瓶颈通常藏在分尺度的AP指标里,所以我每5轮记录一次各类别AP,重点观察红灯圆盘、绿灯箭头这类信号灯类别的AP曲线。如果车辆类别的AP到0.9以上,信号灯AP却只有0.6,问题大概率出在信号灯小目标数据不够或者标注漏检上,应该去补数据和增强策略,而不是盲目换更大的模型。
3.4 类别混淆:刹车尾灯与红灯,卡车与公交
类别区分最让人头疼的几对混淆组合是:夜间刹车尾灯和红灯圆盘、卡车和公交车、公交车和小巴。这些问题的根因往往在数据和标注层面。
我的处理方式是三管齐下:第一,统一标注口径,比如卡车必须包含货箱部分的完整车体,公交车只标载客车体,规范写成文档定期抽检;第二,增加混淆样本,如果模型总把红灯圆盘和刹车尾灯弄混,就专门多放一些两者同框的样本,让模型学会用上下文区分;第三,在后处理里增加类别约束,比如红灯状态下车尾灯位置的检测框,置信度计入判定时要降权,交给逻辑层去甄别,而不是让模型硬扛。
4. 小目标检测的专项优化:从切片、特征融合到注意力机制
4.1 提高输入分辨率:朴素但有效
小目标检测第一板斧就是提高输入分辨率。我在1080p监控画面上对比过:640输入时,40像素左右的信号灯几乎无法稳定召回;1280输入时,同一批样本召回率能提高15到20个百分点。这个提升非常直接,相当于把所有目标在特征图上的响应面积放大了。
如果算力有限,可以采用两阶段裁剪推理:先用640输入快速扫描全图,发现可疑信号灯区域后,再用1280输入对局部区域做二次检测。这样兼顾速度和精度,但系统复杂度会上升。
4.2 切片推理:把大图切小,让目标相对变大
切片辅助推理是目前解决小目标最实用的一招。原理很简单:把一张大图切成若干小块,每块分别过模型检测,再把结果合并回原图坐标。由于每块小图里的目标相对尺寸变大了,小目标自然就更容易被检出。
我实际使用下来,在1280输入模型上叠加切片推理,信号灯AP又上涨了5到8个点。切片时有两个细节要注意:相邻切片之间必须保留重叠区域,我这边设置16%的重叠率,否则目标恰好被切在边缘时会漏检;合并结果时要做一次基于坐标的NMS,把重叠区域的重复检测框合并掉。
4.3 浅层特征融合与小目标检测头
YOLOv8的默认特征金字塔已经做了多尺度融合,浅层分辨率高、局部纹理丰富,适合小目标;深层语义强,适合大目标。但默认结构面对超小目标还是不够,我做了两个增量修改:
- 在默认三个检测头的基础上,为更浅一层特征额外增加了一个小目标检测头,专门负责约32像素以下的目标回归。
- 在这个检测头前叠加轻量级注意力模块,对通道和空间分别做加权,让网络自动把注意力放在信号灯、车牌这类小目标区域。
实现上不用把整个结构推倒重来,基于官方yaml做增量修改就够了。这个改动训练成本不大,但收益非常稳定。
4.4 数据增强组合拳
小目标训练集的扩充不能只靠平移翻转。我用的增强组合包括:
- copy-paste:把远处车辆或小尺寸信号灯从一帧复制到另一帧的交通场景里,直接增加小目标有效样本量;
- 随机尺度缩放:按0.3到1.0范围随机缩放目标尺寸,避免模型对某个体量区间过拟合;
- Mosaic四图拼接:拉大大目标与小目标的尺度方差覆盖;
- 随机色彩抖动加高斯模糊:模拟逆光、雾天和夜间眩光。
其中copy-paste和随机尺度缩放收益最大,建议优先保证。马赛克增强在训练后期要关闭,这点前面已经提过,非常关键。
4.5 分桶评测:用召回率定位优化方向
优化完以后,我在验证集上按目标尺寸把召回率分成四个桶:0到16像素、16到32像素、32到64像素、64像素以上。分桶看召回率能直接告诉你瓶颈在哪一层。
如果0到16像素的信号灯召回率很低,说明还需要更多小目标样本或更高输入分辨率;如果32到64像素区间很低,更可能是标注漏检或类别混淆问题。这种按尺度分桶的评测方式,比只看一个mAP数字有效得多,也是我后期调优最依赖的手段。
5. 类别区分与闯红灯判定:检测完之后的关键一步
5.1 空间与时序两层判定规则
即使YOLOv8把车辆和信号灯都检测出来了,要判定闯红灯还差两层逻辑:空间层,预先在图像中标定停车线的坐标位置,当车辆检测框底边中心越过停车线时,认为该车处于越线状态;时序层,用跟踪算法把连续帧中同一车辆串联起来,记录车辆第一次越线时间、当时信号灯状态、以及后续是否持续越线。
我采用的判定逻辑是:当信号灯处于红灯状态,且某一车辆ID从停车线一侧向另一侧跨越,并在连续3帧内保持越线,就触发一次闯红灯事件候选。连续帧确认能过滤掉大量单帧误检,比如检测框瞬时抖动、车辆压线未通过等情况。
5.2 车辆跟踪与ID稳定性
跟踪模块我采用的是检测加跟踪分离方案。夜间车辆尾灯、车牌在不同帧变化很大,纯外观特征不够稳,所以我结合了检测框IOU、运动预测和外观特征做联合匹配。跟踪ID的稳定性直接影响闯红灯记录的可靠性,ID切换一次就等于丢失一段完整证据链。
我调整了多种场景下的匹配阈值,兼顾遮挡恢复和ID保持。实测在多车密集路口场景下,车辆ID保持率能达到九成以上,足以支撑连续3帧的判定逻辑。
5.3 模型层区分物理目标,规则层区分行为
类别区分的最终经验是:模型层区分物理目标,规则层区分行为。
模型层只负责识别“这是什么”——车辆类别、信号灯颜色,不要试图让模型去学“是否闯红灯”这种时序语义,单帧根本学不会,还会污染类别标签。规则层负责结合停车线、车道、信号灯相位信息,生成最终判断。比如左转箭头红灯和直行圆盘绿灯同时出现时,不同车道的车辆必须匹配不同信号灯规则,这种逻辑必须放在规则层完成。
6. 实测效果、翻车现场与我的调试心得
6.1 实测指标与性能对照
我在一组未参与训练的真实路口视频上做了最终评测,整车目标的mAP@0.5在0.92左右,信号灯状态的mAP@0.5在0.87左右;小目标(32像素以下)的召回率从最初0.62提升到了0.79。单帧推理在m模型、1280输入下约45毫秒,路侧摄像头按25帧每秒计算,一台中端推理服务器可以同时处理多路视频流。这套性能用来做闯红灯事件触发与人工复核是够用的。
| 指标 | 初始基线 | 优化后 |
|---|---|---|
| 整车mAP@0.5 | 0.86 | 0.92 |
| 信号灯状态mAP@0.5 | 0.74 | 0.87 |
| 小目标(0~32像素)召回率 | 0.62 | 0.79 |
| 单帧推理耗时(1280输入) | 约40ms | 约45ms |
6.2 最典型的四个坑
坑一:验证集随机划分导致指标虚高,上线后直线下降。这是我第一版模型最大的问题,后来改成按视频片段划分才解决。
坑二:标注时漏标小目标。模型学到了忽略小目标,不管怎么调参都拉不回来,最后回去补标小目标样本才真正解决。小目标数据质量决定模型上限,这句话一点都不夸张。
坑三:马赛克增强在训练后期不关闭,小目标定位收敛不稳定。训练时观察到验证集存在周期性抖动,关闭马赛克后曲线立刻平稳。
坑四:刹车尾灯和红灯圆盘的混淆,一度让红灯状态误报率很高。后来靠补同框样本、调整类别权重和规则层降权,才把误报压下去。
6.3 后续可以继续迭代的方向
这套方案做完后,还有几个方向可以自然扩展:加入更完整的车辆跟踪与轨迹预测,让规则层用更长历史轨迹做闯红灯意图预判;增加夜间增强分支,专门针对低照度样本训练一组权重;融合多路相机的交叉验证,同一个违法行为两个视角都触发时,置信度更高;模型量化到INT8后部署到边缘盒子,进一步压缩推理成本。这些都是基于现有框架的增量修改,收益可观。
最后分享一个很多教程不会细说的经验:小目标检测调优时,先盯住验证集上分桶召回率的变化,再决定是加分辨率、加数据还是改结构。盲目堆模型参数、堆训练时长,往往是在用算力掩盖数据问题和标注问题。真正值得投入精力的,是把数据拦干净、把类别定义清楚、把判定逻辑和检测输出解耦,这三件事做扎实,YOLOv8的潜力才能被充分榨出来。