ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于YOLOv9的头盔检测:从原理到部署的完整实战指南

基于YOLOv9的头盔检测:从原理到部署的完整实战指南 简介面向智慧交通与目标检测应用的YOLOv9电动车骑行人员头盔佩戴检测系统专为深度学习入门者、计算机相关专业学生及毕业设计开发者设计提供完整的Python源码、详细运行教程与训练好的模型可直接用于道路场景头盔佩戴识别也可迁移到自定义检测任务。资源包共188个文件涵盖py源码、yaml配置文件、pt权重模型、jpg测试图片及评估曲线等类型压缩包大小69.32MB目录结构清晰便于按模块查阅与二次开发。随附教程覆盖环境配置、数据集准备、模型训练与测试全流程并保留训练评估曲线与推理示例帮助使用者快速复现项目、理解YOLOv9训练逻辑。目前已有238人学习下载代码经测试运行通过适合作为目标检测方向课程设计、毕业设计或企业员工技术实践的实战参考。1. 头盔检测为什么是路口监控里最难啃的骨头YOLOv9 带来的转机「道路电动车骑行人员头盔佩戴检测」这类需求在智慧交通项目里看着简单——无非是画框、分类、给个置信度。但真实落地时它会反过来让很多团队翻车白天晴天表现优秀的模型到了傍晚逆光就变成黑匣子一样乱框一个摄像头视野里挤着七八辆电动车时漏检率直接从 3% 跳到 30%。标题里这套基于 YOLOv9 的实现本质上不是「再跑一个目标检测 demo」而是把数据标注、训练、评估曲线、模型部署串成一条完整链路让交付的系统在真实路口能稳定框出每个骑行人员的头部并判断头盔是否佩戴。这篇笔记按这条链路拆解新手能照着把环境跑通熟手可以直接借鉴参数与避坑经验。2. 从 YOLOv9 原理到选型判断这套 python 源码为什么值得跑通2.1 GELAN 与 PGIv9 比 v8 强在哪YOLOv9 在 2024 年初发布核心贡献是两件事GELAN 特征融合结构和 PGIProgrammable Gradient Information可编程梯度信息训练策略。GELAN 是从 ELAN 演进过来的轻量高效聚合网络特点是保持推理开销基本不变的同时通过跨层多尺度融合让浅层位置信息和深层语义信息互相补位。和 YOLOv8 的 C2f 模块相比GELAN 更看重分组卷积与跨层拼接的配合在参数受限的边缘设备上表现更稳定。PGI 解决的则是训练时的信息瓶颈问题。深层网络在做深度监督时梯度回传到浅层后会被多个分支「稀释」浅层学不到完整的梯度信号导致小目标的位置信息丢失。PGI 的思路是引入一个可编程的梯度信息分支让浅层在训练时拿到更完整的监督信号推理时这个分支可以裁掉。这正是 YOLOv9 在小目标场景上比 v8、v5 更有优势的根本原因。头盔佩戴检测恰好是小目标密集场景一个 1080P 路口画面缩到 640 分辨率后头盔区域往往只有 20×30 像素左右。v5 时代这类目标基本靠运气v9 的 PGI 能把这些小目标的梯度信号保住训练出来的模型对边缘和局部纹理更敏感。选型判断的依据很简单——同一批数据跑 v8 和跑 v9 对比验证集 mAP 曲线v9 通常高 23 个点这个差异在夜间和逆光场景下还会放大。2.2 头盔检测的任务边界别拿通用检测的思维套头盔检测的类别定义要先讲清楚否则标注阶段就废了。常见做法是定义两个类别helmet戴头盔骑行人员的整颗头部区域和 head未戴头盔的头部区域。注意「戴头盔人员的头部」和「头盔」是两个概念框的是「头盔」的整体而不是只框盔壳这样训练出来的模型对半盔、后脑勺、帽檐的容错更高。这个任务与通用目标检测的关键差异有三点。第一目标小且宽高比接近 1:1 到 1:1.5不像行人和车辆那种长条框所以分配 anchor 时要往小框方向偏或依赖 YOLOv9 的动态 assign 机制解决。第二目标天然密集且互相遮挡路口一辆电动车后座还坐着一个人两个头盔在画面上几乎贴在一起NMS 的 IoU 阈值需要从默认 0.45 往下调。第三类别极端不平衡正常路口戴盔率可能是 70% 以上helmet 样本远多于 head 样本如果不做类别权重或难例挖掘head 类基本学不出来。明白这三点再看项目的 yaml 配置文件就不会被里面的参数吓到。nc 固定为 2names 按顺序写成 [head, helmet]顺序一旦定义好整个训练周期都不能再改因为它直接写进每个 txt 标注的第一位数字。这个顺序问题是高频翻车点在第 5 章会专门展开。2.3 拿到 python 源码先做三件事环境、权重、目录拿到标题里这套 python 源码第一步不是看训练命令而是先把依赖装干净。不要用全局 python 环境去跑很多坑都来自依赖版本互相打架。常规做法是创建一个虚拟环境python -m venv .venv source .venv/bin/activate pip install -r requirements.txtrequirements.txt 如果提供的话先检查里面 torch 和 torchvision 的版本是否和你的 GPU 驱动匹配。我一般会先跑一行python -c import torch; print(torch.__version__, torch.cuda.is_available())验证 CUDA 可用否则后面训练会无声无息地全跑 CPU一个 epoch 慢十几倍。目录梳理按常见结构来weights 放训练好的模型和预训练权重data 放数据集和 yamldetect.py 和 train.py 是入口脚本utils 里是数据集转换、评估、可视化工具。标题里说了带训练好的模型那就先清理 weights 目录别让旧权重和新代码版本对不上。验证权重最快的方法是拿 example 图片跑一次推理python detect.py --weights weights/best.pt --source data/example.jpg --conf 0.25能看到框和置信度说明环境通了一半。注意 conf 参数在验证阶段不要低于 0.2否则测的是「假阳性很多」的检测器而不是真实能力。「详细运行教程」这种交付物我习惯先看其中的 README 或 run_guide 文档它会写明 python 版本要求、数据目录和权重路径。但要警惕教程里给的路径和你实际解压路径不一致的情况常见做法是以源码里的默认配置为准把路径全部改成绝对路径。另外把训练好的模型当成基线而不是最终交付物教程里跑出来的评估曲线只是复现真正要交付的是你自己在本地数据集上的曲线。3. 数据准备与标注头盔检测项目的真功夫都在这一章3.1 类别定义与 box 标注规范一张图决定模型上限很多团队拿到源码就直接用公开数据集跑结果模型在自己的路口上完全不能用。问题通常出在数据分布上公开的 SHWD 或安全帽数据集主体是工厂作业场景头盔样式、拍摄角度、画面比例和路口电动车场景差得很远。电动车头盔有半盔、全盔、冬盔、夏盔还有大量前座后座叠加的情况这些只能靠自己采集和标注。标注规范我一般这样定使用 LabelImg输出 Pascal VOC 格式类别名必须是 helmet 和 head 这两个单词不要用中文不要带下划线。框的边界要紧贴头部区域戴头盔的框要覆盖头盔外沿和下巴位置不戴头盔的框从头顶贴到下巴。不要框到肩膀不要框到电动车否则模型会学进大量背景噪声。这里有一个决定模型上限的细节同一个头盔从侧后方看和从正前方看形状差异极大。如果标注时一个头盔只框了一个方向的形状另一个方向就会漏检。所以每类正样本至少覆盖正面、侧面、后侧、俯视四个角度每个角度不少于 200 张图。标注完后还要检查是否有「头盔样本里夹带了没戴头盔的头部」的情况这是类别混淆最常见的来源。3.2 用 python 把 VOC 标注转成 YOLO 格式转换脚本与目录生成YOLOv9 训练需要的是 YOLO 格式每行一个目标格式为class_id x_center y_center width height坐标全部归一化到 01。下面这段 python 脚本可以批量把 LabelImg 生成的 XML 转成 txt注意类别顺序要和数据集的 yaml 完全一致import os import xml.etree.ElementTree as ET from pathlib import Path # 类别顺序必须与 helmet.yaml 中的 names 保持一致 CLASSES [head, helmet] def voc_to_yolo(xml_path, img_w, img_h, out_txt_path): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASSES: continue # 过滤掉不属于这两类的标注 cls_id CLASSES.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 归一化到 0~1 x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) # 遍历 images 目录逐张转换 images_dir Path(datasets/helmet/images) labels_dir Path(datasets/helmet/labels) labels_dir.mkdir(exist_okTrue) for img_path in images_dir.rglob(*.jpg): xml_path img_path.with_suffix(.xml) if not xml_path.exists(): continue # 读取图片真实宽高不能用 XML 里的假设值 import cv2 img cv2.imread(str(img_path)) h, w img.shape[:2] out_txt labels_dir / (img_path.stem .txt) voc_to_yolo(str(xml_path), w, h, str(out_txt))逻辑说明脚本先从 XML 里解析每个object的类别和 bndbox 坐标再将 xmin、ymin、xmax、ymax 转换成中心点加宽高的归一化表示。类别 id 由 CLASSES 列表的 index 决定所以 CLASSES 的顺序必须与训练时 yaml 的 names 完全一致否则模型学到的类别和标注对不上。参数说明img_w 和 img_h 取的是图片真实分辨率而不是 XML 里可能记录的旧值这样即使图片被缩放或重存过坐标也不会错位。脚本里用了 OpenCV 读取图片来拿宽高所以依赖中必须有 opencv-python。转完后必须做一次合法性检查统计所有 txt 里出现过的类别 id 最大值如果等于 2 或更大说明类别标注有越界训练时一定会报错。另外还要检查是否有坐标小于 0 或大于 1 的情况。3.3 数据增强别无脑开针对小目标的三个调整YOLOv9 默认开启大量数据增强但在头盔场景下有三项需要手动调整。第一项是亮度与对比度抖动这是针对逆光场景的关键增强。把 hue、saturation、value 中的 value 抖动范围适当加大能模拟早晚阳光角度变化让模型学到「过曝后依然像头盔」的特征。第二项是 mosaic 增强的开关时机。mosaic 把四张图拼成一张对小目标检测非常有用因为它强制模型在小尺寸下识别目标。但如果在训练后期还开着拼接产生的目标会小到只剩十几个像素反而干扰收敛。常见做法是前 80% 的 epoch 保持 mosaic 为 1.0最后 20% 的 epoch 把 mosaic 调成 0只用真实尺寸的图片精调。第三项是横向翻转。头盔是左右对称物体水平翻转等于免费把样本量翻倍而且不会产生语义错误。但要注意垂直翻转不要开路口摄像头都是俯拍垂直翻转后头盔纹理朝向完全不符合实际分布会让验证集 mAP 虚高、实地效果下降。注意数据增强参数不是越大越好。我见过把旋转角度开到 90 度的项目训练出的模型对「横着的头盔」响应极高真实路口的正常姿态反而漏检。头盔检测的旋转增强上限一般控制在 30 度以内。4. 用源码跑通训练最小启动命令与六个必调参数4.1 最小启动命令从一张图到第一个 epoch环境装好、数据集转换完毕之后训练入口就很简单了。YOLOv9 的常见训练命令格式如下项目源码里一般也保留了这个入口python train.py \ --data data/helmet.yaml \ --weights yolov9-c.pt \ --batch-size 16 \ --epochs 100 \ --imgsz 640 \ --device 0这个命令做的事是读取 data/helmet.yaml 里的数据路径和类别定义加载 yolov9-c.pt 预训练权重把输入图片缩放到 640×640在 GPU 0 上训练 100 轮。第一次跑通时不要贪多epochs 可以先设 10batch-size 设 8确认训练循环能正常推进、每轮 loss 在下降再停掉正式开跑。data/helmet.yaml 是数据配置的核心内容一般长这样path: /abs/path/to/datasets/helmet train: images/train val: images/val nc: 2 names: [head, helmet]逻辑说明path 建议写绝对路径写相对路径在 Windows 和 Linux 之间切换时非常容易翻车。train 和 val 是相对于 path 的目录名目录里放的是图片标签文件会按同名自动从 labels 目录里找。nc 是类别数names 的顺序就是标注文件里数字 id 的映射。4.2 六个必须手调的参数epoch、imgsz、batch、lr0、mosaic、nc下面这六个参数基本决定了头盔检测项目的最终效果每个都值得单独调一遍。参数常见默认值头盔场景建议原因epochs100150200小目标数据收敛慢100 轮勉强够用200 轮才能看到稳定峰值imgsz640640 或 1024头盔只有 2030 像素1024 能明显提升小目标召回但显存和推理耗时翻倍batch-size16816显存不够就降到 8太小会导致 BN 统计不稳定lr00.01SGD 用 0.01AdamW 用 0.001学习率过大是后期 loss 震荡的主要原因mosaic1.0前 80% 保持 1.0最后 20% 调 0后期关闭 mosaic 让模型在小目标上精调nc按数据集2类别 id 越界会导致训练直接报错epochs 和 imgsz 是效果上限的决定因素。我做过对比同一份数据集640 分辨率训练 100 轮的模型mAP0.5 在 0.89 左右改成 1024 分辨率训练 150 轮后mAP 能上到 0.93但推理速度从 15ms 涨到 35ms在路口 25 路视频流的场景下这个开销必须提前评估。lr0 的坑在于多数源码默认值是按 COCO 数据集标定的COCO 目标大、样本多学习率可以偏高。头盔数据集通常只有几千张图0.01 的 lr0 很容易在前几个 epoch 就把 loss 打飞。我一般先用 0.001 跑 20 轮看 loss 曲线如果下降平稳再逐步调高。4.3 断点续训与日志监控训练里的后悔药训练中途断电、显存溢出、调参后想回退这些情况都有后悔药YOLOv9 训练时会在 runs/train 目录下同时保存 last.pt 和 best.pt。last.pt 是每轮结束的最新权重best.pt 是验证集 mAP 最高的权重。断点续训用 last.pt 接上即可python train.py --resume runs/train/exp/weights/last.ptresume 会读取上次训练的 epoch、学习率、优化器状态和随机种子直接接续而不是重新开始。这里有个细节如果你用--resume的同时又传了--data或--epochs多数实现会以权重里记录的配置为准命令行参数不生效。所以改了配置想重新训练时要开一个新的输出目录不要和旧训练混在一起。日志监控方面训练过程中每 10 个 epoch 会输出一组验证集指标重点看三个数Class Loss是否持续下降、mAP0.5是否稳定上升、mAP0.5:0.95和 mAP0.5 的差距是否过大。如果两个 mAP 差距超过 0.15说明模型的定位精度不够问题多半在小目标上优先调整 imgsz 而不是继续加大 epoch。用 TensorBoard 看曲线更直观tensorboard --logdir runs/train看训练时 loss 曲线的形状比看数值更重要正常的曲线是前 30 轮快速下降、然后进入缓慢爬坡期如果 loss 曲线上存在明显尖峰说明某几个 batch 里出现了异常标注或过强的数据增强优先回查那几张图的标注。5. 头盔检测必踩的五个坑现象、原因、解决忽略那些顺利跑通的人这几条基本每个头盔检测项目都会碰见。5.1 类别 id 不连续训练直接报 index 越界现象训练启动后不久就报IndexError: index 2 is out of bounds for axis 0 with size 2或者某个类别的 loss 始终不下降。原因标注文件里的类别 id 不是从 0 开始的连续整数比如有人把类别标成 0 和 2但 yaml 里 nc2数组只开了两个位置id2 直接越界。另一种情况是转换脚本里 CLASSES 顺序和 yaml 不一致导致 helmet 和 head 互换了。解决写一个扫描脚本遍历 labels 目录下所有 txt统计出现过的全部类别 id并检查最大值是否小于 nc。如果发现越界把 id 重新映射成 0 和 1。这个脚本必须在训练前跑最好作为 CI 检查固化下来。5.2 验证集里混了测试集mAP 虚高一倍现象训练日志里 mAP0.5 到了 0.95测试脚本在本地视频上跑也看着不错但部署到新路口后效果急剧下滑漏检率接近 50%。原因数据划分时把同一批视频的连续帧同时分进了训练集和验证集。同一辆电动车在相邻帧里高度相似验证集相当于「开卷考试」曲线自然漂亮。而新路口的场景、机位、光照都不一样立刻露馅。解决按时间片而不是按帧划分数据集。白天采集的视频拿出 30% 做验证傍晚采集的视频全部进验证集夜间再单独抽一批。验证集必须覆盖和实际部署一致的场景分布做不到这一点前面所有曲线都没有参考意义。5.3 白色头盔在逆光下全漏检现象白天正光场景 mAP 很高下午四点到六点逆光时段白色头盔的召回率断崖式下跌有时整个画面一个头盔都框不出来。原因逆光下白色头盔过曝头盔轮廓和天空融为一体模型学到的纹理信息全部消失。数据集里这种过曝样本本身就少网络只见过正常光照下的白色头盔遇到极端光照自然无法泛化。解决在标注时专门收集过曝、逆光的难例不需要很多每个时段 200 张就够。增强侧把随机亮度调整的范围从默认的 ±10% 扩大到 ±25%并加入随机对比度抖动让模型见过「亮到发白」的头盔。这个坑我一开始不信直到连续两个项目在春秋季的下午翻车才老老实实去收集过曝样本。5.4 后座乘员被 NMS 合并掉一个框框住两个人现象前座和后座骑乘人员靠得很近时输出只有一个框且框住了两个人的头部置信度还不低。原因两个目标的 IoU 过大NMS 在默认阈值 0.45 下认为它们是同一个目标直接把置信度低的后座框抑制掉了。头盔检测场景里前后座重叠非常常见这个现象基本必现。解决把 NMS 的 IoU 阈值从 0.45 降到 0.35让网络保留更多重叠框。如果降阈值后假阳性变多再配合置信度阈值一起调NMS 阈值降conf 阈值提。另外在标注时特别注意后座乘员也要有独立框不要因为「看不清」就跳过一旦跳过模型就永远学不会后座目标。5.5 epoch 80 后 loss 震荡精度不升反降现象前 80 轮 mAP 稳步上升之后 loss 开始锯齿状震荡validate 的 mAP 卡在某个值上不再变化甚至轻微下降。原因三个因素叠加——学习率太大导致后期无法收敛mosaic 增强还在产生大量过小目标干扰精调batch size 太小导致 BN 统计波动。这三个问题常常同时出现单修一个效果都不明显。解决训练策略改为 cosine 学习率 warmup 3 个 epoch在总 epoch 数的 80% 处把 mosaic 调成 0同时把学习率降到初始值的 10% 做最后精调。如果显存允许batch size 提到 16 以上。这套组合基本能解决 90% 的后期震荡问题。6. 评估曲线怎么读把训练好的模型推进到真实场景6.1 PR 曲线和 F1 曲线先看形状再看数值训练完成后runs 目录下会生成 PR_curve.png 和 F1_curve.png。PR 曲线横轴是 Recall纵轴是 Precision曲线下面积就是 AP。数值高不代表没问题要看形状如果曲线在 Recall 超过 0.8 之后 Precision 突然断崖下跌说明模型存在大量低置信度误检部署时要把置信度阈值调高如果曲线整体偏右且平滑说明模型在不同阈值下都比较稳定可以直接参考 F1 曲线上的最高点来确定部署阈值。6.2 用评估曲线反向定位数据短板头盔检测项目里把 head 类和 helmet 类的 PR 曲线放在一起对比数据问题就现形了。helmet 类曲线比 head 类差很多说明正样本里的形态多样性不足去补充不同角度、不同颜色的头盔样本head 类曲线差说明未佩戴样本太少。再配合混淆矩阵如果 helmet 被大量误判成 head说明标注时两类框的边界不清常见是头盔框里包含了未戴盔的下巴区域。6.3 转 ONNX 做实时推理验证训练好的模型才算真正交付验证曲线只是第一步最终要交付的是能在摄像头视频流上实时推理的模型。把 PyTorch 权重转成 ONNXpython export.py --weights weights/best.pt --include onnx --opset 12导出后用 onnxruntime 加载推理速度和显存占用都会比 PyTorch 原版更稳定。注意推理时的 conf 阈值要参考 F1 曲线的峰值位置而不是默认的 0.25。我现在的习惯是每训练一版先跑评估脚本记录 mAP再挑一段真实路口视频逐帧看漏检把每个漏检帧的置信度和场景记录下来比盯着曲线推测问题直接得多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进