☰
头盔检测数据集处理与YOLOv5训练避坑指南
2026/10/5 8:09:53 网站建设 项目流程

简介:面向YOLOv5头盔目标检测学习者和安防场景开发者,这份数据集提供可直接用于模型训练的头盔佩戴图像与XML标注文件,省去自行采集、清洗和标注的繁琐环节。压缩包共160个文件,由80张JPG图片与80个XML文件一一对应组成,XML记录检测目标的位置与类别信息,可转换为YOLOv5训练所需的格式;整体体积仅2.86MB,轻量紧凑,便于快速下载、解压和开展实验。当前已有482人学习下载,适合入门实践、算法对比以及安全帽佩戴检测场景的初版模型搭建。对于刚接触目标检测的学生,可使用该数据集熟悉YOLOv5从数据准备、配置训练到结果验证的完整流程;对于需要快速迭代的开发者,也能作为小型样例集验证脚本与参数设置。图像覆盖不同拍摄角度和背景,有助于扩充样本多样性;目录结构简单,下载后按标准方式组织即可完成训练前准备,为后续调优检测精度提供稳定的数据基础。

1. 头盔检测数据集:从zip到可训练模型的第一道关卡

做工地安全帽检测的同行应该都有过这种经历:拿到一个名为“用于yolo5头盔目标检测的数据集.zip”的压缩包,解压一看,里面既有VOC格式的xml标注,又有几个不知道干嘛的txt文件,图片尺寸五花八门,摄像头角度也乱七八糟。先别急着把数据丢进训练脚本——目标检测项目里,数据集的质量直接决定模型上限,代码反而是次要的。这个zip包能否真正变成可用的YOLOv5训练集,取决于你愿不愿意花半小时做数据体检、格式统一和样本划分。这篇笔记就是沿着这条线,把这个过程完整拆开。

2. 认识这份头盔数据集的内部结构:目录、标注文件与类别平衡

2.1 解压后的标准布局:images与labels是怎么对应的

拿到zip后第一件事不是浏览图片,而是看目录结构。常见做法是数据集落在两种布局里:一种是Pascal VOC风格,JPEGImages目录放jpg图片,Annotations目录放xml标注;另一种是YOLO风格,images和labels两个兄弟目录,每个图片对应一个同名txt标注。我见过不少二手数据集,压缩包内部结构是两种混着来的,甚至还有一层嵌套目录,第一眼会看晕。

先把目录树打出来:

unzip helmet_dataset.zip -d helmet_data cd helmet_data find . -type f | head -50

我的习惯是unzip时指定解压目标目录而不是直接解压到当前文件夹,避免散落一地的文件污染工作区。如果zip包是分卷压缩的,在Windows下用Bandizip或7-Zip合并解压;在Linux下则需要先合并分卷才能解压。如果解压过程反复报CRC错误,不要用unzip -o强行跳过错——这意味着文件在传输过程中损坏了,后面训练时标注缺失、图片花屏的坑都从这里来。

目录梳清后,最重要的动作是确认图片和标注是否一一对应。统计一下图片数量和标注数量:

ls images | wc -l ls labels | wc -l

这两个数字理论上应该完全相等(排除少数无目标图片)。如果labels比images少十几张,通常是标注遗漏;如果labels更多,要小心是不是某个标注文件格式损坏,系统自动生成了空文件。缺标注的图片不能直接删——你可以把这些图片留作“无目标负样本”,但它们不能进入带标注的数据集参与损失计算。

2.2 看标注文本:一个txt文件里的五个数字是什么意思

YOLOv5的txt标注文件每一行代表一个目标框,格式是class x_center y_center width height,注意这里的坐标是归一化的,且width和height是相对整张图片尺寸的比例,不是像素值。我经常看到新手把这个数值直接当像素用,画出来的框全是歪的。

打开一个标注文件看看:

cat labels/helmet_000123.txt

典型内容像这样:

0 0.511719 0.312500 0.156250 0.115625 1 0.602734 0.345833 0.128906 0.104167

第一列是类别ID。head类的ID是0,head_with_helmet(带安全帽的人头)是1,但这份数据集的ID编号不一定如此,需要你确认。如果数据集的类别超过两个——比如混合了person(人体)和helmet(安全帽本体)——那么这个num_classes和你的YOLOv5模型配置文件必须严格对应,不一致会导致训练时索引越界报错。

