☰
YOLO安全帽手套检测数据集实战:格式转换、划分与训练避坑指南
2026/10/1 4:52:04 网站建设 项目流程

简介:面向YOLO系列目标检测学习者,兼顾安全施工与智能巡检场景,这套数据包以真实场景的安全帽佩戴、手套佩戴检测为主线,提供一千张高质量图片,并预置VOC、COCO、YOLO三种主流格式标签,可以直接导入训练框架,省去手工格式转换的麻烦。压缩包内共两千个文件,其中xml与txt标签文件占绝对多数,另有五个html图文教程、三个Python数据集划分脚本以及一个yaml模型配置,整体体积仅五十MB左右,轻量易用。已有五百四十四人学习下载,适合目标检测入门者、工业安全项目开发者用作基准数据或算法验证。下载后可以按教程逐步完成YOLO环境搭建,借助划分脚本自由切分训练集、验证集与测试集,并输出ImageSets下的索引文件,顺利开展YOLOv5、YOLOv8等模型训练与调参实验。

1. YOLO安全帽手套检测数据集:先分清它到底是什么,才能省下踩坑时间

做工地或厂区安全检测的工程师,拿到这种打包资料时,通常已经踩过一轮坑了:网上找数据集,下载下来只有图片,没有标签;或者有标签但只有一种格式,想用YOLO训练还得自己写转换脚本。而「YOLO安全帽手套检测数据集(含1000张图片)+对应voc、coco和yolo三种格式标签+划分脚本+训练教程.rar」这个标题,解决的就是这四件事:给足样本、给对齐好的多格式标签、给分类划分工具、给训练路径参考。

不过我得先说句实在话:1000张图对这个任务来说只是「能起步」的量级,距离生产环境还差得远。这篇笔记的目标,就是让你拿到这个包以后,能在一两个小时里完成从解压到跑通训练的全过程,同时把常见的坐标系错位、标签串位、过拟合这些坑提前排掉。适合第一次碰目标检测的从业者,也适合有基础但想快速搭一套安全帽检测基线的人。

2. 拆包后的三种标签:VOC、COCO、YOLO坐标系的换算与对齐

2.1 三种格式的存储方式与坐标系,先找出唯一的「真源」

解压rar之后,常见目录结构一般是这样的:图片放在一个文件夹,VOC格式的XML放在Annotations,COCO格式的JSON放在一个文件里,YOLO格式的TXT放在labels。文件名通常一一对应,比如0001.jpg、0001.xml、0001.txt,COCO则是一个大JSON包含所有图的标注。

真正动手之前,务必先把三种格式的坐标表示搞明白,这是整个流程里最容易翻车的地方:

格式载体坐标表示训练时直接用?
VOCXMLxmin, ymin, xmax, ymax,左上右下,绝对像素否,需转换
COCOJSONbbox = [x, y, width, height],左上角加宽高,绝对像素否,需转换
YOLOTXTclass, cx, cy, w, h,归一化到[0,1]是

YOLO训练时只认TXT,标签内容是「类别编号 + 归一化后的中心点x、中心点y、宽、高」。这个坐标系在转换时最容易错,因为中心点坐标是从左上角往右下算一半的宽和高,不是直接拿x1加上x2除以二就了事——分母里还有一个归一化系数。

我的习惯是:拿到包以后,先抽三到五张图,手动打开一个VOC XML和对应的YOLO TXT,对照一下同一目标转换出来的数值是否一致。如果发布者的三种格式是从同一个标注池导出的,那基本没问题;但如果不一致,你得决定以谁为准。

大多数情况下,我建议以VOC的XML作为「真源」。原因是VOC格式里包含<size>节点,直接记录了原图的宽和高,转YOLO时不需要猜;COCO的JSON里虽然也有width和height字段,但单独文件容易在手动编辑时被改坏。CLIP工具链里很多转换脚本也是从VOC起步的,路线最稳妥。

2.2 从VOC XML生成YOLO TXT:一套可靠的转换脚本

以VOC格式为真源,转换YOLO标签的核心公式是:

