做智慧交通AI项目的人应该都有同感:真正卡脖子的不是算法,而是数据。就拿头盔检测来说,网上能找到的开源数据集,要么是国外场景,人种、车辆样式和国内差异不小;要么就一两千张,模型训练完一放到实际路口就露馅。正因为这样,我花了很长一段时间整理、清洗、标注,最终沉淀出8300张头盔检测数据集,格式直接按YOLO训练要求来,服务智慧交通场景下的安全帽/头盔佩戴检测。这篇博文不吹模型多牛,我把数据怎么组织、标注怎么定口径、YOLO怎么训、上线怎么部署的完整思路都写出来,给正在做类似项目的朋友一个可以直接抄作业的参考。
这套数据集目前主要用在非机动车骑行场景,比如电动车、摩托车驾乘人员的头盔佩戴识别,也能扩展到工地安全帽检测这类相似任务。适合谁看?做智慧交通算法研发的工程师、搞毕业设计的学生、还有想快速验证YOLO落地方案的产品经理,都能从这里找到自己关心的那部分。8300张的规模不算大,但配合合理的标注口径和训练策略,足够练出一个能上路的模型,这一点我在后面会详细说。
1. 头盔检测到底要解决什么问题,为什么数据集决定上限
1.1 智慧交通场景下的头盔检测痛点
头盔检测在目标检测任务里属于“看起来简单、做起来麻烦”的类型。说简单,是因为类别少,无非就是戴头盔、没戴头盔,甚至有些人会加一个“人头”背景类。但真实路口环境远比想象中复杂:目标小,骑手在整个画面里可能只有几十个像素;视角刁钻,摄像头通常装在立杆上,俯拍角度下头盔和人头会严重重叠;天气和光照变化大,白天逆光、晚上路灯昏暗、下雨天头盔反光;再加上车辆运动导致的模糊,行人、背包、车筐、雨棚这些干扰项,模型很容易误检。
我曾经在一个实际路口测试过网上某套几千张的通用人头数据集训练的模型,白天效果还能看,一到傍晚和夜间召回率直接掉到六成以下,误报更是一塌糊涂,把路牌的阴影、公交车窗里的人脸都当成没戴头盔的骑手。后来复盘发现,问题多半不在模型,而在数据集:训练样本里根本没有足够的暗光、远距离、遮挡场景。所以做这类项目,第一个教训就是:模型的精度上限,在开始整理数据那天就定死了。
智慧交通场景下,头盔检测通常还要跟抓拍联动,比如识别到“未佩戴头盔”事件后触发取证拍照、视频片段截取。这种应用对召回率的要求极其苛刻,漏一个等于整个系统失效。同时误报又不能太高,否则后端审核人员会被大量无效事件淹没。召回率和误报率都要兼顾,这比单纯比mAP高零点几更考验数据质量。
1.2 8300张数据集该如何规划类别与标注口径
拿到8300张原始图片,第一步别急着框框,先把类别和标注口径定清楚。我见过很多项目在这个环节犯错,比如把“头盔”和“人头”都归成一类,模型训练完根本没法判断戴没戴;或者只标“头盔”不标“未戴头盔的人头”,模型学到的是“画面里有没有头盔”,而不是“这个骑手有没有戴头盔”,完全偏离需求。
比较稳妥的做法是采用三分类:
helmet:骑手或乘车人头部佩戴头盔的实例,框住头部区域即可head:骑手或乘车人头部未佩戴头盔的实例,同样框住头部区域person:完整的骑行人,用于关联判断,辅助模型理解上下文
为什么要多做一个person类?因为后处理时可以通过“同一目标框内person和helmet/head的匹配关系”来输出最终事件,避免模型把远处路人、行人头盔、车筐里的头盔都算进去。如果项目只需要最简单的“戴/未戴”二分类,可以把head理解为“未戴头盔的人头”,但在训练集里显式标出来,而不是当成背景忽略,这会大大降低误检。
标注框的口径也要统一。我的习惯是:头盔框紧贴头盔外轮廓,人头框紧贴头皮,而不是连肩膀、连身体一起框。原因很简单,目标检测里框的IoU直接影响回归损失,框得越紧,模型学到的位置越准;另外,多目标重叠时,紧贴的框能减少相邻目标的互相干扰。
另外要决定的是要不要把乘车人单独标出来。电动车和后座乘客往往是交通事故中受伤更重的群体,很多智慧交通项目的要求是前排后排都要检测。这组8300张数据里,我特意包含了后排乘客样本,比例大约占15%,用于防止模型只认得驾驶员不认乘客。这里有个细节:后排乘客通常被驾驶员身体遮挡严重,标注时只要头部可见就应该标出来,不要因为遮挡就丢弃,否则训练出的模型对遮挡目标极其不敏感。
1.3 数据规模与场景覆盖的平衡
8300张听起来不少,但对于深度学习训练,尤其是要覆盖多场景、多时段的项目,其实不算充裕。更关键的不是总量,而是分布。我给这套数据定了一个场景覆盖比例,训练出来的模型泛化性明显好于我之前用“全是晴天白天”数据集的版本:
| 场景维度 | 覆盖比例 | 说明 |
|---|---|---|
| 白天/傍晚/夜间 | 50% / 30% / 20% | 夜间至少保证1000张以上,否则天黑就废 |
| 远距离/中距离/近距离 | 40% / 35% / 25% | 远距离小目标太少会导致漏检严重 |
| 单骑手/多人/遮挡 | 50% / 30% / 20% | 遮挡样本用于提升召回率 |
| 摩托车/电动车 | 30% / 70% | 两类车型外形差异大,不能偏科 |
| 晴天/阴天/雨雾 | 60% / 25% / 15% | 雨雾样本可以后期用图像增强补一部分 |
规划分布时不要平均主义,要按实际路口出现频率来定。比如夜间样本虽然只占20%,但夜间场景的误检率往往是白天的好几倍,所以我反而会额外收集夜间负样本(没有人、没有头盔的空路面)加入训练集,让模型知道“没有头盔才是常态”,而不是看见什么都想识别。
2. 从原始图片到YOLO训练集:8300张数据的构建流程
2.1 数据来源、清洗与合规处理
这套数据的来源主要有三块:一是公开的、明确允许科研和商业使用的数据集,二是与合作方在内部场地拍摄的模拟路口画面,三是用3D渲染引擎批量生成的合成场景。合成数据的作用不可小觑,尤其是夜间、雨雾、极端角度这类真实场景难采集的情况,渲染出来的图片和真实画面的分布差距可以通过后续“域适应”或者混合训练来弥补。
不管数据来自哪里,第一步永远是清洗。8300张图片如果直接从采集设备里倒出来,通常会有大量重复帧、模糊帧、纯背景帧。我一般用感知哈希对全图做去重,再按清晰度(拉普拉斯方差)筛掉模糊图,最后人工抽检一遍。这步很枯燥,但省掉它,后面所有环节都会被污染。
合规方面要格外注意。涉及人脸、车牌等个人信息的图片,用于公开发布或商业系统时最好做脱敏处理,模糊人脸区域或者干脆只保留头部轮廓信息。另外如果是从第三方数据集搬运,务必确认License允许你的使用方式,商用项目和论文实验对版权要求完全不同。不要因为省事踩了版权坑,这比模型效果差严重得多。
清洗完还应该做一次格式检查。YOLO训练要求每张图片对应一个同名txt标注文件,没有目标的图片可以有txt也可以为空,但目录结构要绝对规范。我会先跑一段脚本统计标注文件数量、框数量、类别分布,再抽查可视化结果,确保不是“图片和标签错位”这种低级问题,否则训练出来的模型会莫名其妙地乱飘框。
2.2 标注工具与YOLO格式转换细节
标注工具我用过不少,从最古老的labelImg到现在的X-AnyLabeling、CVAT都用过。对于这种几千张规模的数据集,我推荐用CVAT或者X-AnyLabeling,原因是它们支持自动标注预标注。比如先用一个在公开数据上预训练好的YOLO模型跑一遍所有图片,生成初始框,人工只需要修正漏标和错标,速度能快两三倍。
不过预标注有个大坑:模型对头盔这类小目标容易漏检,如果人工完全信任预标注结果,漏掉的那部分就会固化到训练集里。正确做法是预标注完成后,强制人工把所有图片按“头盔-人头”两个类别从头扫一遍,不要只盯着模型画出来的框看。
标注时有一个容易被忽略的细节:不要把背景中的广告牌、海报上的人头、车窗贴纸里的人像标成目标。真实路口这种干扰很多,一旦标入训练集,模型就会学到“凡是长得像人头的都框出来”,部署时误报率飙升。
标注完成后,如果是用CVAT导出的COCO格式或VOC格式,需要转成YOLO的txt格式。转换逻辑很简单:读取每个目标的x_min, y_min, width, height,归一化为中心点坐标和宽高,写入txt。归一化公式是:
x_center = (x_min + width / 2) / image_width y_center = (y_min + height / 2) / image_height box_width = width / image_width box_height = height / image_height训练YOLO时,模型期望的标签格式是class_id x_center y_center width height。注意这里不是左上角坐标,而是归一化后的中心点坐标。我见过有人转换时忘了归一化,导致训练Loss直接飞了,排查半天才发现是标签范围错误。
2.3 训练集/验证集/测试集划分与增强策略
划分数据集时,我强烈建议按场景划分,而不是随机划分。如果同一个路口的图片既出现在训练集又出现在验证集,验证分数会虚高,因为模型等于“开卷考试”。合理做法是先把所有图片按采集场景分组,再把整个组按比例分配到train/val/test中。
我用的是约8:1:1的比例,脚本很简单:
import os import random from collections import defaultdict # 假设已经按场景给每个图片文件名加了前缀,如 site1_001.jpg img_dir = 'images' label_dir = 'labels' scene_dict = defaultdict(list) for f in os.listdir(img_dir): scene = f.split('_')[0] # 场景分组 scene_dict[scene].append(f) train_files, val_files, test_files = [], [], [] for scene, files in scene_dict.items(): random.shuffle(files) n = len(files) train_files.extend(files[:int(n * 0.8)]) val_files.extend(files[int(n * 0.8):int(n * 0.9)]) test_files.extend(files[int(n * 0.9):]) # 按文件名列表拷贝图片和标签到对应目录 for split, file_list in [('train', train_files), ('val', val_files), ('test', test_files)]: os.makedirs(f'images/{split}', exist_ok=True) os.makedirs(f'labels/{split}', exist_ok=True) for f in file_list: os.rename(os.path.join(img_dir, f), os.path.join(f'images/{split}', f)) label_name = f.replace('.jpg', '.txt') if os.path.exists(os.path.join(label_dir, label_name)): os.rename(os.path.join(label_dir, label_name), os.path.join(f'labels/{split}', label_name))注意,测试集只在最后评估时用,训练过程中不要碰它,更不要拿测试集做早停判断。
数据增强方面,YOLO训练框架本身会在线做Mosaic、MixUp、HSV扰动、随机翻转等,不需要额外离线生成。但头盔检测有一个特例:不要开启上下翻转增强。因为监控摄像头都是固定俯拍视角,头盔的“上下颠倒”在真实场景里几乎不会出现,盲目翻转反而让模型学到错误的视角不变性。如果用了YOLOv8的augment参数,默认不含上下翻转,这个倒不用担心;但自定义增强Pipeline时要特别注意。
3. YOLO模型选择与训练调参实战
3.1 版本选型和预训练权重怎么选
YOLO这三年迭代太快,v5、v8、v10、v11一个接一个。我的选型原则是:项目生产环境追求稳定,优先用生态最成熟的版本。目前工业落地最多的是YOLOv8,文档全、社区大、部署方案多,遇到问题随便一搜就有答案。YOLOv5年代虽久但依然能打,尤其是一些老设备上的TensorRT方案特别成熟。v10、v11确实有精度提升,但如果你不是做算法研究、而是做项目交付,不要盲目追新,否则光适配部署就够呛。
具体到型号,8300张数据集这个量级,我推荐从yolov8s起步。nano太小,在复杂路口容易漏小目标;medium及以上对显存要求高,边缘设备推理也吃力。如果你手里的硬件是Jetson Orin Nano这类设备,yolov8s在640输入下能做到实时,精度也够用;如果在服务器上试跑、不急着部署,可以先用yolov8m拉一下精度上限,再蒸馏到小模型上。
预训练权重一定要用。不管你的数据集场景多特殊,COCO预训练模型已经学好了通用的边缘、纹理、形状特征,从头训练反而是浪费。下载的时候注意选择官方发布渠道,不要在来路不明的第三方链接下载权重。我习惯把权重放到项目根目录的weights/下,训练命令里直接指定model=yolov8s.pt,Ultralytics会自动从官方下载。
3.2 训练参数配置与损失函数观察
先梳理一下YOLOv8的损失函数构成,理解这些,后面看训练曲线才不至于一脸懵。YOLOv8的损失主要分三块:分类损失用BCE,边界框回归损失用CIoU,另外还引入了DFL(Distribution Focal Loss)来让边框回归预测一个分布而不是一个孤立值。DFL对小目标回归更友好,这也是YOLOv8在头盔这类小目标上表现优于老版YOLOv5的原因之一。
我习惯的参数配置给你一个参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
imgsz | 640 或 1280 | 头盔属于小目标,优先用1280训练,部署时再降到640 |
epochs | 300 | 早期停止可能在150轮触发,但预训练模型通常收敛较快 |
batch | 16 / 32 | 根据显存选,不够就16,配合梯度累积 |
lr0 | 0.01 | 预训练模型微调一般不调太高 |
lrf | 0.01 | 余弦退火的学习率下限 |
weight_decay | 0.0005 | 默认值 |
warmup_epochs | 3 | 让学习率平滑上升,防止前期震荡 |
close_mosaic | 10 | 最后10轮关闭Mosaic增强,让模型在真实分布上微调 |
训练过程中要盯两条东西:一条是box_loss、cls_loss、dfl_loss是否稳定下降,另一条是metrics/mAP50和mAP50-95是否同步上升。mAP50只判断“大概框到了没”,mAP50-95更严格,要求框的位置足够准。头盔检测里,若是做统计和预警,mAP50到0.92以上基本能用;若是做精确取证,mAP50-95得到0.75以上才放心。
这里特别说一个容易踩的坑:很多人只看mAP50高就认为模型没问题,结果部署时发现明明检测到了头盔,但框偏移了半个头,导致后处理判断“是否佩戴”时出错。这是因为mAP50对框位置误差不敏感。所以训练时最好把mAP50-95作为主要参考指标,这个指标上不去,说明回归分支没学好。
3.3 训练中常见异常:bn崩溃、loss不变、混淆矩阵异常
我实际训练时遇到过几次BatchNorm崩溃的情况,就是loss突然变成NaN或者inf。这种问题业内叫“BN崩溃”,本质是BatchNorm层在训练过程中统计量跑飞了。原因不外乎三个:学习率太大、某个batch里出现异常标签(比如归一化坐标超过1或小于0)、或者图片里全是空标签导致某些batch统计量方差为0。排查方法很简单:先调低lr重训,不行就检查所有txt标签里的坐标是否都在0~1之间,特别是人工标注转格式时最容易出这种问题。
还有一个很常见但容易被忽略的:训练了很久loss不降,mAP完全不动。这种情况十有八九是数据划分出了问题,比如train和val是同一批图片的不同拷贝,或者是标注格式错位导致模型从随机噪声里学不到东西。我的排查顺序是:先可视化几张训练图片的标注框,确认标签没问题;再单独用val集跑一次推理,看输出框是否和真值对得上;最后看配置文件里的类别数是不是和数据集一致。
训练结束后,Ultralytics会输出confusion_matrix.png。有人看到混淆矩阵对角线数值加起来不是100%就慌了,其实这是正常的,因为矩阵里包含了背景类(background),而且预测结果里有置信度阈值过滤,阈值以下的分到了background。真正要注意的是矩阵里helmet和head之间有没有明显混淆——如果大量“戴头盔”被预测成“head”,大概率是标注框口径不统一,比如有的框包住了整颗头+头盔,有的只框了头盔边缘。这种问题只有回到数据层面修正,调模型参数没用。
4. 模型部署与智慧交通场景落地
4.1 从best.pt到ONNX/TensorRT导出
训练完不要直接拿best.pt上生产,PyTorch格式在服务端推理效率太低。我一般的流程是:先导出ONNX,再根据部署设备选择转成TensorRT引擎或者直接用ONNX Runtime推理。
导出命令很简单:
yolo export model=runs/detect/train/weights/best.pt format=onnx dynamic=True imgsz=640加了dynamic=True可以让输入尺寸灵活变化,适应不同分辨率的监控流。部署到Jetson设备上时,再用TensorRT做一次加速:
trtexec --onnx=best.onnx --saveEngine=best.engine --fp16TensorRT开启FP16精度后,在边缘设备上的推理速度通常能提升一倍以上。但注意,FP16在某些老旧GPU上精度损失略明显,如果发现检测框抖动变严重,回到FP32也不丢人。
4.2 小目标、夜间和运动模糊场景的针对性优化
智慧交通项目最常见的部署反馈是“白天还行,晚上就崩”。针对夜间场景,单纯增加夜间训练样本还不够,我还有几个组合拳:
第一个是提高输入分辨率。训练时用1280,部署时如果算力够,也尽量别降到640以下。分辨率降低对普通目标影响不大,但对头盔这种小目标简直是灾难,直接从3x3像素变成1x1像素,谁也检测不出来。
第二个是图像预处理。夜间监控画面经常偏暗,在推理前做一次自适应直方图均衡化(CLAHE),能明显提升暗部细节的可见度。但这步必须和训练时的预处理保持一致,否则等于推理时换了数据分布,效果反而更差。最稳妥的做法还是把CLAHE处理后的图片放进训练集里一起训练,让模型自己适应增强后的图像风格。
第三个是跟踪去抖。单帧检测在运动模糊场景下会产生漏检,但帧率通常在25fps以上,相邻帧之间目标位置变化不大。我会在检测层后面接一个ByteTrack或者DeepSORT,用跟踪的结果来平滑检测缺失,如果连续三帧都能检测到头盔,只有中间一帧丢了,跟踪器会补上,最终事件逻辑仍能稳定触发。这比单纯调高检测阈值更有用。
4.3 与视频监控系统联动的推理逻辑
部署到真实监控系统时,检测模型只是最底层的一环,真正决定用户体验的是后处理逻辑。举例来说,如果模型输出了helmet和head两类目标,最终“未戴头盔事件”的判断不能只看某一帧有没有head框,而要结合跟踪ID做时序判断。
我的做法是这样的:对每个跟踪ID维护一个状态机,连续N帧且至少M帧检测到head,才判定为“疑似未佩戴”;连续N帧且至少M帧检测到helmet,则判定为“已佩戴”。这样做的好处是避免单帧误检导致的事件风暴,同时也能捕捉到“骑手中途摘掉头盔”这类动态变化。阈值N和M要按实际帧率调整,25fps下我一般设N=15、M=10。
如果部署设备是手机摄像头、实时预览这种场景,可以把YOLO模型导出为NCNN或者TFLite格式,做端侧推理。我自己实测下来,手机端跑yolov8n在640输入下能稳定30fps以上,但小目标漏检率会比Jetson上的yolov8s高一截。所以不是紧急项目,建议优先用边缘盒子而不是手机。
5. 常见问题速查与实际提升技巧
5.1 问题排查速查表
整理了一下我从数据整理到部署过程中遇到的高频问题,做成速查表方便你对照:
| 问题 | 可能原因 | 解决建议 |
|---|---|---|
| 训练loss变成NaN | 学习率过大或标签坐标越界 | 调低lr0到0.001,检查所有txt坐标是否在0~1 |
| mAP高但实拍误报多 | 负样本不足或场景偏差 | 增加纯路面、行人、空车图片作为负样本,重训 |
| 夜间漏检严重 | 暗光样本太少 | 增加夜间样本到至少1000张,或做CLAHE增强后加入训练 |
| helmet和head互相混淆 | 标注框口径不统一 | 统一为头盔外轮廓、头皮外轮廓,重新检查有争议框 |
| 部署变慢 | 模型太大/分辨率太高 | 尝试yolov8s或n,导出TensorRT FP16引擎 |
| 远距离小目标全漏 | 训练时分辨率太低 | imgsz改成1280训练,部署测640/1280权衡 |
| 同一目标重复框 | NMS阈值太低 | 调高NMS IoU阈值到0.6~0.7,配合跟踪去重 |
| 混淆矩阵总和不是100% | 存在background类或置信度阈值过滤 | 属正常现象,只需关注目标类之间的混淆情况 |
5.2 数据迭代与难例挖掘
训练第一版模型后,别急着交付。我的习惯是:把模型放到真实测试路口跑一周,收集所有误检和漏检的图片,做一次“难例挖掘”。所谓难例,就是模型置信度很高但判断错误的图片、以及置信度很低但真实存在的目标。把这些难例单独拉出来,让人工重新标注,并入原始训练集做增量训练。
这个流程能让模型的泛化能力提升非常明显。举一个真实例子:第一版模型在某个路口频繁把“白色安全帽形状的路灯罩”识别成头盔,我收集了30多张这种图,标为背景负样本,重训后误报率直接降了一个数量级。难例挖掘比盲目增加“普通图片”效率高十倍。
5.3 用蒸馏和量化把精度换成速度
如果你的部署设备性能有限,又不想牺牲精度,可以试试模型蒸馏。比如用训练好的yolov8l当教师模型,去蒸馏yolov8s或yolov8n学生模型,让大模型的“暗知识”迁移到小模型上。Ultralytics对蒸馏没有内置支持,但网上有不少开源实现,核心思路是在正常训练损失基础上,额外加一个让学生模型的特征图模拟教师模型输出的损失项。
另一个思路是量化。TensorRT INT8量化可以用更小的内存跑出接近FP16的精度,但对校准数据集有要求——最好从你的验证集里选几百张覆盖不同场景的图片做校准,而不是随便拿训练集充数。我实测过,量化后推理速度提升30%左右,精度损失在1%以内,属于性价比很高的优化手段。
最后再分享一个实际体会:一个头盔检测系统上线后,真正的挑战不在算法,而在数据持续迭代。我一般按两周一版的节奏,把新场景的坏case收集起来补标,模型越用越准。如果你正在做智慧交通相关项目,别迷信大模型,先把数据、标注、评估口径这三样基础打牢,YOLO家族能给你带来的收益会比想象中大得多。