ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于YOLOv9的道路电动车头盔检测:环境搭建、训练与工程避坑

基于YOLOv9的道路电动车头盔检测:环境搭建、训练与工程避坑

简介:面向智慧交通场景,这套基于YOLOv9的电动车骑行人员头盔佩戴检测系统,为计算机、人工智能等相关专业毕业生或开发者提供完整可运行的毕业设计/课程项目范例。包含Python源码、训练好的模型权重、评估曲线和详细运行教程,覆盖从环境配置、数据集整理、模型训练到测试推理的全流程。资源包共188个文件,大小约69.32MB,其中83个Python脚本负责训练、检测与后处理,30个yaml配置用于数据集与模型参数定义,pt权重文件为训练好的模型,jpg/png图片为测试样例与结果可视化,另有ipynb笔记、shell脚本等辅助材料。目前已有238人学习下载。除了代码和模型,还提供逻辑清晰的目录结构与评估曲线,便于理解YOLOv9在该任务上的调优过程,适合作为毕业设计、项目实训或算法研究的基础,可直接修改数据集与参数扩展到其他目标检测场景。

1. 道路电动车头盔检测:YOLOv9 工程包的价值不在权重,在链路

智慧交通这个方向上,道路电动车骑行人员头盔佩戴检测早就不是新命题,但每次真正落地都会在同一个地方翻车:不是算法认不出头盔,而是从源码、环境、模型到评估曲线,完整的链路很难一次性跑通。基于YOLOv9实现道路电动车骑行人员头盔佩戴检测系统的这个工程包,恰好把链路压成了“下载就能复现”的状态——带python源码、详细运行教程、训练好的模型和评估曲线,适合两类人:一类是把头盔识别当核心算法的项目开发者,另一类是想快速做路口试点、先看效果再决定投入的工程评估人员。它的真正价值不是某个权重有多准,而是源码能跑、曲线能看、踩过的坑有迹可循。接下来我按一套真实落地的习惯拆开讲:为什么选YOLOv9、怎么跑通推理、怎么补数据重训,以及最让人熬夜的那几个坑。

2. YOLOv9在头盔检测里的选型逻辑:换模型不是玄学

2.1 从YOLOv5/v8到v9:头盔这类小目标到底受益在哪

先看目标尺度。道路场景里,一个电动车主从头到盔在1080p画面中通常只占几十像素,640分辨率下头盔的边长经常只有20~40像素,属于标准的小目标。小目标检测最怕的不是模型“笨”,而是网络在深层下采样后,浅层的细节梯度已经丢了。YOLOv9的两个核心设计正好踩在这个痛点上。

第一个是PGI(Programmable Gradient Information,可编程梯度信息)。它的思路是通过辅助可逆分支保留完整的梯度路径,让浅层特征在训练时不被“信息瓶颈”吃掉。对小头盔这种靠边缘纹理和局部颜色判断的目标,浅层梯度的存活率直接决定召回率。第二个是GELAN(Generalized Efficient Layer Aggregation Network,通用高效层聚合网络),用更少的参数量换接近的精度,轻量版本下推理延迟和YOLOv8s基本持平。

我在同一批自己标注的头盔数据上做过对比,v9s比v8s的mAP50高2到4个点(具体数值因数据分布而异),而且提升主要集中在夜间和远距离样本上。白天顺光、头盔颜色单一的场景两者差距不大。所以选型建议很直接:如果你的数据里有大量小目标、遮挡、光线变化,YOLOv9的收益是看得见的;如果只是拿公开数据集跑分,v8未必差很多。量力而行,别把模型换代当万能药。

2.2 跑通系统需要哪几层环境,缺一不可

工程包给你了源码和权重,但环境仍然是第一道坎。我习惯按下面这张表分层核对,哪一层缺了,后面都会以奇怪的方式报错。