cx = (xmin + xmax) / 2 / width cy = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height

下面这段代码,假定你的VOC XML放在Annotations/,图片宽度高度可以从XML的size节点里读取:

import os import xml.etree.ElementTree as ET classes = ["helmet", "glove"] # 必须与后续 data.yaml 里的 names 顺序保持一致 def voc_convert(size, box): dw = 1.0 / size[0] dh = 1.0 / size[1] cx = (box[0] + box[2]) / 2.0 cy = (box[1] + box[3]) / 2.0 w = box[2] - box[0] h = box[3] - box[1] return cx * dw, cy * dh, w * dw, h * dh def voc2yolo(xml_path, out_path): root = ET.parse(xml_path).getroot() size_node = root.find("size") img_w = int(size_node.find("width").text) img_h = int(size_node.find("height").text) lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in classes: continue # 跳过未在 classes 列表里登记的物体 cls_id = classes.index(cls_name) box_node = obj.find("bndbox") x1 = float(box_node.find("xmin").text) y1 = float(box_node.find("ymin").text) x2 = float(box_node.find("xmax").text) y2 = float(box_node.find("ymax").text) cx, cy, w, h = voc_convert((img_w, img_h), (x1, y1, x2, y2)) # 归一化后顺手做一个边界钳制,防越界 cx = max(0.0, min(cx, 1.0)) cy = max(0.0, min(cy, 1.0)) w = max(0.0, min(w, 1.0)) h = max(0.0, min(h, 1.0)) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) # 批量处理 xml_dir = "Annotations" txt_dir = "labels_yolo" os.makedirs(txt_dir, exist_ok=True) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue stem = os.path.splitext(xml_name)[0] voc2yolo(os.path.join(xml_dir, xml_name), os.path.join(txt_dir, stem + ".txt"))

逻辑说明:voc_convert里用的是(xmin+xmax)/2这种算中心点的方法,而不是先减再除,是因为它在数学上等价但少一步变量传递,出bug的概率低。classes列表的顺序决定了类别编号,这个顺序必须与训练时的data.yaml完全一致,否则模型训练出来预测的类别全是对不上的。

参数说明:.6f是保留六位小数,YOLO训练要求不高,六位足够;cls_id从0开始,这一点很多第一次转格式的人会写错,从1开始会导致所有类别偏移一位。

2.3 转换后边界越过图像范围怎么办:clip的几何与取舍

如果你转换的框正好在图像边缘,算出来的cx或w可能略大于1,也可能小于0。YOLO官方的数据加载器遇到这种情况通常不会报错,而是直接裁剪,但更多情况下你的训练loss会在前几个epoch出现奇怪的抖动。

第一个选择是像上面代码一样在归一化数值上做clip,保证所有值都在0到1之间。这种处理简单,但会改变框的几何:比如一个框越界了10像素,归一化后clip会把中心点硬拉到边界上,框的宽度不变,视觉上这个框会比原来小一圈。

第二个选择是在像素坐标系先做clip,再重新归一化。也就先让x1 = max(0, x1)、x2 = min(img_w, x2),然后再走转换公式。这样做几何上更准确,但代码里多两步。我的建议:如果是包里自带的标签,一般不会有越界情况,直接skip这一层;如果你之后拿自己标的数据来训练,务必选第二种,因为标注软件手滑把框拉出画布是常态。

另外,如果你是从COCO格式直接转YOLO,注意bbox的表示是[x, y, width, height],和VOC的[xmin, ymin, xmax, ymax]是不同的。转换公式里是cx = x + w/2、cy = y + h/2,不要套用VOC的(x1+x2)/2逻辑。

3. 划分脚本不是random一下:同一张图不能既进训练又进验证

3.1 先检查样本边界:图层、标注、类别分布

划分脚本是数据集里的第一道闸门。很多人图省事,从图片文件名里直接取前80%做训练集,后20%做验证集,这是最危险的做法,因为图片文件在磁盘上的排列顺序往往带有采集时间或场景分组信息。如果前80%都是白天拍的,后20%全是黄昏拍的,训练出来的模型对黄昏场景几乎零泛化。

