☰
骑手头盔车牌YOLO数据集:标注、训练与部署全流程
2026/10/2 9:36:21 网站建设 项目流程

简介:这份数据集面向从事目标检测算法学习与智能交通应用开发的工程师和研究者,围绕骑手安全监管场景,提供骑手、头盔佩戴状态与车牌号码三类目标的标注图像,可用于训练和测试YOLO等实时检测模型,解决头盔违规识别与车辆号牌采集的实际问题。压缩包共253个文件,约74.67MB,其中121张jpg与3张png为原始图像,126个txt为对应标注文件,另含yaml配置文件与cache缓存文件,目录结构清晰,便于直接接入YOLO训练流程。目前已有43人学习下载。数据集覆盖骑手、戴头盔、不戴头盔、车牌号等类别,标注信息完整,适合作为课程设计、毕业设计或算法验证的实证素材,帮助读者快速搭建检测实验、评估模型在头盔违规与车牌识别任务上的表现,并理解数据标注与模型训练之间的衔接关系。

1. 骑手头盔车牌数据集:为什么“戴没戴”和“车牌号”要放在同一套 YOLO 里

外卖骑手违规抓拍这件事,单看某一帧画面其实很简单:一个人、一顶头盔、一块车牌。但真把它做成能跑的 YOLO 目标检测系统,麻烦立刻翻倍——头盔是“戴在头上”还是“挂在车把上”,车牌是“被骑手腿部遮挡”还是“逆光过曝”,这两类目标的尺度、遮挡模式、标注粒度完全不同。我见过太多团队先做一个纯头盔检测,上线后发现要联动车牌才能定责,于是回头重构数据集,标注白干一遍。

这套“骑手、戴头盔、不戴头盔、车牌号数据集”要解决的就是这个联动问题:在同一套标注体系里,同时给出骑手人体框、头盔状态框和车牌框,让一个 YOLO 模型一次前向就输出三类结果。它适合做智慧交通、园区骑行管理、外卖平台合规抽检的工程师,也适合刚接触目标检测、想找一个“类别少但场景真实”的数据集练手的人。核心难点不在模型,而在标注口径和类别定义——这两件事定错,后面训练再久都是玄学。

2. 数据集拆解与 YOLO 标注口径:三类目标怎么框才不返工

2.1 骑手、头盔、车牌三类的边界定义

先说结论:这套数据集里最容易被低估的是“骑手”这一类。很多人以为骑手框就是人体框,但实际场景里骑手坐在电动车上,下半身被车体挡住,如果按标准人体检测去框全身,会框进大量车体,导致模型学到“电动车=骑手”的错误关联。我一般建议骑手框只框到可见躯干加头部,不强行补全被遮挡的腿部,让框紧贴可见像素。

头盔类要拆成“戴头盔”和“不戴头盔”两个独立类别,而不是一个“头盔”类加一个状态属性。原因很直接:YOLO 的输出是类别加框,状态属性需要额外分支,而拆成两类后,模型在分类头就能直接区分,训练和部署都简单。代价是两类样本必须均衡,否则“不戴头盔”这种少数类会被压制。

车牌类的坑在于“车牌号”这三个字。如果标题里的“车牌号”指的是要识别字符,那这是检测加识别的两阶段任务,YOLO 只负责框出车牌位置,字符识别要另接 OCR。如果只是检测车牌区域,那 YOLO 单阶段就够。从数据集命名习惯看,绝大多数“车牌号数据集”实际提供的是车牌区域框,字符识别是下游的事。这一点必须在动手前确认,否则标注粒度会错。

类别名建议英文标签框选范围常见误标
骑手rider可见躯干加头部,不含被遮挡腿部框进整辆电动车
戴头盔helmet_on头盔可见轮廓,含帽檐把挂在车把的头盔标成戴
不戴头盔helmet_off骑手头部裸露区域漏标低头、侧头场景
车牌plate车牌完整矩形,含边框框进车牌支架或车身贴纸

2.2 从原始标注到 YOLO txt 的转换脚本

拿到手的标注大概率是 VOC XML 或 LabelMe JSON,YOLO 要的是每张图一个 txt,每行“类别索引 中心x 中心y 宽 高”,全部归一化到 0 到 1。下面这个脚本处理 VOC XML 转 YOLO txt,是我常用的版本,加了类别映射和越界裁剪。

