做智慧交通项目两年多,被问得最多的一个需求,居然不是车辆识别,而是“帮我在路口把没戴头盔的电动车骑手找出来”。这个需求听起来简单,但真正用通用目标检测模型去跑,问题一堆:要么把路边工人戴的安全帽当成头盔,要么傍晚逆光环境下直接漏检,最头疼的是戴了深色头盔的骑手在后排视角里和头发几乎分不清。后来我干脆基于实际路口场景整理了一套8300张的头盔检测数据集,目标格式直接对齐YOLO系列,重新训练之后效果才算真正稳定下来。这篇文章把数据集的构建逻辑、标注细节,以及用 YOLOv8 从训练到智慧交通场景落地的完整过程记录下来,给准备做非机动车监管、道路安全相关项目的朋友做个参考。
1. 头盔检测数据的真实需求:为什么通用模型总是“差一口气”
1.1 智慧交通场景里的头盔检测到底是什么任务
先把这个任务本身讲清楚。头盔检测在智慧交通里通常属于“非机动车骑行安全监管”中的一个子模块,摄像头安装位置一般是路口电警杆、人行道上方的治安监控,或者学校、园区门口的闸机相机。检测对象是电动自行车、摩托车骑行人员,要判断的是“头部区域是否佩戴了符合标准的安全头盔”。输出结果通常用于语音提醒、违法抓拍、统计报表,甚至联动闸机放行。
这和常规的人脸检测、行人检测有本质区别:目标小,头部区域在 1920×1080 或者 4K 画面里往往只有几十像素;角度固定但光照变化剧烈,早中晚、顺光逆光、树影、车灯反光都会造成干扰;遮挡严重,汽车从画面里穿过、雨伞遮住头部、后座乘客藏在骑手身后,都是常态。
1.2 为什么直接用公开预训练模型不靠谱
很多人第一反应是“去模型库下载一个检测安全帽的模型”。这里有个认知偏差需要纠正。公开的预训练模型,比如 COCO 上训练的 YOLO 权重,本身只覆盖了 person、bicycle、motorcycle 这些通用类别,根本没有“头盔”这个类别。你下载的那些“安全帽检测模型”,大概率是针对工地场景训练的,检测的是黄色、白色的 ABS 安全帽。
工地安全帽和骑行头盔是两个完全不同的视觉目标。工地安全帽通常是半圆壳,颜色鲜艳,光源以自然光为主;骑行头盔形状更流线,颜色五花八门,冬天还有全包式带面罩的款式。更关键的是拍摄视角不同,工地摄像头从高处俯拍,骑行车牌摄像头是从侧后方、侧前方平视。模型在工地数据上学的特征,到了路口场景基本属于“半失效”状态。
1.3 8300张数据集要解决的核心矛盾
这套数据集设计的初衷,就是解决上面说的视角差异、类别差异、光照差异三个核心矛盾。8300 张不是拍脑袋定的数量,而是基于“场景覆盖优先,实例丰富度其次”的原则倒推出来的。按每个摄像头常见机位算,需要覆盖至少 4 个典型视角(正向、侧向、后向、斜上方),每个视角下又要包含晴天、阴天、夜间、逆光、雨天 5 类光照条件,再乘以不同车型和头盔样式的组合,最少也需要 7000 张以上才能保证基本覆盖。8300 张里留出了 15% 左右的冗余,用来补特殊情况,比如冬季穿连帽外套、外卖箱遮挡等。对智慧交通场景来说,一张覆盖完整光照条件的真实样本,价值远大于十张高度相似的连续帧。
提示:数据集规模不是越大越好。同样的图片内容,如果只是连续视频抽帧,信息冗余非常严重。构建数据集的优先级应该是“场景多样性 > 实例数量 > 总张数”。
2. 8300张图的来源、采集策略与标注规范
2.1 视频抽帧与现场拍摄的取舍
数据来源我采用的是“视频抽帧为主,补充拍摄为辅”的方式。视频抽帧的好处是场景连续、姿态自然。路口摄像头录制的视频里,骑手从远处驶来、靠近、通过镜头、远离,整个过程会产生尺度从极小到正常再变小的连续变化,这对训练小目标检测非常有利。
抽帧频率需要控制在一个比较微妙的区间。每秒抽 1 帧,10 分钟视频能得到 600 张,但同一个骑手在连续帧里的形态高度相似,容易让模型过拟合到特定姿态。我的做法是抽帧后再做一次“相似度去重”,计算相邻帧的图像哈希相似度,超过阈值就丢弃后一帧,这样 600 张最后大概能保留 300 到 400 张有效样本。补拍主要针对夜间和雨雾天气,因为常规监控视频里这些时段的可用帧太少,夜间补了 800 张左右,雨天补了 400 张左右。
2.2 类别设计:二分类还是多分类
类别定义是标注前第一个要决策的问题,我最终采用了两个类别的方案:helmet(佩戴头盔)和no_helmet(未佩戴头盔)。
为什么不拆成“半盔”“全盔”“工地帽”“安全帽”这样的细分类?原因有两个。第一,从需求端看,监管系统最终只需要一个二值判断“戴了没戴”,细分类别会增加模型输出头复杂度,也在标注阶段引入大量边界争议。第二,从训练角度看,二分类的类间差异大,模型收敛更快,准确率更容易做高。但这里有个隐含问题:如果你希望模型能区分“正确佩戴”和“只是顶在头上没系扣”两种状态,那就必须在标注时把“头盔位置是否覆盖整个头顶”作为判定边界,这个后面会细说。
实际标注时,如果一个骑手戴了头盔,但头盔明显翘起、后脑勺外露,我会归为no_helmet。因为从执法和提醒角度,这种佩戴方式是无效的,模型学到“戴歪了不算戴”这个规则,比单纯学“头盔外观”更有价值。
2.3 标注边界与质量清洗的真实细节
标注工具我用的是 LabelImg 和 X-AnyLabeling 混合使用,前者负责早期大批量标注,后者用在后续的难例补充上。框的标注规范非常关键,我定的规则是:
- 检测框只包含头部区域,不包含脖子以下的身体部分
- 头盔和头发重叠时,以头盔外轮廓为框边界
- 后座乘客的头部被骑手遮挡超过 50%,不标注
- 被汽车完全遮挡、只剩局部头部的目标,不标注
- 模糊到人眼都难以判断是否戴头盔的目标,标记为
ignore并不参与训练
清洗环节最花时间。第一轮自动清洗用 YOLOv8 预训练模型跑一遍推理,把置信度高于 0.9 的预测框和人工标注框做 IoU 比对,差异大于 20% 的样本单独抽出来人工复查。第二轮人工逐张检查所有包含夜间、逆光、雨天的图片。整套流程下来,8300 张图共标注了大约 13700 个实例,平均每张图 1.65 个头部目标,其中有遮挡的目标占到 12% 左右。
3. 数据集最终构成与YOLO格式的组织方式
3.1 数据集的整体分布与划分比例
8300 张图中,包含helmet实例的图片约 4900 张,包含no_helmet实例的图片约 5400 张,两者有交叉是因为有的图片里骑手戴了、后座乘客没戴。从实例数量看,helmet约 7200 个,no_helmet约 6500 个,类别基本均衡,没有做刻意欠采样。
场景分布上,我按视频来源分为三类:城市主干道十字路口、非机动车专用道、园区/学校门口。比例大致是 1000 张夜间、800 张雨天/阴天、550 张逆光,其余为正常日照。训练集、验证集、测试集按 78:12:10 划分,也就是训练集 6474 张,验证集 996 张,测试集 830 张。划分时特别做了场景隔离,来自同一条视频片段的不同帧只能进入同一个集合,避免验证集和训练集出现“近亲样本”,否则验证指标会虚高,实战时容易打回原形。
3.2 YOLO 标注格式的核心细节与转换脚本
市面上公开的头盔检测数据集中,有一部分是 COCO 格式,有一部分是 VOC 格式,这套数据集的基础格式则是YOLO 的 txt 标注文件。每张图片对应一个同名的.txt文件,每行表示一个目标,格式为:
class_id x_center y_center width height注意,这里的x_center y_center width height全部是相对于图片宽高的归一化比例,取值在 0 到 1 之间。例如一张 1920×1080 的图片,某个目标的左上角像素坐标是 (480, 270),右下角是 (960, 810),那对应的归一化值就是:
x_center = ((480 + 960) / 2) / 1920 = 0.375 y_center = ((270 + 810) / 2) / 1080 = 0.5 width = (960 - 480) / 1920 = 0.25 height = (810 - 270) / 1080 = 0.5如果你手里的标注是 VOC 格式的 XML 文件,转换时最需要注意的是类别 ID 的映射关系。yaml 配置文件里 classes 的顺序决定了 ID,顺序定下来之后所有文件必须同步,否则训练时类别就全乱了。我在这套数据集发布时已经把所有 XML 转换成了 txt,并附带了一个 Python 转换脚本,方便拿到 VOC 格式数据的同学自行处理。
3.3 目录结构与 yaml 配置的最终形态
目录结构我采用了 YOLO 工具箱最通用的方式:
helmet_dataset/ ├── train/ │ ├── images/ │ └── labels/ ├── val/ │ ├── images/ │ └── labels/ ├── test/ │ ├── images/ │ └── labels/ └── helmet.yaml其中helmet.yaml的内容非常简洁,却决定了训练的一切:
path: /absolute/path/to/helmet_dataset train: train/images val: val/images test: test/images nc: 2 names: 0: helmet 1: no_helmet每次新项目拿到这套数据,我做的第一件事永远是检查图片文件名和标注文件名的前缀是否完全一致,以及 yaml 中nc的数量是否和names列表长度一致。这两个地方出错不会报任何 warning,模型照样能训练,但精度会莫名其妙地低几个点。
4. 基于YOLOv8的训练实操:参数、损失曲线与常见坑
4.1 环境准备与预训练模型的选择策略
训练工具我选了 YOLOv8,主要原因是生态成熟、文档齐全,导出 ONNX 和 TensorRT 都很顺畅。Python 环境建议用 conda 新建一个独立环境,Python 3.10 即可,然后用pip install ultralytics安装。
预训练模型的选择是个值得展开的点。YOLOv8 官方提供从n到x五个尺寸的权重。智慧交通项目最后部署的目标设备一般是 NVIDIA 的边缘盒子或者 GPU 服务器,我推荐从yolov8m.pt起步。yolov8n虽然快,但对头盔这种小目标来说特征表达能力不足;yolov8x精度高,可推理速度在边缘设备上跑不满实时要求。yolov8m是性价比折中点。下载预训练权重时留意一下来源,尽量从官方 GitHub Release 页面获取,避免从不明渠道下载到被篡改的权重文件。
4.2 数据配置与训练参数的调试记录
数据配置准备好之后,训练命令可以写成这样:
yolo detect train \ --model yolov8m.pt \ --data helmet.yaml \ --epochs 100 \ --imgsz 640 \ --batch 16 \ --device 0 \ --workers 8 \ --patience 20 \ --cache ram几个参数的选择逻辑需要特别说明。imgsz 640是默认值,头盔检测目标在原始画面里偏小,我试过 800 和 960,mAP 提升在 1 个点以内,但训练显存占用大幅上升,推理速度也变慢,最终维持在 640。batch 16是 24G 显存显卡的稳妥选择,如果你的显卡只有 12G 显存,建议降到 8,如果在训练日志里看到类似 “loss is NaN” 的现象,先查学习率和 batch size 是否不匹配。patience 20表示在验证集 mAP 连续 20 轮不提升时自动停止训练,能省不少时间。
损失曲线是训练过程中最值得盯的指标。YOLOv8 的损失由三部分组成:边界框回归损失(box_loss)、分类损失(cls_loss)、特征分布损失(dfl_loss)。正常训练时,三类损失在前 20 轮快速下降,之后进入平台期缓慢收敛。你需要重点关注的是 box_loss,如果它下降非常快但 cls_loss 居高不下,说明模型“找到了物体但分不清类别”,这种时候要优先检查类别定义边界是否清晰,尤其是“戴歪的帽子到底算哪类”这种标注分歧。
4.3 混淆矩阵与 mAP 指标的解读方式
训练结束后,YOLOv8 会输出混淆矩阵和 PR 曲线。我习惯先看验证集上的混淆矩阵,它比 mAP 数字更诚实。一个健康模型的状态是:主对角线上的数值在 0.9 以上,非对角线的交叉值控制在 0.05 以下。头盔检测项目最容易出现的错误是把helmet判成no_helmet,也就是漏检,这通常和夜间样本不足有关,需要回补数据而不是盲目调参。
mAP 方面,mAP50达到 0.92 以上、mAP50-95达到 0.68 以上,是这套数据集在 YOLOv8m 上的基本水平。如果你的 mAP50-95 明显偏低,大概率不是模型问题,而是标注框不够紧。标注边界稍微外扩一圈,会让 IoU 计算时损失大量分数,这时候回到标注清洗阶段,把框重标一遍比换更大的模型有效得多。
4.4 训练过程中的典型“炸弹”:BN 崩溃与类别失衡
训练中途模型突然崩掉,最经典的元凶是 BatchNorm(BN)崩溃。YOLOv8 的 C2f 模块和检测头里大量使用 BN 层,当 batch size 设为 1 或 2 时,每个 batch 的统计量波动极大,BN 的 moving mean 被带偏,训练损失会突然飙升到十几个点,然后模型就废了。
遇到这种情况,我的处理顺序是:先把 batch 提到 8 以上;如果显存实在不够,就在配置里把--batch减小后同时调整--optimizer的学习率;之后再检查数据加载是否按随机顺序打乱。还有一个容易被忽略的原因:训练集里类别严重失衡。如果 8300 张图中某个类别的实例数量占比超过 90%,模型会倾向于把一切目标都预测成那个类别。解决方式是在数据层面做均值控制,或者调高少数类损失权重,但最简单的还是先保证标注实例数不要出现数量级差异。这套数据集在设计时已经把正负实例控制在接近 1:1,所以不涉及这个问题。
提示:不要一上来就套用网上流传的各种“改进 YOLO”结构。先用原版 YOLOv8m 跑通基线,记录 mAP 和推理速度,再决定要不要引入注意力机制或 Transformer 模块。没有基线的“改进”,最后说不清是改好了还是改坏了。
5. 从模型到智慧交通场景:部署、误报处理与数据闭环
5.1 推理端到端的工程化细节
模型训练完不是终点,要真正接入智慧交通系统,必须经过“PyTorch → ONNX → TensorRT”的导出链路。YOLOv8 的导出命令很简单:
yolo export model=runs/detect/train/weights/best.pt format=onnx dynamic=True但这里有个容易被忽略的点。导出 ONNX 时,如果用的是dynamic=True,推理框架需要处理动态输入尺寸,边缘设备上可能反而更慢。对固定路口画面来说,我通常直接固定输入尺寸为 640×640,用dynamic=False导出,换取更好的硬件优化空间。导出后再转成 TensorRT 的 engine 文件,显存占用和延迟都能显著降低。
部署后的推理流程不是简单地把每一帧都丢给模型。实际画面里大量时间是空路口的静态帧,全帧检测既浪费算力,又会造成大量重复预警。我的做法是先做轻量级背景差分或 ROI 区域设定,只在检测区域内出现运动目标时才触发模型推理。这个环节能把单路视频的 GPU 占用率降 40% 以上,对边缘盒子来说等于白送了 40% 的并发路数。
5.2 误报与漏报的典型案例及修正手段
部署之后的场景远比训练集复杂。我遇到过典型的误报:雨天路面积水反射的倒影被模型当成未戴头盔的路人;夜间路灯下被拉长的人影;外卖箱上印着的人脸图案引发检测;还有监控杆被树木遮挡时,树叶晃动造成的瞬移目标。
处理这些问题的思路不是堆模型,而是分层治理。第一层是过滤帧,检测框 IoU 在连续 3 帧内不稳定且面积抖动超过 50% 的目标,直接丢弃;第二层是目标跟踪,用 ByteTrack 给每个检测目标分配 ID,只有 ID 跟踪连续 5 帧以上才推送结果;第三层是场景掩码,把固定画面中的电线杆、树木、路灯等非目标区域手动标成 mask,推理时跳过。这套逻辑做完,误报率大概能再降一半。
5.3 数据闭环:把每一张误报样本都攒成改善数据的机会
智慧交通项目最值钱的不是初始数据集,而是持续运转的数据闭环。模型上线后,我维护了一个“hard examples”目录,规则是所有被过滤掉的高置信度误报目标、连续跟踪后仍判断错误的难例、以及用户反馈中标记为“错误识别”的图片,每周自动归档一次。每攒到 500 张,就用半自动标注工具快速打标,然后和原训练集合并重新训练一轮。
这个体系运行三个月后,模型在新增场景上的表现会有明显提升,尤其是那种“光线奇怪、角度刁钻”的情况下。纯靠初始的 8300 张图,模型能覆盖 80% 的路口场景,剩下 20% 的边角场景没有别的办法,只能靠时间换数据。注意新增数据和旧数据混合训练时,建议旧数据保持 70% 以上比例,否则模型会对新场景“过度记忆”,忘了老场景的特征。
整个头盔检测项目做下来,我的体感是三成时间在调模型,七成时间在搞数据和场景适配。8300 张数据集提供了一个扎实的起点,但真正让系统在路口稳稳跑下去的,是前期规范的标注边界,中期严谨的训练评估,和后期不打折扣的数据闭环。如果你也准备做类似项目,先把“什么算戴了头盔、什么算没戴”这个标准定义清楚,再谈模型选型和参数优化,一定能少走很多弯路。