☰
电动车头盔检测数据集构建与YOLO训练实战指南
2026/10/12 3:00:10 网站建设 项目流程

简介:电动车安全帽(头盔)数据集面向目标检测与道路交通安全视觉任务,精选约1000张电动车骑行场景图片并全部完成标注,可用于训练头盔佩戴识别、骑乘人员检测等模型,适合深度学习初学者、算法工程师及智慧交通项目开发者直接使用。压缩包共1528个文件,764个png图像与764个xml标注文件一一对应,采用VOC格式标注,解压后即可接入YOLO、Faster R-CNN、SSD等主流检测框架,免去格式转换步骤;资源整体约390.97MB,规模适中,既能支撑快速原型验证,也可作为数据扩充的基础。目前已有2794人浏览学习,资源热度和可用性得到一定市场验证。借助这批数据,读者可省去手动采集与标注的耗时环节,直接开展模型训练、验证集评测、精度调优等流程;同时可基于现有标注信息,结合数据增强构建更大规模数据集,服务于电动车违章监测、路口安全预警、头盔佩戴自动巡检等落地场景。

1. 电动车安全帽(头盔)数据集:为什么公开数据跑得通、真实场景总翻车

做电动车安全帽(头盔)检测的视觉项目,门槛往往不在模型,而在数据集。我见过好几个团队在公开的头盔检测数据集上跑出接近满分的 mAP,一拿到真实路口场景就漏检频出:半盔被当成遮阳帽、夜间戴深色盔的骑手直接消失、雨天帽兜被误判成头盔。

这套数据集的本质,不是一堆图片和框,而是你对「戴没戴头盔」这个业务问题的定义——类别怎么定、头部怎么框、模糊目标怎么处理、正负样本怎么配比,全都会变成模型上线后的翻车点。把这条链路想清楚,比调任何训练参数都重要。

这篇文章写给要自己搭数据集、给路侧或园区安防类项目做头盔识别验证的开发者。接下来我会把从数据采集、类别定义、格式转换到训练验证和数据闭环的完整做法讲清楚,每个环节都给可直接抄作业的命令和参数。

2. 从零搭一套头盔检测数据集:类别定义与目录结构先定死

2.1 类别怎么定:两类头部框是起步阶段的最优解

头盔检测看起来是标准的目标检测问题,但类别定义直接决定模型能从数据里学到什么。项目里常见的方案有三种:单类别只管框头盔;两类别框住头部区域、标记戴盔与未戴盔;三类别同时标骑手、头盔和头。

实际部署里最常见的思路是两级检测——第一级先在画面里检出骑手或头部区域,第二级判断这个区域里有没有头盔。对应到数据侧,最稳的标注方式是只标头部框,类别分成 helmet(戴盔头部框)和 no_helmet(未戴盔头部框)两类。头部框比头盔框的语义更稳:头盔被车把、雨棚遮挡时只框头盔容易漏,而头部框至少能把目标锚住。

不建议直接给骑手全身框。全身图里既有头盔又有衣服,模型很容易学到「这人在骑车」,而不是「这人戴了盔」,换一个场景视角就失效。三类别方案能同时输出骑手位置、头盔位置和头位置,信息最全,但标注工作量接近翻倍,多人交叉场景还容易自相矛盾。我的建议是初期只做两类头部框,跑通流程之后再按业务需要补 person 类。

有一点要提前定死:类别顺序一旦确定就不能改,训练配置和推理配置里的 names 必须严格一一对应,中途改类别顺序等于重新标注。数据规模也影响类别选择,单类别头盔框方案两千张图就能看到像样的 mAP,但上线后误检多;两类头部框方案需要更多负样本才稳;三类别方案最好有一万张以上的图片预算。

2.2 采集策略:公开数据当种子,自采数据补场景