import os import xml.etree.ElementTree as ET # 类别顺序必须和训练时的 data.yaml 完全一致 CLASS_MAP = { "rider": 0, "helmet_on": 1, "helmet_off": 2, "plate": 3, } def voc_to_yolo(xml_path, out_dir, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: continue # 未定义类别直接跳过,避免污染训练 cls_id = CLASS_MAP[name] bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 裁剪到图像范围内,防止标注越界导致归一化出负数 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) if xmax <= xmin or ymax <= ymin: continue # 宽高为0的脏框丢弃 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base + ".txt"), "w") as f: f.write("\n".join(lines))

逻辑说明:脚本先按 CLASS_MAP 过滤类别,保证只有四类进入训练;再做坐标裁剪,这一步能救回不少因为标注工具拖拽越界产生的脏框;最后归一化保留六位小数,精度足够且文件不会过大。参数上,img_w 和 img_h 必须和实际图片尺寸一致,如果你用了统一缩放,这里要填缩放后的尺寸,否则框会整体偏移。常见错误是直接拿 XML 里的 size 字段,但有些标注工具的 size 字段和真实图片对不上,稳妥做法是用 PIL 或 OpenCV 读一次真实尺寸。

2.3 数据集划分与 data.yaml 配置

划分比例我一般用 8:1:1,但这个数据集有个特殊点:不戴头盔样本天然少,如果随机划分,验证集里可能一个不戴头盔都没有,mAP 直接失真。正确做法是按类别分层抽样,保证每个 split 里四类都有。下面是对应的 data.yaml。

path: /data/rider_helmet_plate train: images/train val: images/val test: images/test nc: 4 names: 0: rider 1: helmet_on 2: helmet_off 3: plate

参数说明:path 是数据集根目录,train/val/test 写相对路径即可;nc 必须等于 names 的长度,写错会在训练启动时报维度不匹配;names 的顺序必须和转换脚本里的 CLASS_MAP 完全一致,顺序错一位,模型学到的就是错位标签,这种错误在混淆矩阵上表现为两类互相混淆,很难从 loss 上看出来。建议转换完先跑一遍校验脚本,统计每个类别的框数量,数量为 0 的类别要么补样本,要么从 names 里去掉。

3. YOLO 训练配置与参数调优:从预训练模型到收敛

3.1 环境搭建与预训练模型选择

环境这块,CUDA 版本和 PyTorch 版本对不上是最高频的翻车点。我一般用 conda 建独立环境,先装 PyTorch 再装 ultralytics,避免依赖冲突。预训练模型直接用官方 COCO 权重,不要用自己之前在小数据集上训过的权重,因为 COCO 里有人和车,迁移到骑手和车牌场景比随机初始化快得多。如果显存只有 8G,选 n 或 s 版本;显存 16G 以上可以上 m 或 l。

conda create -n yolo_rider python=3.10 -y conda activate yolo_rider pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python yolo checks # 确认环境和GPU识别正常

逻辑说明:先建环境再装包,避免污染 base 环境;torch 用官方 index 保证 CUDA 匹配;yolo checks 会打印 GPU 是否可用、版本号,这一步能提前发现驱动问题。参数上,cu118 对应 CUDA 11.8,如果你的驱动只支持到 11.7,就换成 cu117,不要硬上。

3.2 训练命令与关键参数含义

训练命令本身很短,但每个参数都影响收敛。下面这条是我在这个数据集上跑通的配置。

yolo detect train \ data=/data/rider_helmet_plate/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ mosaic=1.0 \ mixup=0.1 \ degrees=5.0 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ project=runs/rider \ name=exp1

参数说明:imgsz=640 是速度和精度的平衡点,车牌这种小目标如果低于 640 会丢细节;batch=16 在 8G 显存上跑 s 模型基本安全,爆显存就降到 8;lr0 是初始学习率,0.01 对迁移学习偏大,如果 loss 前几个 epoch 震荡明显,降到 0.005;patience=30 表示 30 轮没提升就早停,避免过拟合;mosaic=1.0 开启马赛克增强,对小目标和遮挡场景帮助很大,但最后 10 轮建议关掉,让模型适应真实分布;degrees=5.0 做小角度旋转增强,模拟骑手车身倾斜;hsv 三个参数做颜色扰动,应对不同光照下的车牌反光。

3.3 训练过程监控与指标解读