这里有个实用技巧:把分类映射写进data.yaml时,务必和标注文件里的ID顺序保持一致。常见错误是训练脚本正常跑,但输出检测时类别标签错位——原本是helmet的框显示成了head。血泪经验:别靠肉眼记忆,直接查data.yaml里的names列表,逐一核对每个txt里的class值。

2.3 类别不全与大头小头失衡:先做数据体检

拿到数据集先别急训练,先看两个维度:类别覆盖和尺寸分布。因为头盔目标检测通常涉及“戴了”和“没戴”两种状态,实际业务场景还可能区分“戴了但戴得不规范(比如没系下颚带)”。但这种多分类在通用数据集中很少见,你手上的zip大概率只有两个类别。如果只有person类别,那这就不是正经的头盔检测数据集——你需要后退一步,去和发布者核对。

再看尺寸分布。检测小目标与检测大目标的模型行为完全不同。我用一个简单脚本统计目标宽度和高度:

import os label_dir = "helmet_data/labels" widths = [] heights = [] for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname), "r") as fp: for line in fp: parts = line.strip().split() if len(parts) != 5: continue w = float(parts[3]) h = float(parts[4]) widths.append(w) heights.append(h) print(f"共 {len(widths)} 个目标框") print(f"宽度: min={min(widths):.4f} max={max(widths):.4f} mean={sum(widths)/len(widths):.4f}") print(f"高度: min={min(heights):.matplotlib:.4f} max={max(heights):.4f} mean={sum(heights)/len(heights):.4f}")

在YOLOv5的anchor设置中,默认anchor适合中等偏大的目标;如果你统计后发现大量目标框的相对尺寸小于0.05,说明小目标很多,依赖默认设置会漏检。这种尺度过大的情况一般出现在无人机俯拍或高处固定的监控摄像机,和最常见的工人头部近景图差异很大——这是先决问题,不是调参问题。

3. 数据质量验证:可视化脚本与常见标注故障排查

3.1 把标注画回图片:用OpenCV快速检查框是否对得上目标

这是整个流程中价值最高的一步。统计数字再漂亮,都不如直接画框出来眼睛看一遍靠谱。这里写个脚本把YOLO格式的标注画回原图,便于判断数据里到底有什么妖魔鬼怪。

import cv2 import os img_dir = "helmet_data/images" label_dir = "helmet_data/labels" save_dir = "helmet_data/visualized" os.makedirs(save_dir, exist_ok=True) colors = {(0, 255, 0), (0, 0, 255)} # head=绿,helmet=红 for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue stem = os.path.splitext(fname)[0] img_path = os.path.join(img_dir, stem + ".jpg") if not os.path.exists(img_path): print(f"标注无图: {fname}") continue img = cv2.imread(img_path) h, w = img.shape[:2] with open(os.path.join(label_dir, fname), "r") as fp: for line in fp: parts = line.strip().split() if len(parts) != 5: continue cls = int(parts[0]) x_c, y_c, bw, bh = map(float, parts[1:]) x1 = int((x_c - bw / 2) * w) y1 = int((y_c - bh / 2) * h) x2 = int((x_c + bw / 2) * w) y2 = int((y_c + bh / 2) * h) color = colors.get(cls, (255, 0, 0)) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(cls), (x1, max(y1 - 5, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) save_path = os.path.join(save_dir, fname.replace(".txt", ".jpg")) cv2.imwrite(save_path, img) print(f"可视化结果已保存到 {save_dir}")

画框结果会发现几个非常典型的问题:

标签和类别画反了。有些数据集把head画成戴帽子的,把helmet画成不戴帽子的——两个框互相调转,人眼能看出来,模型学出来的特征完全混乱。

框一半在图片外面。好的标注应该框住整个目标头部,但有的标注意见只覆盖了头部的一半或四分之一。对于头盔检测来说,框的单位定义很重要——如果你希望模型“检测到戴帽子的人头”,那么框的单位应该是“人头”,任何依赖中心思想和宽高比例的框,单位错了检测结果就偏了。

对同一个人头,head和helmet框重叠。这类数据集的意图往往不是检测人头位置,而是检测“穿戴状态”——一个目标同时具备两个属性。此时标注者不该用两个类别框来注释同一目标,而应该用单框单类别:head是普通/未戴,helmet是戴了。如果数据结构是重叠框,训练时loss计算容易产生矛盾,模型收敛时会出现反复摇摆。

3.2 统计每个类别的目标尺寸分布:小目标占比决定要不要改anchor

画完框,回到尺寸分布的话题,把统计加一个类别维度,看一下两个类别的平均面积差异:

import os from collections import defaultdict label_dir = "helmet_data/labels" stats = defaultdict(lambda: {"count": 0, "areas": []}) for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname), "r") as fp: for line in fp: parts = line.strip().split() if len(parts) != 5: continue cls = int(parts[0]) w = float(parts[3]) h = float(parts[4]) stats[cls]["count"] += 1 stats[cls]["areas"].append(w * h) for cls_id, s in sorted(stats.items()): areas = s["areas"] mean_area = sum(areas) / len(areas) print(f"类别 {cls_id}: {s['count']} 个目标, 平均面积比例 {mean_area:.4f}")

如果两个类别目标面积差异较大,例如helmet类平均面积是head类的三倍,训练时模型会倾向于学习大目标的特征而忽略小目标,导致未戴安全帽的小头漏检。此时我会调整YOLOv5配置文件中的anchors,用小目标专用预设,或直接开启多尺度训练。

3.3 坏图与损坏zip的识别:md5与图片完整性校验

zip包无论是从网盘下载的还是同事拷贝的,都可能中途损坏。unzip能解压不代表所有图片都完整,有时解压过程中图片被截断但解压程序不报错——这类图片在训练时往往表现为OpenCV读取为None或像素值异常,导致dataloader崩溃。

校验图片完整性的方法,用OpenCV读取一遍,宽高均为0的直接标记出来:

python -c " import cv2, os bad = [] for f in os.listdir('helmet_data/images'): p = os.path.join('helmet_data/images', f) img = cv2.imread(p) if img is None: bad.append(f) print('损坏图片:', bad) "

损坏图片直接删掉或替换。这一步能省下训练中途因为ValueError: image is None而中断排查的大量时间。

4. 把数据集转成YOLOv5训练格式:VOC转YOLO脚本与参数说明

4.1 目录重组:把现有数据统一到dataset/images和dataset/labels

无论原数据是VOC风格还是其他风格,最终都要转成YOLOv5可识别的目录结构:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yaml

如果你拿到的zip包内部是Pascal VOC结构,这步就得手工写脚本转换坐标。如果你拿到的已经是YOLO格式txt,目录重组就简单很多,直接用bash搬文件。但有个细节常被忽略:图片扩展名问题。有的数据集里jpg、JPG、png混着出现,YOLOv5能自动适配部分格式,但labels的扩展名永远是txt。检查图片与标注是否重名时,不能光看文件名前缀,要连扩展名一起过滤掉。

4.2 编写VOC转YOLO脚本:归一化坐标计算

我见过几个比较大的盔检测数据集混用VOC标注,里面xml的格式最初是用来做语义分割的——不是说这类标签完全不能用,而是转换脚本得自己确认一下字段是否完整。

import xml.etree.ElementTree as ET import os voc_dir = "helmet_data/Annotations" # VOC xml路径 img_dir = "helmet_data/JPEGImages" # 原图路径 yolo_label_dir = "helmet_data/labels" os.makedirs(yolo_label_dir, exist_ok=True) class_map = {"head": 0, "helmet": 1} # 按你数据集的names顺序调整 def voc_to_yolo(xml_path, img_width, img_height): tree = ET.parse(xml_path) root = tree.getroot() boxes = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: continue cls_id = class_map[name] bndbox = obj.find("bndbox") x_min = float(bndbox.find("xmin").text) y_min = float(bndbox.find("ymin").text) x_max = float(bndbox.find("xmax").text) y_max = float(bndbox.find("ymax").text) x_c = (x_min + x_max) / 2 / img_width y_c = (y_min + y_max) / 2 / img_height w = (x_max - x_min) / img_width h = (y_max - y_min) / img_height boxes.append(f"{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}") return boxes for xml_name in os.listdir(voc_dir): if not xml_name.endswith(".xml"): continue stem = os.path.splitext(xml_name)[0] jpg_path = os.path.join(img_dir, stem + ".jpg") if not os.path.exists(jpg_path): print(f"缺少图片: {xml_name}") continue # 取图片尺寸——不能用xml里的size字段,有些xml的宽高是错的 import cv2 img = cv2.imread(jpg_path) if img is None: continue h_img, w_img = img.shape[:2] boxes = voc_to_yolo(os.path.join(voc_dir, xml_name), w_img, h_img) with open(os.path.join(yolo_label_dir, stem + ".txt"), "w") as fp: fp.write("\n".join(boxes))