数据来源走两条腿。第一条是公开渠道:开源图库里的共享单车、校园通勤照片,加上安全宣传材料里的电动车图片,能快速凑出几千张,先把标注和训练流程跑通。这类图片的拍摄视角多为平视街拍,和路侧高位相机的俯视视角差异很大,所以公开数据只能当种子。第二条是自采:在真实路口、小区出入口、学校门口用手机横屏连拍或者录像抽帧,贴近监控视野。时间段覆盖早高峰、午后、夜间,天气覆盖晴天、阴天、小雨,角度覆盖平视、俯视、侧向。

数量上给一个可抄作业的底限:5000 张图起步,每张图平均 2 到 4 个目标;戴盔与未戴盔的正负样本尽量各占一半,否则分类损失会一边倒。场景记录要特别覆盖外卖骑手、后座乘客、带儿童座椅的家长——这些是真实项目里最多变的样本。自采时注意隐私合规,不拍车牌特写,公开渠道的图片要确认授权再进训练集。

采集负样本千万别省。现实场景里戴盔率高的路口,不戴盔的是少数,如果按真实比例采样,no_helmet 类可能只占 10%,模型学完会倾向于把所有头都判成戴盔。常见做法是刻意压低戴盔率采样比例,把 no_helmet 提到 40% 到 50%;上线后再用阈值把误报压回去。

2.3 目录结构与标注协议:训练前把规则写进文档

目录结构建议把 images 和 labels 平级分开,train、val、test 按同一批文件名一一对应,不要按子目录打散。

helmet_dataset/ ├── images/ │ ├── train/ # 约 4000 张 │ ├── val/ # 约 600 张 │ └── test/ # 约 400 张,按路口独立划分 ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── annotation_rule.md └── data.yaml

val 和 test 要按路口维度划分,而不是全局随机洗牌。同一个路口出现在训练集又出现在测试集,评估结果会虚高,因为模型记住了背景纹理。标注协议要在开工前写进 annotation_rule.md,下面几条是我项目里直接用过的规则,可以直接抄。

规则项建议值
头部框范围紧贴头盔或头皮外沿的矩形,不含脖子
头盔被遮挡遮挡超过一半不标;能看到一半以上照常标 helmet
无盔头部只要人眼能分辨就标 no_helmet,哪怕是远景
极小目标短边小于 12 像素或人眼难断,不标
帽兜、围巾一律不算头盔,标 no_helmet
后座乘客看得清头部就标,不做丢弃

这套协议最关键的一句话是「人眼能分辨就标」。它保证标注员和模型面对的是同一套判断标准,也是后面排查漏检的依据。标注工具选能导出 VOC 格式的桌面工具就行,支持标签热键和自动保存,适合批量操作;不建议用在线众标平台处理这种小目标密集图,坐标精度难以保证。所有图至少过一遍双标注抽检:两个人标同一批 30 张图,计算框的 IoU 一致率,低于 90% 就返工。这一步看着费时间,实际能省掉后面所有怀疑模型的排查时间。

3. 把标注转成 YOLO 能吃的格式:VOC 转 YOLO 的脚本与三个参数坑

3.1 三种标注格式的选型表

标注格式直接影响后续训练流程能不能顺利跑通。下面这张表是我做格式选型时的对照,头盔检测这类小目标密集任务,格式转换越简单越好。

格式坐标形式典型导出工具适合场景
VOC xml像素绝对值 xmin/ymin/xmax/ymax桌面标注工具默认导出人工标注、转换前的中间格式
YOLO txt归一化 x_center/y_center/w/h各开源检测框架偏好训练直接使用,单行一条框
COCO json绝对像素加区域掩码在线平台导出实例分割或统一数据管理

选型建议:从桌面标注工具直接导 VOC,再用脚本转 YOLO。不要手写 YOLO txt,坐标归一化容易写错;也不建议直接从在线平台导 YOLO 格式,不同平台的导出字段顺序偶尔不一致,这一条是真踩出来的。COCO json 适合本来就做实例分割的团队,头盔检测只用矩形框,用不到掩码,徒增转换复杂度。

3.2 VOC 转 YOLO 脚本与关键参数

下面的脚本只处理 helmet 和 no_helmet 两个类别,其他类别直接跳过。输入是 VOC 格式的 xml 目录,输出是同名 txt。