训练启动后重点看三个东西:box_loss、cls_loss 和 mAP50。box_loss 下降但 cls_loss 不降,通常是类别不均衡,不戴头盔样本太少;两个都降但 mAP 不涨,可能是验证集划分有问题。混淆矩阵是排查类别混淆的利器,如果 helmet_on 和 helmet_off 互相混淆严重,说明两类视觉差异不够,需要补充侧头、低头等难样本。另外注意,YOLO 的混淆矩阵在类别数少时偶尔出现总和不为 1 的情况,这是归一化方式导致的,不影响判断趋势,不用慌。

yolo detect val \ model=runs/rider/exp1/weights/best.pt \ data=/data/rider_helmet_plate/data.yaml \ conf=0.25 \ iou=0.5

逻辑说明:val 命令用 best.pt 在验证集上跑一遍,输出每类的 P、R、mAP。conf=0.25 是置信度阈值,车牌这种小目标可以降到 0.2 看召回;iou=0.5 是 NMS 的 IoU 阈值,骑手和头盔框重叠度高,这个值不要调太低,否则头盔框会被骑手框抑制掉。

4. 推理部署与车牌联动:把检测结果变成可用数据

4.1 单图与视频推理的最小命令

训练完的模型要落到实际抓拍流程里,先跑通单图推理确认效果,再上视频流。

from ultralytics import YOLO model = YOLO("runs/rider/exp1/weights/best.pt") results = model.predict( source="test.jpg", conf=0.25, iou=0.5, imgsz=640, save=True, ) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(model.names[cls_id], round(conf, 3), [round(v, 1) for v in xyxy])

逻辑说明:predict 返回的结果里,boxes 包含类别、置信度和坐标;model.names 是类别索引到名称的映射,直接打印可读性好。参数上,save=True 会把画框后的图存到 runs 目录,方便肉眼检查;如果做视频,source 换成视频路径,再加 stream=True 逐帧处理,避免一次性加载爆内存。

4.2 骑手与头盔、车牌的关联逻辑

检测出框只是第一步,业务要的是“这个骑手戴没戴头盔、车牌是多少”。关联逻辑我一般用空间包含关系:头盔框的中心点落在哪个骑手框内,就归属哪个骑手;车牌框和骑手框做 IoU 匹配,取 IoU 最大的骑手作为归属。如果一辆车上有多个骑手,按框面积排序,面积大的优先。这套逻辑不复杂,但边界情况多,比如两个骑手挨得很近,头盔框可能同时落在两个骑手框内,这时候用中心点距离最近的那个。

def associate(riders, helmets, plates): result = [] for r in riders: rx1, ry1, rx2, ry2 = r["xyxy"] item = {"rider": r, "helmet": None, "plate": None} for h in helmets: hx = (h["xyxy"][0] + h["xyxy"][2]) / 2 hy = (h["xyxy"][1] + h["xyxy"][3]) / 2 if rx1 <= hx <= rx2 and ry1 <= hy <= ry2: item["helmet"] = h break best_iou, best_p = 0, None for p in plates: iou = compute_iou(r["xyxy"], p["xyxy"]) if iou > best_iou: best_iou, best_p = iou, p item["plate"] = best_p result.append(item) return result

逻辑说明:先按中心点判断头盔归属,再按 IoU 判断车牌归属。参数上,头盔归属用中心点而不是 IoU,是因为头盔框远小于骑手框,IoU 天然很低;车牌归属用 IoU 是因为车牌和骑手框在空间上不一定包含,但位置接近。compute_iou 需要自己实现,标准交并比公式即可。

4.3 车牌字符识别的衔接方式

如果业务需要车牌号,检测出的车牌框要裁出来送 OCR。常见做法是裁图后做透视矫正,再送轻量 OCR 模型。这里不展开 OCR 训练,但要注意一点:YOLO 检测的车牌框如果偏大,OCR 会把边框字符也读进去,导致识别错误。建议裁图时向内收缩 5% 到 10%,去掉边框干扰。另外,逆光、污损车牌的识别率会明显下降,如果业务对车牌号准确率要求高,检测阶段就要把低质量车牌框过滤掉,而不是指望 OCR 硬扛。

5. 避坑与排查:这个数据集上最容易翻车的五件事

5.1 不戴头盔样本太少导致漏检

现象:训练完 mAP 看着还行,但实际推理时几乎检测不到不戴头盔的骑手。原因:不戴头盔在真实数据里本来就是少数,随机划分后训练集里更少,模型被多数类带偏。解决:先统计每类框数量,对不戴头盔做过采样或复制粘贴增强,把比例拉到至少 1:3;同时在 loss 里给少数类加权,或者用 focal loss 思路调整。