环境层推荐配置用途缺了会怎样
Python3.9依赖管理、脚本运行部分包不兼容3.11+
CUDA11.8 + 对应驱动GPU加速训练慢到没法用
PyTorch2.0.x(cu118版)推理与训练的运行时版本错乱会报算子缺失
GPU显存训练建议8G以上加载模型与batch显存不足直接OOM
requirements.txt工程包自带一次性装齐依赖缺opencv会闪退

这里有个容易忽略的点:PyTorch版本和CUDA版本必须匹配,哪怕CUDA驱动装的是12.x,只要PyTorch编译时对着11.8,torch.cuda就能正常工作。另外没有NVIDIA显卡也能跑推理,只是慢,CPU单帧640分辨率大概2秒到5秒,做个离线分析可以接受。

2.3 用Anaconda搭建YOLOv9环境:命令与验证

在命令行里按顺序执行,这是我把环境装烂了以后固定下来的顺序,每一步都能回滚:

conda create -n yolov9 python=3.9 -y conda activate yolov9 python -m pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 python -m pip install -r requirements.txt python -c "import torch;print(torch.__version__, torch.cuda.is_available())"

逻辑说明:先建独立conda环境,避免污染系统Python;第二步装PyTorch时指定cu118后缀,这决定了后面能不能调用GPU;requirements.txt里的opencv、pandas、matplotlib等由工程包带你装齐。最后一条验证命令必须打印出类似“2.0.1+cu118 True”的结果,如果输出False,后面train.py大概率会静默跑CPU,训练一个模型耗时翻几倍。

参数说明里有三个关键点:torch版本不要盲目追新,2.1以后部分YOLOv9算子要重新编译;index-url参数也可以换成国内镜像源,只要能装上cu118版即可;conda环境名yolov9建议固定,因为后续所有命令行都要activate它。

3. 从源码到视频流:跑通推理要先看懂这四份文件

3.1 源码结构:四个文件各干什么,别上来就改代码

拿到带运行教程的工程包后,第一件事不是看模型,而是建索引。一套YOLOv9头盔工程的标准结构是四类东西,我按职责列在下面:

文件/目录职责你最终会操作它
detect.py推理,读图/视频/摄像头改weights和source
train.py训练,加载数据与参数改data和epochs
val.py评估,产出mAP与曲线改data和weights
tools/标注转换、数据划分、可视化跑转换脚本

这个结构几乎是YOLO生态的公共约定,存在的原因很简单:训练出模型后要用detect.py验证,val.py生成评估曲线,数据不对时进tools修改,四个环节闭环。我拿到任何YOLO工程都会先跑一遍detect.py确认流程通了,再动目录结构——很多人一上来就改路径,最后连官方demo都跑不出来的原因基本都是把某个相对目录拆坏了。

3.2 用训练好的模型跑通单张图片:最小推理命令

工程包里“训练好的模型”一般指best.pt,在runs/train某个目录下。先跑单张图,用最小命令:

python detect.py --weights best.pt --source ./test_images/01.jpg --conf-thres 0.35 --iou-thres 0.45 --imgsz 640 --save-txt --save-conf --project runs/detect --name helmet

参数说明:--weights指定加载的权重;--source可以是单张图或一个目录,目录会被遍历;--conf-thres置信度阈值,0.35是我做头盔检测的常用起点,头盔小、遮挡多,阈值提到0.5以上会明显漏检;--save-txt会在输出目录生成YOLO格式的txt标签,--save-conf让txt里带上置信度,后面转统计记录会用到。

跑完后在runs/detect/helmet下看结果图,重点检查两类错误:一是漏检,骑手头上没有框;二是误检,汽车前挡玻璃、白色栏杆被画上头盔框。如果漏检多,先把imgsz提到960;如果误检多,提conf-thres到0.45。这就是推理阶段唯一的两个调节旋钮。

3.3 本地视频和摄像头实时检测:在视频流上补帧率逻辑

视频源和图片源本质是同一条命令,只是source换一下:

python detect.py --weights best.pt --source ./videos/crossing.mp4 --conf-thres 0.35 --imgsz 640 python detect.py --weights best.pt --source 0 --conf-thres 0.35 --imgsz 640

