
最近在整理手里的智慧交通项目时我把一套头盔检测数据集重新梳理了一遍8300张图片YOLO格式覆盖了路口摄像头会碰到的各种场景。这套数据本来是我自己项目里用的后来不少朋友来问头盔检测该怎么做我发现大家缺的往往不是模型结构而是数据本身。这篇就把我构建、清洗、标注、训练到部署这套方案的全过程写出来包括踩过的坑尤其是那些跑通Demo之后才会遇到的问题。1. 头盔检测这类任务数据到底该长什么样才够用1.1 为什么说数据决定了目标检测的上限头盔检测在智慧交通里属于典型的中远景目标检测任务监控摄像头架设在路口立杆上画面里摩托车、电动车的骑手通常只占几十到上百像素头盔这种小目标很容易被周围环境干扰。模型结构上YOLO系列的开源权重已经很成熟大家拿来即用真正拉开差距的其实是训练数据。我见过不少朋友直接拿公开数据集跑YOLO效果不理想就怀疑模型不行其实问题经常出在数据分布上和实际场景不一致。公开数据集大多是平视角度的街拍照片而路口摄像头是俯视或斜视角度骑手在画面里的姿态、遮挡方式完全不同。8300张图听起来不算多但如果场景覆盖合理、标注干净效果会远好于几万张堆砌的图片。我的经验是先搞清楚数据要解决什么问题再决定数据怎么收集、怎么标注这比先选模型重要得多。1.2 智慧交通场景下数据集要满足的特殊约束实际路口项目的图片和普通目标检测数据集有几个明显差异整理数据时必须考虑进去拍摄角度摄像头一般装在5到8米高的立杆上视角是向下倾斜的头部区域会被身体、车体遮挡一部分标注框不能照搬平视数据集的经验。背景复杂度路口有护栏、绿化带、广告牌、其他车辆目标检测容易把背景中的圆形物体误认为头盔。光照和天气逆光、树荫、夜间路灯、雨天反光都会改变头盔外观。同一顶头盔在不同光照下特征差异可能比不同头盔之间的差异还大。目标密集程度红灯等待区经常多辆两轮车并排骑手互相遮挡这对检测器的召回能力要求很高。基于这些约束我在整理这套8300张数据集时特意按白天、黄昏、夜间、雨天划分了场景比例还保留了一批画面里有大量两轮车聚集的样本。宁可少收一些场景单一的图片也要保证每个典型路口情况都有覆盖。实际项目里如果数据里有一类场景完全缺失模型上线后大概率会在那个场景翻车。2. 8300张数据集的类别设计与标注质量检查2.1 类别怎么分两种设计思路的对比头盔检测项目最常见的分类思路有两种效果差别很大。第一种是按人头框标注对象是骑手的头部区域类别分为head_with_helmet和head_without_helmet。这种做法的优点是直接对应检测目标后座乘客的头部也可以独立标注遮挡时只要头部可见就能检出。缺点是需要标得比较细头部在画面里很小标注工作量大。第二种是按整车框标注对象是整个两轮车含骑手再通过分类或属性识别来判断是否戴头盔。这种做法的优点是框大、好标但遇到后座乘客就很尴尬一辆车两个人都没戴头盔或只有一个人戴头盔时整车级别的标签无法表达。我最终选择了第一种方案还额外加了一个person类别用来提供上下文。实际效果对比下来按人头框训练的模型在路口场景的召回率明显更高尤其能正确处理骑车人戴了头盔、后座人没戴这种很常见的情况。类别设计层面我建议按下面这个对照表来取舍方案框定对象优点缺点按人头框头部区域检测直接、召回高能区分前后座标注精度要求高小目标标签多按整车框两轮车骑手标注简单、不易漏标无法表达部分人戴头盔的复杂情况人头框上下文类别头部骑手/车信息最完整适合后续扩展标注类别多训练和调参成本上升2.2 数据集的场景分布与标注工具这套数据集的场景分布我是按真实路口的统计来配比的白天正常光照约占40%黄昏和逆光约占20%夜间路灯场景约占20%雨天和阴天约占20%。每张图片的平均目标数量控制在2到6个头盔目标之间既保留足够的正样本密度又不会因为单张目标太多导致训练时增益失衡。标注工具用的是LabelImg和X-AnyLabeling混着来。小项目用LabelImg够用导出的格式是VOC XML如果要批量处理、做半自动预标注X-AnyLabeling可以直接挂一个YOLO模型做辅助标注人工只负责修正效率能提高不少。半自动预标注在8400张这种规模下非常实用我的做法是先拿公开权重对全部图片做一轮推理把置信度高于0.9的框自动生成初稿人工只处理0.3到0.9之间那些拿不准的框整体时间至少省了一半。2.3 标注质量检查的三道关标注质量直接影响模型的精度天花板。我踩过最大的坑是标注框不贴边框比头部大一圈或者只框到头盔没有框到下巴导致模型学到的头部特征很飘。后来我定了三道检查流程尺寸分布检查统计所有标注框的宽高占图片宽高的比例头盔检测里大量框占比在2%到6%之间如果发现很多框占比超过15%基本是标注框把上半身也框进去了。标签一致性抽检随机抽5%的图片逐张查看标注框与类别是否正确。重点看两类最容易出错的情况骑手戴了帽子而不是头盔、头盔拿在手里而不是戴在头上。坐标越界处理YOLO格式要求归一化坐标在0到1之间靠画面边缘的目标经常会出现坐标越界需要用脚本统一做截断处理否则训练时OpenCV读取会报错或导致样本作废。3. 从原始标注到YOLO训练格式转换和数据集划分的实操3.1 为什么YOLO的标注格式是txt而不是json、xmlYOLO训练时读取的标签是每一张图片对应一个同名txt文件文件里每行是类别序号 x_center y_center width height坐标全部除以图像宽高做了归一化。这样做的好处是无论输入图片尺寸怎么变标签都不用跟着改模型内部resize时也方便处理。而COCO的JSON或VOC的XML虽然信息更丰富训练时还是要转成这种格式所以直接用YOLO格式能省掉很多中间环节。3.2 COCO JSON转YOLO txt的参考脚本如果你手里的数据是COCO格式或者标注工具导出的是COCO JSON下面这个转换脚本可以拿来直接用。核心逻辑就是读取标注框把绝对坐标转换成归一化坐标再写入txt文件import json import os def coco_to_yolo(coco_json_path, img_root, label_root): with open(coco_json_path, r, encodingutf-8) as f: coco json.load(f) img_map {img[id]: img for img in coco[images]} cat_map {cat[id]: i for i, cat in enumerate(coco[categories])} os.makedirs(label_root, exist_okTrue) for ann in coco[annotations]: img_info img_map[ann[image_id]] img_w, img_h img_info[width], img_info[height] x, y, w, h ann[bbox] # COCO的bbox是左上角坐标加宽高, 转成中心点坐标 x_center (x w / 2) / img_w y_center (y h / 2) / img_h w_norm w / img_w h_norm h / img_h # 边界保护, 防止越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w_norm min(max(w_norm, 0.0), 1.0) h_norm min(max(h_norm, 0.0), 1.0) txt_name os.path.splitext(img_info[file_name])[0] .txt txt_path os.path.join(label_root, txt_name) cat_idx cat_map[ann[category_id]] with open(txt_path, a) as f: f.write(f{cat_idx} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) if __name__ __main__: coco_to_yolo(instances_train.json, images, labels_train)转换完成后记得检查每个txt文件是否与对应图片同名并且确认类别编号和data.yaml里的names顺序一致。这里最容易翻车JSON里的类别顺序是字母序YAML里的names顺序如果没对上训练出来的模型所有类别都会错位表面看loss正常实际结果完全不能用。3.3 目录组织与train、val、test划分YOLO工程的目录结构我建议按下面这样组织后续无论是用Ultralytics还是自己写训练脚本都很方便helmet_dataset/ ├── images/ │ ├── train/ │ │ ├── img_00001.jpg │ │ ├── ... │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── helmet.yaml划分比例我用的8:1:1也就是约6640张训练、830张验证、830张测试。划分时要注意别按文件名顺序直接切最好先按图片所属的路口或拍摄时间段分组再随机打乱否则同一条路段、同一时间段的图片同时出现在训练集和验证集里验证结果会虚高。这一步很多人忽略等到部署到新路口才发现精度掉得厉害。helmet.yaml的内容如下path: /data/helmet_dataset train: images/train val: images/val test: images/test nc: 2 names: 0: head_with_helmet 1: head_without_helmet写完后可以用一行命令验证标签和图片是否匹配确认后就能开始训练了python -c from ultralytics.data import YOLODataset; dYOLODataset(helmet.yaml); print(len(d))如果数据加载正常控制台会打印出训练集图片数量。4. 训练配置、Loss曲线与第一次跑通的经验4.1 YOLOv5和YOLOv8怎么选新项目我优先选YOLOv8。理由很实际Ultralytics的生态最省心数据加载、增强、导出、部署链路都是现成的遇到问题搜到的资料也最多。YOLOv5胜在稳定、历史包袱少如果你的推理设备只支持旧版本的部署框架选v5更稳妥。头盔检测这类单阶段检测任务两个版本都能胜任没必要在选型上花太多时间先跑通一个能用的模型再优化。考虑到目标是路口实时监测我用的是YOLOv8n这个最小的版本作为baseline。它的参数量只有3.2M左右在边缘设备上推理速度非常快。如果验证集mAP达不到要求再逐步换成s、m版本。不要一开始就上大模型后期的量化、裁剪会非常痛苦。4.2 关键超参数怎么定我在这套数据集上的训练配置是这样的yolo detect train \ modelyolov8n.pt \ datahelmet.yaml \ epochs200 \ imgsz640 \ batch32 \ patience30 \ close_mosaic10 \ projectruns/helmet \ nameexp1几个超参的解释imgsz640头盔是小目标理论上用1280输入能涨点但训练显存和推理耗时都会翻倍。我选择640起步后面单独做高分辨率推理测试。batch32取决于显存一般单卡12G显存可以跑到这个值。batch太小会导致BN的统计量不稳定头盔这类小目标检测对BN很敏感。close_mosaic10YOLOv8默认使用Mosaic增强但Mosaic生成的图片和真实场景差异较大最后10个epoch关闭它让模型在接近真实分布的数据上收尾能避免最终精度波动。patience30验证集指标连续30个epoch不提升就早停避免无效训练浪费时间。训练过程中我习惯盯两个东西训练集loss曲线和验证集mAP曲线。正常的曲线是loss前50个epoch下降迅速后面趋于平缓mAP50稳步上升。如果loss下降很快但mAP一直不动通常意味着类别不平衡或者标签有问题这时候先别急着调参回去看数据。4.3 训练阶段最容易踩的三个坑坑一Loss不降反升。有一次我把batch调到64结果训练loss在前10个epoch直接爆炸因为显存不够触发了隐性的梯度累积异常。解决办法是恢复到32或者开启梯度累积来模拟大batch。坑二BN层崩溃。现象是训练中loss出现NaN主要原因是输入图片里出现了纯黑或纯白的异常样本或者标签越界导致计算异常。我把异常样本过滤掉之后就恢复正常了。坑三验证集mAP很高但实际场景很差。这是最隐蔽的坑根源是验证集和训练集来自同一批路口模型相当于背题了。我改用了一个完全没参与训练的新路段做测试集mAP直接掉了10个点。这个体验让我后来做任何数据集都会刻意保留一个未知路段作为最终测试集。训练结束后验证命令也很简单yolo detect val modelruns/helmet/exp1/weights/best.pt datahelmet.yaml控制台会输出mAP50、mAP50-95等指标。对这个头盔检测任务我定的及格线是mAP50不低于0.85mAP50-95不低于0.6。如果达不到优先检查数据问题其次再考虑模型和超参。5. 遮挡、小目标和夜间误检漏检高发区的针对性优化5.1 真实路口场景里的三类高频错误模型训练完真正上路口测试时问题会集中暴露在三个方向后座乘客漏检前座骑手戴了头盔后座乘客没戴但后座乘客被前座身体挡住大半头部只露出很窄一条边检测器经常漏掉。我最初的模型对单骑手场景的mAP有0.9但后座乘客的召回率只有0.6左右。误把头盔当水桶、花盆路口绿化带里的水桶、石墩、圆形垃圾桶在某些角度下和头盔轮廓很像检测器容易给出高置信度的误检。夜间灯光干扰夜间模式下头盔反光、车灯过曝目标和环境对比度变低漏检率明显上升。这些问题不是单纯调阈值能解决的要从数据和增强两个层面去压。5.2 数据增强怎么配合场景YOLOv8自带的增强已经很强但针对头盔场景我额外做了两类增强一是小目标复制粘贴。我写了一个脚本把训练集中小尺寸的头盔目标抠出来随机粘贴到其他图片的空旷区域同时生成新的标注。这个操作相当于把稀有小目标样本的权重提上来对后座乘客漏检的问题改善非常明显。二是夜间模拟增强。对白天图片做亮度降低、加高斯噪声、局部过曝模拟夜间摄像头效果。注意增强不要做得太过否则模型会把黑暗本身当成特征反而在正常白天场景误检。我控制增强后的图片不超过训练集的30%。还有一个容易被忽略的点左右翻转增强。头盔本身左右对称翻转没有问题但如果你的项目后续要接车牌识别翻转会导致车牌文字镜像训练标签和后续任务矛盾。所以我训练头盔检测时开了翻转但在同一份数据上跑车牌识别时就关了两个任务分开处理。5.3 用混淆矩阵和PR曲线定位问题训练完不要只看mAP我习惯导出混淆矩阵图来定位具体的错误模式yolo detect val modelbest.pt datahelmet.yaml plotTrue生成的confusion_matrix.png里如果背景列上有大量头盔目标的误检说明模型把背景当成了头盔优先加负样本或调高置信度阈值。如果头盔类被分到未戴头盔类说明两类特征差异不够需要检查标注边界是否清晰。PR曲线也要重点看。头盔检测在路口的实际运行中我更看重召回率因为漏掉一个未戴头盔的人比多报警一次危害更大。所以部署时我不会把置信度阈值设得很高一般定在0.3到0.35之间用后端逻辑过滤一部分低置信度的误报。6. 部署到路口边缘设备时模型压缩和推理加速的取舍6.1 从PyTorch到ONNX再到TensorRT训练完成后部署到边缘设备的第一步是导出为ONNXyolo export modelbest.pt formatonnx dynamicTrue opset12如果目标设备是NVIDIA Jetson系列再进一步转成TensorRT引擎trtexec --onnxbest.onnx --saveEnginebest.engine --fp16fp16的精度损失通常很小mAP可能只掉1到2个点但推理速度能提升将近一倍头盔检测这类语义明确的任务完全够用。int8量化能跑得更快但需要校准集而且精度可能出现5%以上的波动如果模型本身mAP就在及格线边缘不建议上int8。如果是瑞芯微RK3588这类平台需要先转成ONNX再通过RKNN-Toolkit转成rknn格式。不同平台的算子支持度有差异我建议在项目早期就确认部署平台宁可先用平台支持的算子把模型跑通也不要先追求模型花哨的结构。6.2 1080P实时推理的实测数据我在Jetson Orin Nano上测试了YOLOv8n导出的TensorRT引擎输入分辨率640x640fp16下单张推理耗时约8到12毫秒处理1080P的实时视频流完全够用还能留出余量做多路并行。如果换成CPU推理盒比如工业级的x86工控机ONNX Runtime下单张大概在30到60毫秒只能支撑单路或者两路视频流。实测中我还发现推理耗时不是唯一瓶颈视频解码和后处理同样占资源。多路视频流场景下建议把解码放在单独的线程做帧跳过策略比如每秒只处理10到15帧既保证不漏掉关键画面又给设备留出富余算力。6.3 工程化部署的几个细节部署阶段容易翻车的都不是模型本身而是外围工程细节。我总结了几点NMS阈值IoU阈值用0.45到0.5都可以置信度阈值在路口场景建议0.3起步如果误报太多再逐步上调不要一上来就设0.5。检测框后处理头盔检测通常只需要给后端平台输出未戴头盔目标的中心点和置信度不需要框的原始坐标全部上抛减少网络传输压力。数据回流部署后定期收集新场景的误检和漏检样本合并到训练集重新迭代。智慧交通场景的变化比想象中快夏天遮阳帽、冬天棉帽都会让模型犯错这类新样本往往比额外收集旧场景图片更有价值。7. 模型效果评估与后续迭代的扩展方向头盔检测整个链路跑通后最终在独立测试集上的结果大致是mAP50在0.88左右mAP50-95在0.63左右夜间场景召回率比白天低5到8个百分点但通过数据增强和阈值调整已经能保证路口管控对未戴头盔行为的有效识别。如果后续还想继续提升可以考虑下面几个方向。首先是在推理阶段引入SAHI切片推理。头盔是小目标把大图切块后再推理可以明显提升召回率代价是推理耗时增加不适合实时场景更适合离线批量研判。其次是扩展类别。现有数据集只有头盔相关类别实际智慧交通项目往往还需要同时检测车牌、车辆类型、闯红灯行为。建议在标注阶段就预留好类别扩展位比如同时标注motorcycle、person后续做多任务时就不用重新标注。最后是模型蒸馏。如果希望精度高一点又不想换大模型可以用YOLOv8m或YOLOv8l在同样数据上训练一个教师模型再把知识蒸馏回YOLOv8n。我试过这种方式学生模型的mAP能提升两三个点推理速度保持原样对边缘部署非常友好。我个人在做了这么多头盔检测项目之后最大的体会就是数据不是越多越好而是越对越好。8300张图如果分布均匀、标注干净效果可以超过几万张堆砌出来的公开数据。先把镜头角度、光照、遮挡、小目标这些问题在数据层面解决掉再谈模型结构改进能少走非常多的弯路。希望这篇能把你在构建自己的头盔检测数据集时最想避开的坑都提前标出来。