5.2 车牌框被骑手框抑制

现象:推理结果里车牌框大量消失,调低 conf 也出不来。原因:车牌框和骑手框重叠,NMS 阶段 IoU 超过阈值时车牌被当成冗余框抑制。解决:把 iou 阈值从 0.5 提到 0.6 到 0.7,或者对车牌类单独设置 NMS 阈值;更彻底的做法是训练时用类别独立的 NMS,YOLO 默认就是类内 NMS,如果还抑制,检查是不是车牌被标成了骑手类。

5.3 标注顺序错位导致类别全乱

现象:训练 loss 正常下降,但混淆矩阵里所有类别互相混淆,推理结果类别随机。原因:data.yaml 的 names 顺序和转换脚本的 CLASS_MAP 不一致,标签整体错位。解决:转换后跑校验脚本,随机抽 20 张图把 txt 画回图上,肉眼确认框和类别对应;names 顺序一旦确定,训练中途不要改。

5.4 图像尺寸不一致导致框偏移

现象:训练时 box_loss 居高不下,推理框整体偏移。原因:转换脚本用的 img_w、img_h 和实际图片尺寸不一致,归一化坐标算错。解决:转换前用 OpenCV 批量读一遍真实尺寸,写进脚本;如果数据集里图片尺寸混杂,先统一 resize 再标注,或者转换时按每张图的实际尺寸单独计算。

5.5 验证集划分泄漏导致指标虚高

现象:验证集 mAP 很高,上线后效果差一大截。原因:同一段视频的连续帧被分到了训练集和验证集,模型见过近似画面。解决:按视频源或时间段划分,而不是按帧随机划分;如果数据来自多个摄像头,按摄像头划分更稳妥。这个坑最隐蔽,因为指标好看,容易让人误以为模型很强。

6. 进阶技巧:用切片推理把车牌小目标召回拉上来

车牌在这个数据集里属于典型小目标,640 输入下可能只占几十个像素,召回率上不去。我常用的进阶手段是切片推理(SAHI 思路):把原图切成带重叠的小块,每块单独推理,再把结果映射回原图做 NMS 合并。这样车牌在小块里相对变大,召回明显提升,代价是推理耗时增加。实操上不用引入完整 SAHI 库,自己写切片逻辑就够。

import cv2 import numpy as np from ultralytics import YOLO model = YOLO("runs/rider/exp1/weights/best.pt") def sliced_infer(img, slice_size=640, overlap=0.2): h, w = img.shape[:2] step = int(slice_size * (1 - overlap)) all_boxes = [] for y in range(0, h, step): for x in range(0, w, step): patch = img[y:y+slice_size, x:x+slice_size] if patch.shape[0] < 32 or patch.shape[1] < 32: continue res = model.predict(patch, conf=0.2, verbose=False)[0] for box in res.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() all_boxes.append([ x1 + x, y1 + y, x2 + x, y2 + y, float(box.conf[0]), int(box.cls[0]) ]) return all_boxes

逻辑说明:切片步长由 slice_size 和 overlap 决定,overlap 取 0.2 能保证目标不被切断;每块推理后把坐标加回原图偏移;最后所有框送一次 NMS 合并。参数上,conf 可以比整图推理低一点,因为切片后目标更清晰,误检不会明显增加;slice_size 不建议小于 512,太小会丢失上下文,骑手和车牌的关联会断。

验证切片推理有没有效果,别只看 mAP,要单独统计车牌类的召回率。我一般会准备一批车牌密集的测试图,分别跑整图推理和切片推理,对比车牌召回。如果召回提升不到 5 个百分点,说明切片收益不大,可能是车牌本身标注质量有问题,先回去查标注。另外切片推理的耗时大约是整图的 3 到 5 倍,实时抓拍场景要评估算力,必要时只对含骑手的区域做切片,而不是全图切。

这套流程我踩过最深的坑是切片边界处的车牌被切成两半,两边各检出一半,NMS 又合并不了,最后输出两个残缺框。后来把 overlap 从 0.1 提到 0.2,并在 NMS 前加了一步框合并,才稳定下来。做小目标检测,宁可多花点推理时间,也别在切片参数上省。希望帮到你。

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

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

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

立即咨询