正确的做法是先统计类别分布。安全帽和手套这两类在工地图片里的出现频率差异可能很大:安全帽几乎每张都有,手套可能只在少数特写镜头里出现。如果你不按类别分层抽样,验证集里很可能一只手套都没有,训练出来的mAP看着还行,实际一测就露馅。

具体到这个小数据集,我建议先做三件事:第一,统计图片总数和标签总数是否一一匹配;第二,统计每一张图里有哪些类别;第三,粗略看一眼有没有空标签文件——1000张的包,混进去几张0KB的txt很正常,训练时YOLO会跳过空标签,但如果空标签恰好全分到验证集,会让验证集的评估样本数变少。

3.2 一份同步三类标签的train/val划分脚本

下面这个脚本不依赖sklearn,用纯Python的Counter做分层抽样,按类别组合分桶,再按比例抽取,避免同类图片全部挤到同一侧:

import os import random import shutil from collections import defaultdict src_img_dir = "images" src_label_dir = "labels_yolo" # 以yolo txt为划分参考 out_root = "dataset_split" train_img = os.path.join(out_root, "images", "train") val_img = os.path.join(out_root, "images", "val") train_lab = os.path.join(out_root, "labels", "train") val_lab = os.path.join(out_root, "labels", "val") os.makedirs(train_img, exist_ok=True) os.makedirs(val_img, exist_ok=True) os.makedirs(train_lab, exist_ok=True) os.makedirs(val_lab, exist_ok=True) random.seed(42) sample_list = [] bucket_map = defaultdict(list) for f in os.listdir(src_img_dir): stem = os.path.splitext(f)[0] txt_path = os.path.join(src_label_dir, stem + ".txt") if not os.path.exists(txt_path): continue with open(txt_path) as fp: lines = [line.strip().split() for line in fp if line.strip()] classes_in_img = tuple(sorted(set(line[0] for line in lines))) bucket_map[classes_in_img].append(stem) sample_list.append(stem) val_ratio = 0.2 val_stems = set() train_stems = [] for bucket_key, stems in bucket_map.items(): random.shuffle(stems) cut = int(len(stems) * val_ratio) # 每个桶至少留一个样本进训练集 if cut == len(stems): cut -= 1 val_stems.update(stems[:cut]) train_stems.extend(stems[cut:]) # 同步复制图片和txt标签 for stem in train_stems: img_name = stem + ".jpg" # 如果你的图片是png,改成 .png lab_name = stem + ".txt" if os.path.exists(os.path.join(src_img_dir, img_name)): shutil.copy(os.path.join(src_img_dir, img_name), train_img) shutil.copy(os.path.join(src_label_dir, lab_name), train_lab) for stem in val_stems: img_name = stem + ".jpg" lab_name = stem + ".txt" if os.path.exists(os.path.join(src_img_dir, img_name)): shutil.copy(os.path.join(src_img_dir, img_name), val_img) shutil.copy(os.path.join(src_label_dir, lab_name), val_lab) print(f"train: {len(train_stems)}, val: {len(val_stems)}")

逻辑说明:bucket_map以「类别组合」为桶,例如某张图同时有安全帽和手套,它的桶键是('glove', 'helmet'),只有安全帽的桶键是('helmet',)。每个桶内部随机打乱后,按同样的val_ratio抽出验证集,保证安全帽和手套在训练集、验证集里都保持接近的比例。

参数说明:val_ratio = 0.2表示验证集占百分之二十,1000张图就是约200张进验证集,这个比例对小数据集比较合适。random.seed(42)保证每次运行划分结果一致,方便复现。如果你的图片格式是png,上面代码里两处".jpg"都要改,否则复制时会跳过所有png图,只复制jpg图。

这里有一层更细的坑:如果数据集里某个类别组合的样本数特别少,比如只有3张「只有手套、没有安全帽」的图,那么划分后验证集里可能只有一张手套图,随机性太强。解决办法是把val_ratio改小到0.1,或者干脆把这组全放进训练集,另外从别的桶里人工挤出几张补进验证集。