这个脚本的核心在于取图片尺寸的方式——我坚持用OpenCV读一遍原图获取宽高,而不是从xml的<size>节点读取。原因是很多爬下来的数据集,xml里的宽高信息与图片本身不一致,用了错误尺寸做归一化,所有坐标和宽高比例就被系统性放缩了,模型训练后检测框全部偏移。

4.3 划分train/val:随机种子、比例与分布保持

数据划分是个看起来简单但很容易翻车的环节。我一般按8:2或9:1划分,但要考虑两个额外因素:一是确定随机种子,以便多次实验使用同一份验证集;二是保证val集覆盖到不同光照和环境下的图片,不能所有来自某个工地的照片都挤在val里。

import os import random random.seed(42) # 固定种子保证可复现 img_dir = "helmet_data/images" label_dir = "helmet_data/labels" base = "helmet_data/dataset" os.makedirs(f"{base}/images/train", exist_ok=True) os.makedirs(f"{base}/images/val", exist_ok=True) os.makedirs(f"{base}/labels/train", exist_ok=True) os.makedirs(f"{base}/labels/val", exist_ok=True) filenames = [f for f in os.listdir(img_dir) if f.endswith((".jpg", ".png"))] random.shuffle(filenames) val_count = int(len(filenames) * 0.2) for i, fname in enumerate(filenames): stem = os.path.splitext(fname)[0] label_file = os.path.join(label_dir, stem + ".txt") if not os.path.exists(label_file): print(f"标注缺失,跳过: {fname}") continue subset = "val" if i < val_count else "train" os.rename(os.path.join(img_dir, fname), os.path.join(base, f"images/{subset}/{fname}")) os.rename(label_file, os.path.join(base, f"labels/{subset}/{stem}.txt"))

注意我这里是直接rename而不是复制——省磁盘空间但不可逆,建议在zip解压后的副本上操作。验证集的比例如果不确定,可以先看数据集总量:少于300张图片的数据集,按大概9:1划分可能只有二三十张验证图,指标波动很大,此时宁可多分一些到val。类别不平衡问题无法通过划分解决,只能通过采样权重或数据增强手段来缓解。

5. 头盔检测的训练避坑:数据泄露、类别不平衡、漏检回退

5.1 现象:验证集loss很低但视频里漏检远处小头盔

这是头盔检测项目翻车率最高的一个现象。loss降得漂亮,mAP也好,但部署到现场监控上,距离稍远的小目标几乎全漏。

原因分析:数据集中“近景清晰大头照”占比过高,模型在训练时根本没有见过足够多的小目标正样本。加上如果训练时没有开多尺度,模型对小目标的响应天然弱。解决侧重点是回到第3.2节的尺寸分布统计,如果小目标占比较低,要么增补小目标样本,要么在训练时开启多尺度训练,并配合减小anchor的尺寸。

# hyp.yaml 中相关参数 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 0.0 translate: 0.1 scale: 0.5 fliplr: 0.5 mosaic: 1.0

scale参数控制缩放增强幅度,适当提高可以让模型看到更多尺寸变化;mosaic增强把四张图拼成一张训练样本,对小目标检测有明显帮助。但注意:mosaic开启后,标注框在拼接边界会被裁剪掉一部分,YOLOv5代码内部会做mask处理,但如果你自己改了数据加载逻辑,这里很容易出问题。

5.2 现象:训练几轮后出现大量“空标注”样本进入训练集

感觉上训练过程很顺利,但日志里偶发出现WARNING: NMS time cost或者albumentations相关的警告,查看训练集的标注文件,发现一些txt文件是0字节空文件。

原因是zip包在流传过程中部分标注文件损坏或丢失,而目录里文件名还在。YOLOv5对空标签的默认处理是跳过那张图,但如果跳过比例过高,实际参与训练的图片数远低于预设epoch数,模型欠拟合。

解决:写一个清理脚本,把“有图无标注”和“有标注无图”的文件都拎出来单独放,不要直接删除。人工确认确实没有目标后,再把图片归到一个background目录里作为负样本——这对于“没有佩戴安全帽”的检测尤其重要。因为“未戴”本身就是一种目标,负样本可能在业务上反而是最重要的正样本。

5.3 现象:类别不平衡导致精度虚高

假设数据集中“佩戴安全帽”的框有20000个,“未佩戴”的框只有1000个,模型会倾向于把所有目标都预测为“佩戴”。这么做的mAP可能仍然不低,因为大多数框确实属于majority类,但业务要抓的是“未戴”——这个类别的召回率惨不忍睹。