第二条命令里的0是本地摄像头索引,接RTSP网络摄像头时写rtsp://user:pass@ip:554/Streaming/Channels/101这种完整地址。这里容易忽略的坑在性能:GPU上640分辨率视频推理大约每秒20到40帧,CPU只有几帧,看着会明显卡顿。我的习惯是引入跳帧策略,每3帧检测一次,中间两帧直接复用上一次的检测结果,既保住实时性又不漏掉慢速过路口的骑手。这在工程上叫“状态保持”,不是偷懒,是算力不满时的标准做法。

3.4 结果落地:把边框标签变成违规记录

detect.py输出的labels目录里是txt文件,每行格式为class x_center y_center width height conf。要让结果可用,得把它转成业务表。假设data.yaml里helmet: 0, head: 1,那么head类就是未佩戴头盔。写一个批量转换脚本:

import os, csv from datetime import datetime def make_record(txt_dir, output="violation.csv"): rows = [] for txt in os.listdir(txt_dir): if not txt.endswith(".txt"): continue img_name = txt.replace(".txt", ".jpg") with open(os.path.join(txt_dir, txt), encoding="utf-8") as f: for line in f: parts = line.strip().split() if not parts: continue cls_id, conf = int(parts[0]), float(parts[5]) if cls_id == 1 and conf >= 0.4: rows.append([img_name, "未戴头盔", round(conf, 3), datetime.now().isoformat(timespec="seconds")]) with open(output, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["图片", "类型", "置信度", "时间"]) writer.writerows(rows) make_record("runs/detect/helmet/labels")

逻辑说明:遍历推理输出的每个txt文件,选出类别ID为1且置信度达标的行写入CSV。参数说明里有个坑:YOLO txt里的x_c、y_c、w、h都是相对值,如果要画框或算像素坐标,必须乘上原图的宽高,但做统计记录只保留置信度和类别就够了,不必转换坐标。

4. 训练自己的头盔数据集:标注格式、划分脚本与数据增强

4.1 两类数据来源:公开数据和路口抽帧,先定类别再动手

训练自己的模型,数据集是第一生产力。头盔检测数据有两个常见来源:一是公开数据集,省时间但需要清洗,部分标注有错框或类别混用;二是自己从路口摄像机抽帧,最贴合目标场景,但要花时间标注。我一般混合用:公开集做预训练底子,自采帧做微调。

类别设计上,两分类helmet / head最常用,“head”表示没有头盔的头部。如果想减少“戴帽子”被误判成“戴头盔”,可以加第三类cap(非头盔帽子),让模型学会区分。单场景最小标注量建议800到1200张,正负样本比例控制在1:1到1:1.5,负样本太少是后面误报高发的根源。

4.2 把VOC的XML转成YOLO格式:转换脚本与四个边界坑

标注工具导出的常见格式是VOC的XML,而YOLO训练需要每张图配一个同名txt,每行class x_center y_center width height,坐标全部归一化。以下脚本能完成批量转换:

import os, glob import xml.etree.ElementTree as ET VOC_DIR = "voc_dataset" YOLO_DIR = "yolo_dataset" CLASS_MAP = {"helmet": 0, "head": 1} os.makedirs(YOLO_DIR, exist_ok=True) for xml_path in glob.glob(os.path.join(VOC_DIR, "annotations", "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASS_MAP: continue 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) x1 = max(0, min(img_w, x1)) y1 = max(0, min(img_h, y1)) x2 = max(0, min(img_w, x2)) y2 = max(0, min(img_h, y2)) x_c = ((x1 + x2) / 2) / img_w y_c = ((y1 + y2) / 2) / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{CLASS_MAP[name]} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}") out_name = os.path.basename(xml_path).replace(".xml", ".txt") out_path = os.path.join(YOLO_DIR, out_name) if lines: with open(out_path, "w") as f: f.write("\n".join(lines)) else: open(out_path, "w").close()

这里有四个我在血泪经验里总结的边界坑。第一,没有目标的图也要生成空txt,YOLO训练遇到“有图无标签”会跳过,但遇到“txt缺失”可能报错,空文件最安全。第二,坐标一定要clip到图像范围内,有些标注框会略微越界,不处理会导致归一化坐标出现大于1的值,训练时loss异常。第三,类别ID从0开始,和后面data.yaml的names顺序一一对应,写反了就是灾难。第四,txt文件名必须与图片名完全一致,jpg和png后缀不能混,否则训练时图对不上标签。

4.3 数据划分与训练启动:一份能换机器跑的data.yaml

转换好的数据集要按训练集/验证集划分,常见做法是8:2随机切分,并把目录整理成images/train、images/val、labels/train、labels/val四个目录。切分脚本各家写法都差不多,核心是保持图片和标签的文件名一致。

划分完成后编辑data.yaml:

path: train: dataset/images/train val: dataset/images/val nc: 2 names: ["helmet", "head"]

这里有个非常重要的细节:path字段保持为空,train和val写相对路径。很多项目习惯在data.yaml里写死绝对路径,换台机器就报“File not found”,这是网上提问频率最高的问题。留空让YOLO从运行目录解析,换机器零成本。

启动训练的命令:

python train.py --data data.yaml --weights yolov9-c.pt --img 640 --epochs 100 --batch-size 16 --device 0 --name helmet_train

参数说明:--weights yolov9-c.pt是预训练权重,从头训练会慢得多;--img 640训练分辨率,小目标多可以提到960,代价是显存和耗时增加;--batch-size 16在8G显存下比较稳,显存不足降到8;--device 0指定第一块GPU;--name会给输出目录改名。训练结束后runs/train/helmet_train下会生成best.pt和last.pt,前者是验证集上最优的权重,推理时默认选它。

5. 头盔检测训练避坑指南:5个最让我熬夜的坑

5.1 头盔目标太小导致漏检:先调imgsz而不是换模型

现象:测试图片效果不错,但视频里远处骑手几乎全漏,只有靠近镜头的人被检出。原因很简单:头盔在640分辨率下只有20像素左右,模型的小目标特征不够。解决路径我按顺序推荐:先推理时加--imgsz 960,看漏检是否下降;明显下降就训练时也把imgsz提到960,让训练和推理尺度一致;如果还是漏,就在检测流程前加一步区域放大——先检头部区域,把裁切块再送进分类器判断是否有头盔。这一步的优先级高于换模型,因为换v9x不会比调对尺度更有效。

5.2 挡风玻璃被判成头盔:向负样本要精度

现象:画面里汽车前挡玻璃、白色反光栏杆被画上头盔框。原因是训练集负样本不够,模型把“浅色高反光圆形区域”当成了正样本。解决分两步:第一步把误检截图收集起来,不标注、直接放进训练集图片目录,让它们以背景形式参与训练;第二步把--conf-thres从0.35提到0.45,把低置信度误检切掉。注意这里不要给误检目标标成“helmet”,那会教坏模型,负样本图片进目录不配txt即可。

5.3 夜间和逆光场景大面积失守:数据增强止血

现象:白天mAP有0.85,夜间场景掉到0.5以下,骑手和头盔几乎融进暗部。原因不是模型坏了,而是训练集的亮度分布和夜间真实场景差太远。我一般做三件事:训练时把hsv_h、hsv_s增强从默认值略微调高,模拟色彩偏移;去现场抽真实夜间帧加入训练集,注意是真实夜间帧而不是白天图压暗,夜间有路灯过曝和车灯反光,压暗模拟不了;推理前对画面做CLAHE自适应直方图均衡,提升暗部细节。三管齐下,夜间召回率通常能回升一半以上。

5.4 loss迟迟不降:大概率不是模型问题,是配置问题

现象:训练到第10个epoch,box_loss和cls_loss都不降,val曲线平得像一条直线。排查时别先怀疑代码,按顺序看三件事:学习率是否过大,默认lr=0.01配batch-size 16是稳定组合,小数据集反而要降到0.005;batch-size是否太小,2或4会让梯度噪声大,训练不稳定;数据集里是否有脏标签,比如txt坐标越界、类别ID对不上。最后一个问题最隐蔽,我踩过最大的坑是某张图的txt里混进了另一张图的坐标,loss直接起飞,清洗后恢复正常。建议训练前先写脚本扫一遍所有txt,检查坐标是否在0到1之间、是否有空文件,这五分钟能省一整晚。

5.5 模型部署到低算力设备跑不动:不是非得换硬件

现象:工控机CPU上跑640推理,单帧耗时2秒,没法做实时。很多人的第一反应是换显卡,但工程上可以按下面的顺序缓过来:第一,把模型导出为TensorRT或ONNX并量化到FP16或INT8,单帧能降到几十毫秒;第二,推理分辨率降到320或448,牺牲少量mAP换响应时间,对头盔这种大色块目标,精度损失有限;第三,换成YOLOv9的轻量级版本,参数量直接降一档。如果还不行,再加跳帧逻辑,每3帧检测一次,中间帧沿用上一帧结果。这套组合拳下来,低算力设备也能跑出可用的帧率。

6. 评估曲线怎么读、怎么用:别等部署完才后悔

读评估曲线是在吃后悔药之前先看一眼体检报告。训练后工程包里通常有PR曲线、F1曲线、混淆矩阵和results曲线,每一张对应一个决策点。

6.1 PR曲线和F1曲线:先定业务阈值,再上线

PR曲线的横轴是召回率,纵轴是精确率,曲线越靠近右上角说明模型越稳健。实际使用不是看mAP数字,而是在PR曲线上找精确率和召回率急剧变化的拐点,这个拐点附近的置信度阈值就是业务可用的起点。F1曲线已经把conf-thres和F1得分画在一起了,F1最高点对应的阈值等于“性价比阈值”。如果执法场景偏严,宁漏勿误,就在这个阈值上加0.05;如果先警告再人工确认,可以减0.05。

6.2 混淆矩阵告诉你漏检方向,而不是只看mAP

混淆矩阵是最容易被忽略的图。两分类场景下只看两个关键格子:真实“helmet”被预测成background,说明远距离小目标漏检,回去调imgsz或加近景样本;background被预测成“helmet”,说明色彩误报,回去加负样本。如果“helmet”和“head”互相混淆,问题在标注框边界画得不一致,而不是参数。先修标注,再调阈值,顺序不能反。

6.3 用验证集做一次“模拟执法”:上线前的最后一关

读曲线是宏观,最后一关要落到具体场景。我用val.py在验证集上按业务阈值跑一遍,再用一个简单脚本统计“帧级别”指标——这一帧里到底漏没漏:

python val.py --data data.yaml --weights best.pt --conf-thres 0.35 --iou-thres 0.45 --imgsz 640
import glob, os gt_dir = "dataset/labels/val" pred_dir = "runs/val/helmet/labels" def has_cls(txt_path, cls="1"): if not os.path.exists(txt_path): return False with open(txt_path) as f: return any(line.split()[0] == cls for line in f if line.strip()) tp = fp = fn = 0 for pred_txt in glob.glob(os.path.join(pred_dir, "*.txt")): base = os.path.basename(pred_txt) gt_pos = has_cls(os.path.join(gt_dir, base)) pred_pos = has_cls(pred_txt) if pred_pos and gt_pos: tp += 1 elif pred_pos and not gt_pos: fp += 1 elif not pred_pos and gt_pos: fn += 1 print(f"TP={tp} FP={fp} FN={fn}")

逻辑说明:这里算的是图像级“有没有未戴头盔的人”,不是框级mAP,但对业务最直接——漏掉一帧等于漏掉一次可能的违章记录。has_cls里cls="1"对应data.yaml中head类的ID,如果你用的是三分类,改成对应类别ID即可。

这也是我每个项目收尾时必做的一步:把PR曲线定阈值、混淆矩阵看方向、场景级模拟验证逐一过完,才敢把权重交给现场。读曲线不是给验收看的,是给自己兜底的。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

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