3.3 划分完成后必须检查的3项:类别比例、空标签、文件名对齐

划分完别急着开训练,先跑三个快速检查。

第一,统计train和val各自的类别数量。可以用一条bash命令完成:

for split in train val; do echo "== $split ==" cat dataset_split/labels/$split/*.txt | awk '{print $1}' | sort | uniq -c done

正常情况是:两个split里类别0和类别1的数量比例接近,且都不为零。如果某个类别在验证集数量为0,你需要手动把几张对应的图从训练集挪过来。

第二,检查有没有0字节的空标签:

find dataset_split/labels -name "*.txt" -size 0 -print

如果找到空文件,原因通常是对应图片里确实没有目标,或者是标注时漏了。空标签文件本身不致命,YOLO训练时会跳过空图,但它会减少你的有效训练样本量。我的经验是统计一下数量,如果少于5个可以直接删掉对应图片,如果多就说明数据质量有问题,得回去看原始标注。

第三,检查图片和标签是否一一对应。常见错误是图片是001.jpg,标签却是001.txt,看起来没问题,但划分脚本里扫描的是图片目录,标签文件名的stem可能不同导致复制时漏掉。一个简单校验:

for d in train val; do img_count=$(ls dataset_split/images/$d/*.jpg 2>/dev/null | wc -l) lab_count=$(ls dataset_split/labels/$d/*.txt 2>/dev/null | wc -l) echo "$d: $img_count images, $lab_count labels" done

如果两边数量不一致,返回去检查源文件,不要带病训练。

4. 跑通YOLO训练的最小配置:data.yaml、命令和六个关键参数

4.1 data.yaml的写法:路径、类别顺序不要想当然

现在数据已经分成train和val两个目录,接下来写YOLO需要的数据配置文件。这里以YOLOv8为例,因为它是目前社区里做检测最常用的改进版本,既有预训练权重,命令行也直观:

# helmet_glove.yaml path: /home/user/helmet_glove_dataset # 数据集的绝对根目录 train: images/train val: images/val test: images/test # 如果没有test,删除这一行 names: 0: helmet 1: glove

有一个必须强调的点:names里的编号必须从0开始,且不能跳号。如果你写成:

names: 1: helmet 2: glove

YOLO的数据加载器会直接报错,或者类别索引错位导致预测结果乱套。另外,path建议写绝对路径。相对路径在你的终端工作目录不在数据集根目录时会各种找不到文件,这个坑我至少见过三次。

4.2 训练命令中的六个关键参数,照着调不翻车

配置写好后,训练命令可以这样起:

cd /home/user/helmet_glove_dataset yolo detect train \ data=helmet_glove.yaml \ model=yolov8s.pt \ epochs=100 \ batch=16 \ imgsz=640 \ cos_lr=True \ patience=20 \ project=runs \ name=helmet_glove_exp1

这里六个参数是重点:

参数建议值说明
modelyolov8s.pt用预训练权重做迁移学习,比从零训练收敛快得多
epochs1001000张图配100轮足够,配合patience提前停
batch16显存16GB左右的显卡够用;显存不足先减半,不要直接改imgsz
imgsz640安全帽目标不算特别小,640是速度和精度的平衡点
cos_lrTrue余弦退火学习率,小数据集上比固定阶梯下降更稳
patience20连续20轮验证损失不降就停,防止空跑

参数说明:yolov8s.pt是最小的可用预训练模型。有人喜欢一上来用yolov8m或yolov8l,但在1000张图这种数据量下,模型越大过拟合越快。你可以在第一轮跑yolov8s,跑完看验证集mAP,如果效果不错就不换;如果mAP不到0.8,再考虑提一级模型。

显存和batch的关系:batch=16在16GB显存上一般能跑640分辨率,但如果你的显卡只有8GB,把batch降到8,而不是把imgsz降到416——缩分辨率会直接影响小目标检测能力,安全帽在画面里占比不大,缩图像是保显存的下策。

4.3 训练中的损失曲线,哪些波动不用管、哪些要停

训练起来以后,YOLO会输出box_loss、cls_loss、dfl_loss三条曲线。很多人第一次跑时盯着train_loss看,训练损失从第20轮到第50轮一直在降,心里高兴,但验证集mAP纹丝不动,这是因为训练损失下降可能只是模型在背诵训练集,不是泛化。

正确看法是:val/box_loss和val/cls_loss才是决定什么时候停的指标。当val曲线开始掉头向上,而train曲线继续下降,这就是过拟合的信号。patience参数就是为此设计的,如果20轮内val损失没有创新低,训练会提前终止。

另外,如果你看到loss曲线在某个epoch突然跳高,然后下个epoch又恢复,这种孤立尖峰不用管,经常是batch里混了一张比较复杂的图导致的。但如果loss跳高后一路不回来,那就是学习率过热,需要调低初始lr,或者把cos_lr关闭回归固定lr,先排除变量。

5. 安全帽手套检测训练避坑:五条从失败里摸出来的经验

5.1 现象:验证集mAP很高,实拍却漏检测

这是目标检测项目里最经典的情况,验证集mAP达到0.85以上,但在工地上随便拍一张测试,安全帽漏了。

原因:验证集和训练集来自同一个数据包,图片角度、光线、拍摄设备都高度相似,模型记住了「这张图是安全帽」的分布,而不是「安全帽这个物体」的本质特征。再加上划分脚本如果没做严格分层,某几个场景的图可能全落在训练或验证里,让验证集指标虚高。

解决:第一,训练时不要关掉mosaic和随机仿射增强。YOLOv8默认开启了mosaic,但如果你的YOLO配置里被别人改过,务必确认mosaic=1.0;第二,在训练完后,挑几张包外的实拍图,放到一个单独的文件夹里跑一遍yolo predict,用肉眼判断漏检程度,而不是只看mAP。第三,如果漏检严重,去补充真实场景的图片,这比调任何参数都有效。

5.2 现象:标签类别串位,安全帽被识别成手套

训练和验证loss都很正常,但推理时安全性被预测成手套,或手套预测成安全帽,而且错误方向统一。

原因:十有八九是类别索引串了。VOC XML里的name可能是中文标签“安全帽”,你的classes列表里写的是helmet,脚本里因cls_name not in classes把这个框跳过了,所有框的索引整体偏移;又或者你用了第2.2节的脚本,但classes = ["glove", "helmet"]写反了顺序,导致类别0变成了手套。

解决:别用肉眼在编辑器里比对,直接写一条命令,从原始XML里提取真实类别列表:

grep -rh "<name>" Annotations | sort | uniq -c

然后把输出的类别名称,和data.yaml里names的顺序逐字核对。如果XML里是中文,先把XML里的中文映射成英文再进classes列表。这类问题是所有格式转换流程里最容易出的玄学问题,排查路径固定,一次通。

5.3 现象:损失先降后崩,训练在某个epoch直接NAN

训练到第30轮左右,loss突然变成nan,终端输出一片乱码,重新跑一遍结果一样。

原因:BN崩溃。当batch size较小时,BN层统计的均值和方差波动大,加上学习率略高,梯度过一段训练后爆炸,数值溢出。在小数据集上,很多人会把batch设成4甚至2来硬跑,这是最常见的导火索。

解决:先把初始学习率从默认的0.01降到0.001,再训练一轮。如果还崩,把batch从4提到8,或从8提到16,让BN统计更稳。还有一个办法:换用yolov8n.pt预训练模型,模型更小,梯度更温和。注意,NAN问题不是随机出现的,同一配置重复跑大概率复现,所以别赌运气,按参数顺序排查。

5.4 现象:1000张图训练到一半就过拟合

从第40轮开始,验证集mAP停止上涨,train_loss还在降,val_loss掉头向上。

原因:数据量少 + 模型容量够大,模型开始死记标签。这种情况在安全帽手套这种外观相对固定的目标上很常见,尤其是手套,背景一旦相似,模型会靠背景纹理做判断。

解决:先做减法,换yolov8n或yolov8s,不要一上来就用m或l;再把epochs从100砍到60,patience设成15,让训练在下坡之前停住。数据增强方面,可以加一点HSV变换和随机翻转,但不要上mixup,小数据集上mixup容易把安全帽的颜色带偏。如果你发现val_loss过了某个epoch才上升,就用回调保存那个epoch前的权重,YOLOv8的best.pt本身就是按val mAP保存的,这点比较省心。

5.5 现象:标签与图片在划分时配对错位

训练报错找不到某张图的标签,或者打开训练日志,发现images/train/foo.jpg对应的labels/train/foo.txt不存在。

原因:划分脚本扫描的是图片目录,但标签的命名后缀或存储目录不一样。最常见的有三种:一是图片是.jpg,脚本里写死成.png;二是源pack里YOLO标签的文件夹叫label而脚本里写的是labels;三是图片文件名带_结尾而标签文件没有。

解决:划分前先统一排查文件名。用bash跑一遍:

for f in images/*.jpg; do stem=$(basename "$f" .jpg) [ -f "labels_yolo/$stem.txt" ] || echo "missing label for $stem" done

把所有缺标签的文件名列出来,先处理再划分。这个问题最隐蔽,因为不是所有图都缺,只是少数几张,训练前面几百轮完全正常,直到踩到那张缺失样本才中断。

6. 从训练到可交付:混淆矩阵判读与预训练迁移的实操技巧

6.1 混淆矩阵的对角线为什么「总合不唯一」

训练结束,YOLOv8会输出混淆矩阵图。很多人发现矩阵对角线上的数值加起来不等于验证集图片数,怀疑自己数据有问题。其实,YOLO的混淆矩阵是按行归一化的,每一行表示该类实际样本被预测成各个类别的比例,加起来是100%;如果你按列再看一遍,列和行的归一化方向不同,总和自然不是唯一值。判读技巧是看「非对角线」的格子:安全帽被误检成手套的格子如果超过5%,说明两类在外观上有重叠,需要补拍区分度高的样本;如果某一行的background比例偏高,说明该类别在验证集里角度太刁钻,模型没见过。

6.2 用预训练权重分两阶段训练:冻结层与渐进解冻

1000张图直接全参数微调,速度快但容易过拟合。更稳的做法是两阶段训练。第一阶段冻结backbone,只训练head部分:

yolo detect train data=helmet_glove.yaml model=yolov8s.pt freeze=10 epochs=30 batch=16 imgsz=640

freeze=10表示冻结前10层,对yolov8s的结构来说大约等于把backbone的浅层和中层冻住,让模型先用已有特征提取能力去拟合安全帽和手套的分类头。第二阶段去掉freeze参数,用第一阶段的best.pt作为新权重,全量微调:

yolo detect train data=helmet_glove.yaml model=runs/helmet_glove_exp1/weights/best.pt epochs=40 batch=16 imgsz=640

这样做的逻辑是:先让输出层快速适应两类目标,防止随机初始化的head在前几个epoch产生大梯度把预训练特征冲坏;第二阶段再慢慢松开全网络,让深层特征往安全帽方向偏移。实际跑下来,比一次性全量训练mAP高2到3个点,而且更稳定。

6.3 从安全帽扩展到手套类别时,怎样低成本补数据

如果你还要继续在这个数据包上扩展新类别,建议先把现有1000张图的增强版本做出来:水平翻转、随机亮度抖动、随机旋转15度以内,混入训练集。增强图比例控制在原图的二分之一以内,太多会让模型对纹理过度敏感。要补真实样本的话,优先拍不同光照方向的安全帽和手套叠在一起的场景,这类组合样本在这个包里的占比往往不足,是模型失败的高发区。我的习惯是,每次训练前先按第3.3节的方式打印类别分布,确认两类样本比例在1:1到2:1之间才开跑,否则先做数据平衡。这个习惯救过我很多次,希望帮到你。

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

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

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

立即咨询