import os import xml.etree.ElementTree as ET CLASS_MAP = {"helmet": 0, "no_helmet": 1} def voc_to_yolo(xml_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in CLASS_MAP: continue # 只保留头盔相关类别 b = obj.find("bndbox") xmin = max(0, float(b.find("xmin").text)) ymin = max(0, float(b.find("ymin").text)) xmax = min(img_w, float(b.find("xmax").text)) ymax = min(img_h, float(b.find("ymax").text)) bw, bh = xmax - xmin, ymax - ymin if bw < 2 or bh < 2: continue # 丢弃小于2像素的退化框 x_c = (xmin + xmax) / 2 / img_w y_c = (ymin + ymax) / 2 / img_h lines.append(f"{CLASS_MAP[name]} {x_c:.6f} {y_c:.6f} {bw/img_w:.6f} {bh/img_h:.6f}") with open(os.path.join(out_dir, xml_name[:-4] + ".txt"), "w") as f: f.write("\n".join(lines)) voc_to_yolo("annotations", "labels/train")

逻辑说明:脚本先取图片宽高做归一化分母,然后遍历 xml 里的 object 节点,把类别名映射成数字,再把边界框转成中心点加宽高的格式。越界裁剪放在最前面,避免负值和超出图宽的值进入训练。

三个参数坑要特别留意。第一个是类名对不上:VOC 里如果写的是中文名或者 cap 这类别名,CLASS_MAP 匹配不上会把整张图丢成空文件,训练不报错,但 mAP 直接归零。第二个是空文件处理:一张图没有目标时 txt 是 0 字节,训练能接受空标签,但评估时空标签会拉低召回统计,保留空文件不要删。第三个是 basename 一致性:图片和标签必须严格同名,大小写、扩展名后缀都不能差,否则训练时找不到对应标签。

3.3 转换自检脚本:坐标、分布、可视化三步走

转换完不能直接开训,先跑一遍自检脚本。

def check_labels(label_dir, class_num=2): bad, cls_count = [], [0] * class_num for f in sorted(os.listdir(label_dir)): p = os.path.join(label_dir, f) with open(p) as fp: for line in fp: t = line.split() if len(t) != 5: bad.append((f, "字段数不对")) # 缺字段 continue c, x, y, w, h = map(float, t) cls_count[int(c)] += 1 if not (0 <= x <= 1 and 0 <= y <= 1): bad.append((f, "中心点越界")) if not (0 < w <= 1 and 0 < h <= 1): bad.append((f, "宽高越界")) if w * h < 0.0001: bad.append((f, "框面积过小")) return bad, cls_count bad, cls_count = check_labels("labels/train") print(bad, cls_count)

自检脚本检查四件事:每行是不是 5 个字段、中心点是不是落在 0 到 1 之间、宽高是否合理、框面积是否小到失去意义。输出的类别分布能直接看出正负样本比,如果 no_helmet 占比低于 20%,别急着训,先回上一章补数据。

我还会顺手统计框的宽度分布。头盔框宽度集中在整图宽度的 5% 到 15% 属于正常;如果出现大量超过 30% 的框,多半是标注时把骑手全身框进来了,这类错误会带偏小目标检测。自检通过后抽 20 张图把框叠加画出来目检一次,这一步虽然土,但能拦住绝大多数低级错误。

4. 用 YOLO 训一个头盔检测基线:最小配置与关键参数

4.1 data.yaml 先写对:names 顺序就是类别生死线

数据准备好之后,第一份要写的配置文件是 data.yaml。

path: /workspace/helmet_dataset train: images/train val: images/val test: images/test nc: 2 names: 0: helmet 1: no_helmet

path 建议用绝对路径,相对路径在换机器跑的时候容易找不到目录。train、val、test 填的是 images 下的子目录,不需要写 labels 目录,检测框架默认去同名路径下找 txt。names 的类别顺序必须和第 3 章转换脚本里的 CLASS_MAP 完全一致,这里错了,训练出来的模型类别含义和推理结果就全对不上。nc 写上更明确,虽然多数框架会按 names 自动推断,但显式写出来能少一次排查。

4.2 训练启动命令与参数实测建议

训练命令用 YOLO 的官方命令行,干净可控。

yolo detect train \ data=helmet.yaml \ model=yolo11n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=15 \ seed=0
参数建议值说明
modelyolo11n.pt轻量权重起步,显存够再换 s 版本
imgsz640速度与精度的默认平衡点,小目标多可试 960
batch16按显存调,8GB 卡跑不动就降到 8
epochs100配合 patience 15 早停,够了
lr00.01数据量小于五千张建议降到 0.005

头盔在画面里属于中小目标,imgsz 调到 960 通常能涨几个点 mAP,但推理速度会明显下降,路侧设备如果只有几十毫秒的预算,要谨慎。batch 取 2 的幂次,对显存利用率最好。lr0 玄学成分不小,数据量小的时候大学习率容易震荡,降一半更稳。数据增强参数先全部用默认值,等 baseline 跑出来再动,一上来就调 mosaic 只会让问题更难定位。

4.3 评估别只盯 mAP:每类 AP 和 recall 才是业务指标

训练结束后,评估脚本要按类别拆开看。

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") m = model.val(data="helmet.yaml", split="test") print(f"mAP@50: {m.box.map50:.3f}") print(f"mAP@50-95: {m.box.map:.3f}") for i, ap in enumerate(m.box.ap50): print(f"class {int(i)} AP50: {ap:.3f}")

评估时 split="test" 要求 data.yaml 里写了 test 项,否则框架会静默退回 val,结果会偏乐观。头盔检测业务上 mAP@50 是主指标,IoU 阈值压到 0.5 而不是 0.75,是因为密集场景下小目标的位置标注本身有抖动,阈值过严意义不大。

更关键的是各类别的 recall。漏报一个未戴盔的人,比误报一个戴盔的人代价高得多,所以训练完先看 no_helmet 的 recall 和 AP。打开混淆矩阵:如果 helmet 被误判成 no_helmet,说明正样本头盔不够典型,补全盔和半盔的正面样本;如果 no_helmet 被误判成 helmet,说明负样本难例不足,要去采集遮阳帽、帽兜这类近似目标,而不是调损失权重。

5. 头盔数据集训练最常踩的 5 个坑:现象、原因与排查方法

5.1 半盔被当成遮阳帽:类别混淆的根源在样本占比

现象:训练完的模型把戴遮阳帽、棒球帽的骑手误检成 helmet,置信度还很高。

原因:训练集里半盔样本占比低,模型学到的其实是「头顶有东西等于头盔」这个粗糙特征。半盔本身外形和棒球帽在 2D 图像上高度相似,只有侧面轮廓能区分,而侧面的半盔样本往往最少。

解决:补拍半盔的正面、侧面、背面样本;单独抽查半盔类别的混淆矩阵,确认错分来自哪些拍摄角度;如果业务上对半盔和全盔没有区分需求,可以都并入 helmet。如果反复压不住,就把半盔拆成一个独立类别训练,推理时再合并输出。

5.2 连帽衫帽兜算不算头盔:标注规则留白最坑

现象:冬季穿连帽衫、雨天穿雨衣的骑手,帽兜被模型高置信判成 helmet,截图给业务方看非常尴尬。

原因:标注规则里没定义帽兜归属,标注员凭感觉标,有人标 helmet 有人标 no_helmet,模型学到的是矛盾的标签分布。帽兜的轮廓和半盔确实接近,正面看尤其像。

解决:在标注规则里写死——只有硬质保护头盔算 helmet,连帽衫帽兜、围巾一律 no_helmet。如果业务场景明确要求拦截帽兜,就单独拆出 hood 类别做三分类。这些难样本按 10% 到 20% 的比例重复采样,让模型多看到几次,别被常规样本带偏。

5.3 工地安全帽混进电动车场景:合并还是拆类

现象:戴黄色工程安全帽的骑手被漏检,或者普通自行车头盔被识别成安全帽,两个类别来回串。

原因:工地安全帽和自行车头盔在颜色、帽檐形状上接近,而数据集偏重市区通勤场景,工地安全帽样本太少,模型没见过自然就归到相近类别里。

解决:先定业务口径。城市通勤场景一般把两类合并成一个 helmet,对外输出只关心戴没戴。工地周边巡检场景则拆出 hardhat 独立类别,专门采集白、黄、蓝三色安全帽的各角度样本,每个颜色都要有正面、侧面和俯视视角,否则拆类后依然互相干扰。

5.4 夜间逆光下深色头盔消失:这问题调参救不回来

现象:白天 mAP 能到 90 以上,夜间直接掉到 70 上下,戴深色头盔的骑手几乎全漏。

原因:头盔是塑料深色材质,夜间和背景融为一体;640 分辨率下小目标只有几十个像素,对比度再一低,特征就完全被淹没了。这类问题不是模型不收敛,而是数据里根本没有夜间样本。

解决:采集夜间红外补光或闪光灯补光样本;训练时打开 HSV 通道增强模拟低照度,把亮度通道的扰动范围加大;推理侧对亮度直方图做自适应拉伸,而不是手动调曝光。这里有个血泪经验:调阈值、加注意力模块都试过,最后还是老实回去补了夜间数据才见效。

5.5 漏标与错标:标注质量先于模型参数

现象:训练损失很低,但验证集漏检多,模型对某些图「视而不见」。

原因:标注阶段大量小目标被漏标,模型把带框区域当成负样本学;多人重叠时头部框边界乱画,模型学到的是歪框。漏标比错标的危害更大,因为它悄无声息地污染了正负样本边界。

解决:双标注抽检,一致性低于 90% 的批次返工;统计无框图占比,某个路口无框率异常就回原图核查,多半是漏标;更换标注工具后抽 20 张图对比坐标框,防止工具之间的坐标偏移。标注质量的坑不会在训练初期暴露,往往在业务方拿现场图来质疑的时候才炸开,所以前置抽检必须做。

6. 数据集上线前的验证与数据闭环:让误检样本反哺训练集

6.1 用一段真实路口视频做端到端验证,顺手压掉抖动误检

静态测试集只能证明模型认识这些图片,不能证明模型能用。我每次训练结束都会去部署点位附近拍 10 分钟左右的视频抽帧,逐帧跑推理,统计漏检、误检和置信度抖动。真实监控里最常见的现象是单帧闪烁——某一帧误检一个头盔,下一帧又消失,截图看不出问题,在视频里非常扎眼。

处理办法是 3 帧滑窗投票:同一目标连续两帧以上命中才保留输出。

from collections import Counter, deque class FrameVote: def __init__(self, window=3, min_hits=2): self.queue = deque(maxlen=window) self.min_hits = min_hits def update(self, frame_dets): # frame_dets: [{"id": 跨帧目标id, "conf": 置信度}, ...] self.queue.append(frame_dets) if len(self.queue) < self.queue.maxlen: return [] # 队列未满不做决策 hits = Counter() for dets in self.queue: for d in dets: hits[d["id"]] += 1 return [d for d in frame_dets if hits[d["id"]] >= self.min_hits]

window 越大延迟越高,路口场景 3 帧就够。跨帧的目标 id 用 bbox 中心点欧式距离小于阈值来匹配,不要用目标检测器自带的 id,小目标场景下它不稳定。

6.2 误检样本回流训练集:数据闭环比调参更值钱

数据闭环是这个数据集项目里最值得养成的习惯。每两周把现场新拍到的误检图挑 100 到 200 张,补标后加进训练集,用小学习率 lr0=0.001 续训 20 到 30 个 epoch,千万不要从头训。数据集会一点点向真实部署场景靠拢,误检率是能画出下降曲线的。

这个做法本质上是把现场当成标注数据的源,让数据集自己长大。我自己的教训是:有一回赶上线,漏了梅雨季的样本回流,结果连续几周在雨天路口误检率翻倍,最后乖乖回去补数据。从此养成每周抽 20 分钟翻误检图的习惯,把新规律写进标注规则文档,这比调任何训练参数都值,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询