简介这是一份面向目标检测入门及工程落地人群的YOLO安全帽佩戴检测数据集覆盖真实作业场景中的多种角度、光照与复杂背景图片经LabelImg人工标注边界框质量高。压缩包共2000个文件、约167.37MB其中以1986个xml标注文件为主另含Python划分脚本、txt列表与HTML教程文档数据标签同时提供VOC、COCO、YOLO三种格式并分目录存放拿到后可直接接入YOLO系列训练流程无需再做格式转换。随包附带的划分脚本支持按需求自动生成训练集、验证集、测试集配套的Windows/Linux版环境搭建与训练案例教程可引导用户从环境配置、数据准备到模型训练完整走通一遍有助于快速验证模型效果并提升在真实工地场景下的泛化能力。已有761人学习下载适合正在开展安全帽佩戴检测项目或相关课程设计的学生与开发者使用。1. 5000张图片的数据包为什么我建议你先别急着训练拿到「YOLO安全帽佩戴目标检测数据集」这样的压缩包新手的第一反应通常是解压、打开教程、跑训练。但我看下来这个包真正值钱的部分反而不是训练命令而是那套同时存在的VOC、COCO、YOLO三种格式标签以及那个划分脚本。安全帽检测看起来是个单类别目标检测但工地场景里光线乱、人头密集、安全帽在画面里经常只有几十个像素5 000张图说多不多说少不少能不能把数据吃透直接决定你训练出来的模型是“能跑”还是“能上线”。这篇笔记就顺着这个标题把数据集结构、格式转换、划分脚本、训练参数和常见翻车点完整讲一遍新手可以照着复现熟手可以拿来做边界参考。2. 三种标注格式的差异与选型VOC、COCO、YOLO到底该用哪个2.1 VOC的XML标注从文件夹结构到标签语义一次性看清VOC格式来自Pascal VOC它的核心是一个XML文件对应一张图片。数据包解压后你大概率会看到这样的目录VOC/ JPEGImages/ 000001.jpg 000002.jpg Annotations/ 000001.xml 000002.xml ImageSets/ Main/ train.txt val.txt把000001.xml用文本编辑器打开里面长这样annotation folderJPEGImages/folder filename000001.jpg/filename source databaseSafetyHelmetDataset/database /source size width1920/width height1080/height depth3/depth /size object namehelmet/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin362/xmin ymin511/ymin xmax476/xmax ymax586/ymax /bndbox /object /annotation这里的关键字段就三个filename决定图片对应关系size里的宽高决定后面YOLO归一化要不要读它bndbox里的四个坐标是像素级的左上角和右下角。difficult这个字段在安全帽场景里需要注意很多标注员会把远处模糊的安全帽标成difficult1如果你直接拿VOC格式喂给某个框架它默认会忽略这部分样本而安全帽小目标恰恰是漏检重灾区所以我会在转换脚本里主动把difficult的样本也保留下来。2.2 COCO的JSON为什么它是“大而全”的字典而非表格COCO格式把所有信息塞进一个JSON文件它是一个嵌套字典而不是一张表。安全帽数据集里常见的COCO结构长这样{ images: [ {id: 1, file_name: 000001.jpg, width: 1920, height: 1080} ], annotations: [ {id: 1, image_id: 1, category_id: 1, bbox: [362, 511, 114, 75], area: 8550, iscrowd: 0} ], categories: [ {id: 1, name: helmet} ] }注意bbox的四个数字是[x, y, width, height]左上角坐标加宽高不是左下角也不是中心点。很多同学从VOC转过来直接把xmin, ymin, xmax, ymax塞进COCO的bbox数组结果检测框全跑到奇怪位置这就是没区分“两点式”和“宽高式”的差别。另外area字段对后续计算mAP是有影响的COCO的mAP评估会把面积分档小目标、中目标、大目标分别算如果你转换时忘记填area有些评估库会直接报错或者把目标全部归为大目标。2.3 YOLO的TXT归一化坐标里藏着的最容易写错的除法YOLO格式是每个图片对应一个同名TXT文件每行一个目标格式是0 0.4729 0.4602 0.0323 0.0685四个数字分别是类别序号、归一化中心x、归一化中心y、归一化宽度、归一化高度。归一化的意思是把像素值除以图片宽高所以从bndbox转YOLO要做的核心计算是x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height这里最容易翻车的点有两个。第一个是忘记除以width和height直接把像素值当归一化值写进TXT训练时损失爆炸。第二个是除数和被除数写反用宽度去除y相关坐标导致框的位置完全错乱。这类错误训练过程不会报错只会让mAP非常难看属于典型的“黑匣子问题”。另外YOLO的类别序号是从0开始的如果你的数据集只有“helmet”一类那它的类别序号就是0不是1这个问题留到第五章细说。3. 把VOC/COCO转成YOLO格式转换脚本与四个边界坑3.1 目录结构怎么摆先定一个能跑的转换最小工程无论你最终用哪个框架训练YOLO系列普遍吃的是TXT标签。所以拿到数据集后的第一步是把VOC或COCO统一转成YOLO格式。我一般会先建一个干净的工作目录避免后面路径混乱workdir/ images/ # 所有图片建议直接用原始JPEGImages里的文件 labels/ # 转换后生成的TXT标签 convert_voc.py convert_coco.py split.py一个值得注意的细节是不要在原始目录上原地转换。因为你后面做数据划分、清洗、增广都可能要回溯原始标注如果原始XML被覆盖或目录被改乱找补的代价很高。备一份原始VOC或COCO目录转换产物放新目录这是数据工程的底线习惯。3.2 VOC转YOLO核心实现与参数说明下面这个脚本是我常用的VOC转YOLO实现它读取XML中的尺寸和bndbox输出TXT标签import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_file, out_dir, class_names): tree ET.parse(xml_file) root tree.getroot() # 图片尺寸必须从XML的size节点读取不能假设固定分辨率 size_node root.find(size) width int(size_node.find(width).text) height int(size_node.find(height).text) filename os.path.splitext(root.find(filename).text)[0] out_lines [] for obj in root.findall(object): class_name obj.find(name).text.strip() if class_name not in class_names: continue # 只转换指定类别跳过其他类 class_id class_names.index(class_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, width)) xmax max(0, min(xmax, width)) ymin max(0, min(ymin, height)) ymax max(0, min(ymax, height)) x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height # 过滤绝对尺寸过小的目标这类框通常是标注噪声 if box_w 0 or box_h 0: continue out_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) if out_lines: out_path os.path.join(out_dir, filename .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(out_lines))脚本里的class_names是一个列表比如[helmet]它的索引就是类别序号。逻辑上每读一个XML就生成一个TXT同时做了坐标越界裁剪和零面积过滤。这里的参数有几个值得说明的地方坐标保留6位小数足够YOLO训练使用更多位数只会让文件变大越界裁剪是必要的因为有些标注工具会框出图片边界不裁剪会导致归一化坐标出现负数或大于1的值box_w 0的过滤是为了去掉那些重合点形成的退化框这类框会让损失计算出现除零风险。3.3 COCO转YOLO从JSON里捞出bbox的细节COCO转YOLO的脚本更简单但要注意bbox的宽高语义import json import os def coco_to_yolo(json_file, img_dir, out_dir, img_ext.jpg): with open(json_file, r, encodingutf-8) as f: data json.load(f) # 建立 image_id 到文件名的映射 id_to_filename {img[id]: img[file_name] for img in data[images]} id_to_size {img[id]: (img[width], img[height]) for img in data[images]} cat_id_to_class_id {cat[id]: idx for idx, cat in enumerate(data[categories])} # 按 image_id 聚合标注 annotations {} for ann in data[annotations]: annotations.setdefault(ann[image_id], []).append(ann) for image_id, ann_list in annotations.items(): filename os.path.splitext(id_to_filename[image_id])[0] width, height id_to_size[image_id] out_lines [] for ann in ann_list: class_id cat_id_to_class_id[ann[category_id]] x, y, w, h ann[bbox] # COCO的bbox是[x,y,width,height]转归一化中心时别加除2 x_center (x w / 2) / width y_center (y h / 2) / height box_w w / width box_h h / height out_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) out_path os.path.join(out_dir, filename .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(out_lines))这里的核心提示是COCO的bbox里x, y是左上角不是中心点所以中心点要加w/2和h/2。我见过最离谱的转换错误是把xw当中心点结果所有框向右下偏移一半尺寸。另外categories的id不一定是连续的安全帽这类小数据集通常只有一类所以看不出问题但如果你后面合并了其他数据类别ID跳号是常事必须用cat_id_to_class_id这个映射把它压成连续的YOLO序号。4. 训练自己的安全帽检测模型从划分脚本到yolo命令一步到位4.1 数据集划分脚本train/val/test怎么切分才不翻车标注转换完成后下一步是把数据切成训练集、验证集和测试集。标题里带了“划分脚本”说明数据包作者已经想到这一步但你自己仍然需要理解划分逻辑因为你可能要根据工地场景调整比例。我的常用划分脚本如下import os import random from collections import defaultdict random.seed(42) img_dir images label_dir labels train_ratio 0.8 val_ratio 0.1 # 剩下0.1给test # 只保留“图片和TXT标签同时存在”的样本避免丢标签 valid_samples [] for fname in os.listdir(img_dir): stem, ext os.path.splitext(fname) if ext.lower() not in (.jpg, .jpeg, .png): continue if not os.path.exists(os.path.join(label_dir, stem .txt)): print(f跳过缺失标签的图片: {fname}) continue valid_samples.append(stem) random.shuffle(valid_samples) train_cut int(len(valid_samples) * train_ratio) val_cut int(len(valid_samples) * (train_ratio val_ratio)) train_files valid_samples[:train_cut] val_files valid_samples[train_cut:val_cut] test_files valid_samples[val_cut:] # 输出成YOLO训练用的txt清单文件 def write_split(file_list, path): with open(path, w, encodingutf-8) as f: for stem in file_list: f.write(os.path.join(img_dir, stem .jpg) \n) write_split(train_files, train.txt) write_split(val_files, val.txt) write_split(test_files, test.txt) print(ftrain{len(train_files)}, val{len(val_files)}, test{len(test_files)})划分脚本的逻辑核心是三个第一只保留图片和标签都齐全的样本训练时缺标签的图片会被YOLO直接忽略但你可能在日志里看到“图片没有标签”的警告与其事后排查不如划分时直接过滤第二固定随机种子保证每次运行划分结果一致这能让你在调参时对比的是模型差异而不是数据差异第三把比例按0.8/0.1/0.1切分安全帽数据集建议给test留一点因为你要在最后验证模型在完全没见过的图片上的表现。4.2 配置文件与标签顺序YAML里的路径与类别序号YOLO系列训练前需要一个YAML文件描述数据路径和类别名比如用Ultralytics框架时命名为helmet.yamlpath: /home/user/workdir # 数据集根目录 train: train.txt # 或直接写 images/ val: val.txt test: test.txt nc: 1 names: [helmet]这里的nc是类别数量names列表的顺序必须和转换脚本里的class_names顺序完全一致。举个例子如果你的转换脚本里class_names [head, helmet]而YAML里的names写成[helmet, head]训练不会报错但模型的输出类别语义就对调了部署时会把戴帽子的人头识别成不戴帽子的头这在安全帽检测场景里是致命错误。4.3 训练参数设置imgsz、batch、epochs的参考值与玄学Ultralytics框架训练命令很简单yolo detect train datahelmet.yaml modelyolov8s.pt epochs100 imgsz640 batch16 device0参数选择上安全帽数据集有几个实际考量。imgsz640是默认值如果你的图片是1080p建议试imgsz1280因为安全帽在画面里经常是几十像素的小目标放大输入尺寸对小目标检测提升明显代价是显存占用翻倍、训练速度下降。batch大小取决于显存16G显存跑yolov8s可以开到328G显存建议16如果训练过程出现CUDA out of memory优先降batch而不是换模型。epochs我一般从100起步安全帽这类单类别检测通常50轮就能收敛但100轮可以让你看到损失曲线在后期是否还有下降空间。这里有个常见误用有人习惯一上来就用yolov8x觉得大模型效果一定好。但在5000张图的小数据集上大模型容易过拟合验证集mAP反而不如yolov8s。我通常先用yolov8s跑通全流程确定数据没问题再尝试更大模型做精度提升。5. 避坑指南安全帽数据集训练翻车的五个常见问题5.1 现象训练日志显示“No labels found in …”这是最常碰到的第一个报错。原因通常是YAML里配的路径不对或者TXT标签目录名不是labels。比如Ultralytics默认会在images同级目录找labels如果你把标签放在annotations/txt下就必须在YAML里通过train的路径让框架能找到对应关系而不是直接把标签目录改个名字。解决方法是先确认images和labels目录在同一父目录下且同名文件的扩展名分别是.jpg和.txt。手动检查一个样本比如images/000001.jpg必须对应labels/000001.txtTXT文件里至少有一行非空标注。如果确认无误还报错检查TXT内容是否为纯英文路径且没有被转成.txt\n之类的隐藏字符。5.2 现象训练正常跑完但验证集mAP一直是0这个现象非常迷惑人因为训练损失在下降loss曲线也正常但验证时一个框都测不出来。我排查过几次根源几乎都是标签文件为空或标签内容格式错误。比如TXT文件里写了0 123 456 789 1011这种纯像素值没有做归一化模型训练时看到的是超大数值坐标直接学废。解决方法是随机挑一个TXT文件用脚本读取并确认所有数值都在0到1之间。如果发现像素值回到第三章的转换脚本重新执行重点检查width和height是否从正确的尺寸节点读取。这个坑在VOC格式里最常见因为XML的size节点如果缺失脚本可能用了默认的(0, 0)做分母产生除零错误有的库会静默跳过这条标注。5.3 现象远处工人的安全帽总是漏检这是安全帽检测的经典场景问题不算报错但直接关系到产品能否落地。原因有两层一是安全帽在画面中尺寸太小低于模型特征提取的感知极限二是正样本里小目标比例不足模型没有学到足够的小目标语义。解决方向有三个第一把imgsz从640调到1280小目标在推理时能获得更多像素第二在训练时开启mosaic1.0增强让模型看到更多小目标上下文第三如果数据里有大量远距离安全帽被标注为difficult而没进入训练回到转换脚本去掉difficult过滤逻辑把这些样本保留进训练集。我在一个工地数据集上做对比单是放开difficult样本加调大imgsz就能把小目标recall从62%提升到78%。5.4 现象loss下降缓慢或者前期震荡剧烈刚开始训练时看到loss曲线像锯齿一样上下跳动先别急着改参数。常见原因有两个一是学习率选择不当Ultralytics默认学习率是0.01如果batch从8改到32建议同步把学习率调到0.02左右二是数据集里的标注噪声太大比如一些安全帽被标成了背景模型在学到正确特征之前先被错误标注带偏。解决方法是先查标签质量写一个小脚本统计每个TXT文件的框数量分布如果发现个别TXT里有十几个框而大部分只有一两个检查对应图片是不是群体照片这类样本在安全帽场景里正常。如果标注噪声确实大可以先用yolov8n训练20轮做一次数据清洗把置信度低且与XML原始框差异大的样本挑出来人工复核。这种方法虽然“土”但对脏数据特别有效。5.5 现象模型推理时输出的“helmet”类别错位如果你的模型只有helmet一个类别很少遇到这个问题。但如果你把数据扩展到“helmet”和“head”两类就要小心类别序号在转换脚本里的定义必须和YAML里的names顺序保持一致。我自己翻过一次车把names写成[head,helmet]而转换脚本里head的类别号是1模型训练完成后在测试集上把戴安全帽的工人识别成不戴帽子的正好错位。解决方法是在训练完成后做一个快速验证用写脚本读取几张图片的预测结果打印类别名而不是类别序号。如果发现错位不要重新训练直接调整YAML里names的顺序并重新运行评估即可。这个问题的根源是“类别语义和类别序号绑定”这是YOLO格式里最需要警惕的一个约定。6. 一个验证习惯用极端图片倒逼模型暴露问题模型训练完成后我会先不急着看验证集mAP而是手动构造三组图片来“拷打”模型这个习惯帮我避开了好几次部署时才暴露的问题。第一组是逆光图片就是把工地上太阳直射镜头、人脸一片黑的照片放进测试。安全帽的高饱和颜色通常还能识别但如果没有做颜色增广模型可能在亮度变化大时直接失效。第二组是密集人群的俯视图比如站在塔吊上往下拍的画面此时安全帽只有十几像素检测框会大量重叠NMS会把相邻的检测框误删。如果这一类场景多我会在推理时把nms_iou从默认的0.45调低到0.3让重叠框保留得更少。第三组是完全没有安全帽的负样本比如一群戴草帽的工人模型如果误报出helmet说明它对“帽子形状”过拟合了而不是学会了颜色特征。用这三组图测下来比看一百行验证指标更能说明问题。我自己的教训是有一版模型在验证集上mAP到了92%看起来很不错但一放到用户现场那种半遮挡场景就漏检严重。后来排查发现训练图片里大量是正脸和侧脸几乎没有背对镜头的工人导致模型对头盔后半部分的特征没有概念。补了一批背向样本后漏检率直接降了三分之一。这个经历让我养成了固定的验证习惯每训完一个模型先拿极端场景图片过一遍再去看指标。数据集的5000张图是基础但能不能真正落地靠的是你对自己场景的理解和验证方法。希望这篇笔记能帮你在安全帽检测这条路上少走我走过的弯路。本文还有配套的精品资源点击获取