头盔检测在智慧交通项目里属于那种“看起来不难、做起来浑身是坑”的典型目标检测任务。我从去年开始整理一套专门服务于头盔检测的8300张YOLO智慧交通数据集,最初只是为了完成一个电瓶车骑行头盔抓拍项目,后来发现这套数据不仅能覆盖工地安全帽检测,还能用在摩托车卡口、校园门口接送场景等许多交通管理方向。这篇博文想完整记录这套数据集从采集、标注、训练到部署的全过程,包括我在过程中踩过的坑、反复调整过的参数、以及最终在边缘设备上稳定运行的方案,希望给正在做类似项目的朋友省掉几个礼拜的试错时间。
适合来看这篇文章的朋友主要有三类:一是想做头盔检测但不知道从哪里获取合适数据集的研究者,二是需要把模型部署到道路监控设备上的方案工程师,三是自己尝试用YOLO训练自定义数据集的开发者。下面我会把细节挖得比较深,不是简单给个下载地址就完事。
1. 头盔检测为什么必须自己建数据集
1.1 通用目标检测数据集根本不够用
我在项目初期把COCO、VOC这些公共数据集都试过一遍,结论很直接:能跑通,但不能落地。COCO里根本没有专门的头盔类别,很多摩托车骑车人头盔是被整体标注在person上的,有些行人数据集里连“骑电动车戴头盔”这一场景都找不到。VOC数据集的类别体系更旧,安全帽或者头盔完全不在分类列表里。即便你强行用通用数据集训练,模型只会学到“有人的地方就是人”,完全分不清脑袋上多了一个硬壳意味着什么。
有人可能会说,那我可以拿公共数据集做预训练,再用自建数据微调。这个思路没错,我在实际项目里也是这么做的,但这恰恰说明一套专用数据集是绕不开的。预训练模型的价值在于给骨干网络提供先验特征,真正决定头盔检测精度的还是你在微调阶段喂进去的那几千张标注图。如果这套图类别不清晰、场景覆盖率不够、正负样本失衡,后续一切调参都是白费。
1.2 智慧交通场景的难点和普通目标检测不一样
智慧交通里的头盔检测,和普通目标检测最明显的区别在于视角。城市道路监控不是平视,而是从几米到十几米的杆件上往下俯拍。在这个角度下,头盔在图像里的投影面积往往很小,顶部或侧面的纹理又严重受光照影响。我最初用旧版本YOLOX跑测试视频,晴天下午一段普通路面,mAP看着有0.85,但实际跑起来大面积漏检——原因是模型倾向于识别“轮廓清晰的人脸加头顶”,一旦逆光或者头盔和黑色车身融为一体,预测框就开始乱跳。
另一个不同点是目标尺寸分布极度不均匀。一个1080P画面里,近景骑车人的头盔可能占40×40像素,远景路口另一个骑车人的头盔只有12×15像素。面对这种多尺度问题,通用数据集的类别集中在中大目标,自然训练不出好效果。这也是我决定建专用数据集的核心动机:必须把远景小目标、遮挡、逆光、夜间这些极端情况按比例放进去,让模型在训练阶段就见过足够多的“难例”。
1.3 这个数据集的定位与使用边界
8300张图不是一个很大的数字,但它被设计成“高信息密度”的数据集,而不是“堆数量”的数据集。它的定位是:用于YOLO系列模型在道路监控场景下的头盔检测微调,类别集中在佩戴头盔、未佩戴头盔、骑行人(或人员)以及车辆相关目标上,而不是像通用数据集那样包罗万象。使用边界也很清晰:如果你要做矿区、建筑工地的安全帽检测,这套数据依然有效,因为“摩托车头盔”和“工地安全帽”在视觉特征上有大量共享区域,只需要补充少量工地实拍图即可。
2. 8300张数据集的构成与标注策略
2.1 图像来源与场景覆盖
这套数据的图像来源包括:道路卡口监控截图、执法记录仪视频抽帧、公共区域摄像头测试段、以及部分由协作单位提供的真实场景画面(已经脱敏)。场景上我特意做了分层覆盖:
- 白天正常光照约4600张,这是最容易获取也最容易过拟合的部分;
- 夜间路灯照明约2100张;
- 清晨/黄昏弱光约950张;
- 雨天和反光路面约650张,这部分数量虽然少,但对模型在恶劣天气下的鲁棒性提升非常明显。
| 场景 | 数量 | 占比 | 典型特点 |
|---|---|---|---|
| 白天 | 4600 | 55.4% | 逆光、侧光、强阴影 |
| 夜间 | 2100 | 25.3% | 低照度、车灯过曝 |
| 弱光(清晨/黄昏) | 950 | 11.4% | 色温偏移、对比度低 |
| 雨天/反光路面 | 650 | 7.8% | 镜面反光、雨滴模糊 |
从摄像头的角度看,画面里涵盖了杆件高位视角、桥架侧视角、以及红绿灯路口对角线视角三种主流监控布点。这样做的原因很简单:不同视角下头盔的形态变化极大,高位俯拍看到的是头盔顶面,侧视角看到的是头盔侧面和下颌带,如果只用一个视角训练,部署到另一个视角就会性能崩盘。
2.2 类别定义与标注细节
类别命名直接决定后面训练时的语义边界,这一步不能省。我把类别定为五类:head、helmet、no_helmet、person、vehicle。其中head是指没有任何遮挡、清晰可见的人头,helmet是指正确佩戴的头盔,no_helmet是指手里拿着头盔或头盔挂在车上但没戴的情况,person是人体的整体包围框,vehicle是两轮车的包围框。这样定义不是为了凑数,而是为了在推理阶段能写逻辑:检测到person同时匹配到helmet就是合规,匹配到head或者no_helmet就能触发告警。
标注格式上同时导出了YOLO txt格式和PASCAL VOC的XML格式,YOLO格式用于直接训练,XML格式方便转成COCO json或给其他检测框架使用。框坐标统一按照图像原始分辨率计算,没有做压缩,因为一旦缩放标注框,小目标的位置偏差会被放大。对遮挡目标的标注,我遵守一个原则:如果目标可见面积小于全貌的30%,就不单独标注,避免模型学到半截身体的特征;如果可见面积大于30%,则按可见部分的最小外接矩形来标。这套规则听起来简单,但能显著减少类内噪声。
2.3 三道质量控制关卡
第一关是去重。视频抽帧会产生大量相邻帧高度相似的图像,直接训练会让模型对重复背景过拟合。我用感知哈希算了一遍,把相似度超过0.92的帧合并成场景片段,再从每个片段抽出一张代表性图像,最后人工抽查了约600张。
第二关是标签一致性复查。最容易出问题的是“手里拿着头盔但没戴”和“后座乘客被前座挡住一半”这两种情况。项目里出现过两次标注员口径不一致的事情,一次把拿着头盔的人标成了helmet,另一次把后座的人头标成了no_helmet。统一口径之后,我让训练集里的所有标注都过了一遍半自动校验脚本——用YOLOv8预训练模型先对每张图做预测,再将预测结果与人工标注比对,把不一致的目标挑出来逐一人工确认。这一步非常花时间,但效果很明显:标签噪声率从最初的约4%降到了0.6%以下。
第三关是格式校验。训练前检查每个txt文件里类别ID是否越界、框坐标是否归一化、是否有坐标为负或超出图像宽高的脏数据。这个看起来像是小事,但实际跑训练时,一个坐标越界的框能让损失函数直接变成NaN。
2.4 数据增强的边界控制
YOLO自带马赛克增强、翻转、缩放、色域变换等策略,合理的做法是用,但要有边界。实际上马赛克增强对小目标并不友好——马赛克把四张图拼在一起,每个目标的像素占比被压缩了,模型在训练时看到的“小目标”是人为缩小的,和真实监控画面的小目标信号并不一致。因此我在小目标为主的分支上把马赛克关闭,只开轻微平移、旋转和HSV扰动。对于夜间图像,我没有做太多亮度增强,因为亮度增强后的图像会改变真实夜间场景的噪声分布,效果反而不好。
3. YOLO版本选型与训练前的准备
3.1 选择YOLOv8的原因
我最终选了YOLOv8作为主力训练框架。对比过YOLOv5和YOLOX,YOLOv8在C2f模块里的梯度流动更顺畅,训练收敛更稳定,尤其是在类别不平衡的数据集上,它的分类损失设计更抗噪。YOLOX的Anchor-Free设计在理论上对小目标友好,但工程生态和部署工具链没有YOLOv8完善。
预训练模型我用的是YOLOv8m的COCO预训练权重。不要小看这一步,从头训练需要更大的数据量和更长的训练时间,在head、helmet这种特征高度相似的目标上,COCO预训练权重提供的通用视觉特征能让模型快速收敛。
3.2 数据集划分的内在逻辑
数据划分上有一个容易被忽视的问题:不能只按比例随机划分。如果你直接把8300张图按8:1:1随机分成训练、验证、测试三份,那么同一摄像头同一天连续抓拍的十几张图会被拆散到不同集合里,验证集里出现大量与训练集高度相似的画面,指标看起来虚高,实际部署根本达不到那个水平。我是按场景片段划分的,确保同一个片段里的所有帧统一进入训练集、验证集或测试集。最终训练集约6400张、验证集约1100张、测试集约800张。
3.3 训练参数与硬件配置
硬件是一张V100 16GB。输入分辨率选择640×640,batch size设成16,epochs设成300但配合早停。学习率初始0.01,使用余弦退火调度。对于YOLOv8,优化器选择SGD而不是AdamW,因为SGD在目标检测任务上往往能更好地收敛。
| 参数 | 数值 | 备注 |
|---|---|---|
| img_size | 640 | 兼顾速度与精度 |
| batch | 16 | V100 16GB上限 |
| epochs | 300 | 配合早停 |
| lr0 | 0.01 | 余弦退火 |
| optimizer | SGD | 而不是AdamW |
| 类别数 | 5 | head / helmet / no_helmet / person / vehicle |
4. 训练过程中的观察与调优
4.1 BN层崩溃问题
训练到一半loss突然变大的“BN崩溃”现象,本质是BN统计量在某个batch上被极端激活值污染,梯度爆炸,后续所有batch的均值和方差都被带偏。我遇到过两次,解决方案是把batch size调大一点,同时把输入图像的分辨率稍微降低到416×416试了几轮,后来确认主要诱因是某几张标注错误图像带来的极端梯度。定位到脏数据之后,后续训练没有再出现。
4.2 混淆矩阵怎么看
YOLOv8训练结束后会输出混淆矩阵,有些人反映混淆矩阵的总合不唯一,这是正常的。混淆矩阵每一行代表真实类别,每一列代表模型预测类别,行列合计的含义不同:行合计是真实样本数,列合计是模型预测次数,二者当然不相等。“总合不唯一”是因为你把行维度和列维度的统计混在一起算了。我在项目里主要关注的是no_helmet这一行有多少预测被分到了helmet列,这个数字直接反映了最容易发生的误判方向。
4.3 小目标专项优化
在测试集上按目标尺寸分组的AP差异非常明显。结果大致如下:
| 目标尺寸 | 像素面积 | mAP@0.5 |
|---|---|---|
| 小目标 | < 32×32 | 0.62 |
| 中目标 | 32×32 ~ 96×96 | 0.87 |
| 大目标 | > 96×96 | 0.93 |
为了提升小目标AP,我做了一个特别有效的事:把输入分辨率从640提高到768,同时在推理阶段开启TTA(测试时数据增强)。分辨率提高直接增加了小目标的像素占比,代价是V100上的推理FPS从95降到61,FPS降低了但收益明显。如果你对实时性要求高,还可以试试把输入切成四块分别推理再合并结果,不过那个逻辑在边缘设备上不太现实。
4.4 损失函数与难例挖掘
第三版训练开始,我盯上了难例挖掘。忽略置信度最高的那些样本,把loss排在前面的图像挑出来重新看。这些图像有一个共同特点:要么是头盔颜色与背景色相全的反差不明显,要么是后座乘客被前座挡得只剩三分之一。我针对这批难例又补充了250张专门的数据,并把它们的过采样倍率提高到2.5倍。这一轮操作之后,测试集小目标AP从0.62涨到0.71,效果比单纯调参强得多。
5. 部署到边缘设备与道路监控的实战
5.1 导出与转换链路
训练完成之后,真正麻烦的环节才开始。我尝试了两种部署路线。第一种是NVIDIA设备:先把PYTORCH权重导出为ONNX,再用TensorRT把ONNX转换为engine。重点优化是开启FP16精度,这能让推理速度提升将近一倍,而精度损失控制在0.02以内。第二种是瑞芯微RK3588开发板,这就要用瑞芯微自带的RKNN工具链,先把ONNX转换到rknn格式,期间需要在PC上做量化校准。注意RKNN对某些算子的支持有限,比如YOLOv8里一些自定义激活,需要在转换时手动修改网络配置,把不支持的算子替换成等价的基本算子组合。
5.2 边缘设备性能实测
在树莓派上跑YOLOv8n的轻量版,纯CPU推理的FPS非常感人,320×320分辨率也要3到5秒一帧。后来我换了一种策略:用OpenCV的VideoCapture读取RTSP流,每隔3帧做一次检测,检测帧用640×640推理,中间帧跳过。这样实际处理一条1080P视频流时,树莓派上的CPU占用率保持在70%左右,每秒能处理约4帧。如果你要部署在RK3588上,情况会好得多,NPU加持下YOLOv8s模型能做到30FPS以上。
5.3 误检率高企的案例分析
部署阶段最头疼的是误检。道路监控画面里偶尔出现圆形交通标志牌,模型把它当成头盔;广告牌上的人脸剪影,也被当成no_helmet。最初的置信度阈值设成0.25太低,后续提高到0.35,误检率下降了一半。我还尝试在训练数据里增加60多张完全没有头盔的“纯背景”负样本,在训练时把它们全部放入一个batch,强制模型学习“没有目标就是没有目标”。这两个动作组合起来,误检率从每帧1.2次降到了每帧0.15次以下。
6. 使用这套数据集的个人经验与扩展建议
6.1 在你的场景中快速启动
拿到8300张数据后,最省事的做法是先跑一次标准YOLOv8训练管线,把基线指标跑出来。之后再去关注你本地场景的差异化数据,比如你所在城市有大量蓝色头盔、或者你的监控点位都是夜间补光的,那么你需要补充这些数据。补充时不要一股脑往训练集里塞,而是先按第2章说的口径做标注和质量控制,再加进来增量训练。
6.2 扩展方向:实例分割与多模态
头盔检测数据集能做的事不止是目标检测。把五类目标的包围框标注转换成多边形掩码之后,可以做YOLOv8-seg实例分割。实例分割的好处是能提取更精确的头盔轮廓,对后续的“头盔颜色统计”这类任务有帮助。如果你觉得纯视觉在夜间效果不够好,也可以考虑结合毫米波雷达产生的轨迹信息做多模态融合,只在雷达检测到车辆经过时才启动图像检测,这样既能省算力,也能减少无车辆时的误检。
6.3 长期维护比一次性训练更重要
数据集的长期维护是一个常被忽略但很重要的环节。我在项目里建了一个简易的数据版本管理目录,每次补充新数据都会记录日期、来源场景和类别分布变化。模型上线后,每个月抽一整天时间采集新监控画面,人工标注出模型预测错的地方,加入训练集重新微调一轮。这个循环跑下来,模型的鲁棒性会越来越稳定,而不是一开始调完就放任不管。
再分享一个小技巧:训练完成后一定要把模型在验证集上的错误预测框渲染成图片,按置信度排序,人工翻看一遍。这个操作看起来原始,但比任何指标都更能帮你发现模型真正的短板。我在连续翻了两次错误预测图之后,才发现夜间车灯过曝区域里的头盔漏检占了全部错误的30%,这个比例不靠人工看是根本意识不到的。