ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

头盔检测数据集处理与YOLOv5训练避坑指南

头盔检测数据集处理与YOLOv5训练避坑指南 简介面向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是0head_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_okTrue) 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_okTrue) 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_okTrue) os.makedirs(f{base}/images/val, exist_okTrue) os.makedirs(f{base}/labels/train, exist_okTrue) os.makedirs(f{base}/labels/val, exist_okTrue) 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, fimages/{subset}/{fname})) os.rename(label_file, os.path.join(base, flabels/{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.0scale参数控制缩放增强幅度适当提高可以让模型看到更多尺寸变化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或仅含个位数样本的异常情况。再开始训练。这个流程我每次换数据集都在走虽然繁琐但所有训练中的“玄学”问题八成都可以在这里找到根因。希望这份笔记能让你少走同样的弯路。本文还有配套的精品资源点击获取
返回列表