解决有两个方向:第一是类别重加权,即给少数类更高的loss权重,YOLOv5默认没有直接支持这个功能,需要改损失函数,或者用简单的过采样——在构建dataloader时重复读取少数类样本。第二是评估指标切换——不要只盯着mAP,看各类别的AP和混淆矩阵。我的习惯是训练完第一轮就把混淆矩阵打印出来,男性佩戴识别的错误大多来自“把背景当人头”或“把头盔背景当帽子”,看矩阵比看曲线直观得多。

5.4 现象:zip包反复解压失败或标注文件丢失

如果解压时没有报错,但解压后txt文件数量少了一截,很有可能是压缩包本身不完整,或者个别文件CRC校验失败被解压工具默认跳过了。另一个常见现象是标注文件用Excel打开过再保存,引号全部被吃掉,内容变成0,0.5,0.3,0.2,0.1这种以逗号分隔的格式。YOLOv5读取时按空格split,一读就出问题,标注直接变成乱码。

解决方式是用Python脚本统一清洗:把逗号替换为空格、去除首尾空白、丢弃非5列的行,并且统计每个类别出现的次数。清洗完成后重新跑一次可视化,确认分布无异常,再进训练流程。

提示:这类清洗操作是纯文本处理,不需要重写坐标。但如果发现坐标列数不对,大概率是网络传输丢行或Excel截断,这一部分只能重新获取原始数据或人工复核标注。

6. 数据增强与样本增补:提高头盔检测泛化能力的几个技巧

模型框架选好、数据集格式对齐之后,真正决定头盔检测泛化能力的是数据增强策略和样本增补方法。YOLOv5自带一批增强方式,我一般先用默认配置跑一轮baseline,再针对头盔数据的痛点追加两个增强:

第一是亮度与对比度扰动。工地场景光照变化剧烈,午后逆光、夜间低照度、雨天阴天都会出现。YOLOv5的hsv_h、hsv_s和hsv_v三个参数就是对颜色空间的扰动,头盔检测场景我会调高hsv_v(亮度)的强度,相当于买了一张夜间和逆光环境的“后悔药”,提前让模型见过极端亮度。

hsv_h: 0.02 hsv_s: 0.8 hsv_v: 0.6

第二个是复制粘贴增强(Copy-Paste augmentation)——把同一张图里的头盔粘贴到另一张图的背景中。这个策略对头盔检测特别合适,因为头盔作为刚性物体,形变小,粘贴后依然有物理合理性。但你需要保证粘贴的框不与已有目标重叠,YOLOv5官方源码中没有直接内置这个功能,我实现了一个粗糙版本:

import cv2 import numpy as np import random def copy_paste(img, boxes, paste_boxes, paste_imgs): h, w = img.shape[:2] for pb, pimg in zip(paste_boxes, paste_imgs): x_c, y_c, bw, bh = pb x1 = int((x_c - bw / 2) * w) y1 = int((y_c - bh / 2) * h) x2 = int((x_c + bw / 2) * w) y2 = int((y_c + bh / 2) * h) # 随机粘贴位置 new_x1 = random.randint(0, w - (x2 - x1)) new_y1 = random.randint(0, h - (y2 - y1)) img[new_y1:new_y1+(y2-y1), new_x1:new_x1+(x2-x1)] = pimg[y1:y2, x1:x2] # 新框写入boxes boxes.append([0, (new_x1 + x2 - x1) / 2 / w, (new_y1 + y2 - y1) / 2 / h, (x2 - x1) / w, (y2 - y1) / h]) return img, boxes

这个逻辑要跑在Mosaic增强之后,否则拼接图的尺寸坐标系会变,粘贴位置容易错位。

第三个技巧就不仅是增强,而是样本增补的范畴:如果训练集里“未戴安全帽”的样本太少,直接用爬虫去公开数据集找或自己拍。比起在私有数据里反复调轮次,增加几十张“未戴”的现场照片往往比任何超参调整都有效。我在一个项目里试过,额外补充40张未戴样本,类别AP值从0.42直接升到0.79。

最后验证一下自己做的工作是否到位:重新跑一遍可视化脚本,随机抽20张训练输出和20张验证输出肉眼确认框位是否正确;再跑一次类别分布统计,确认无类别为0或仅含个位数样本的异常情况。再开始训练。这个流程我每次换数据集都在走,虽然繁琐,但所有训练中的“玄学”问题,八成都可以在这里找到根因。希望这份笔记能让你少走同样的弯路。

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

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

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

立即咨询