
简介本资源面向计算机视觉入门与进阶开发者提供一套真实场景下的安全帽佩戴目标检测数据集可用于YOLO系列模型的训练、验证与课程设计实践。数据经labelimg精细标注标注框质量高场景覆盖丰富并同步提供voc、coco和yolo三种格式标签分别存放于不同文件夹可直接对接主流检测框架。压缩包共2000个文件以1000个xml标注、990个txt标签为主另含少量html教程、py脚本与yaml配置整体约33.1MB体积轻便易于下载与迁移。资源附赠数据集划分脚本可按需生成训练集、验证集、测试集并配套Windows与Linux环境搭建及训练案例教程帮助读者快速跑通从环境配置到模型训练的全流程。目前已有557人学习下载适合希望低成本获取高质量标注数据、快速上手目标检测实战的读者参考使用。1. 安全帽检测数据集拿到手1000 张图、三套标签、一个划分脚本先跑通再谈调优工地现场的安全帽佩戴检测是 YOLO 系列最经典也最容易翻车的落地场景之一。你手上如果正好有一份「1000 张图片 VOC/COCO/YOLO 三种格式标签 划分脚本 训练教程」的打包资源那它解决的其实不是「有没有数据」的问题而是「数据能不能直接喂进训练管线」的问题。很多人卡在第一步图片有了标签格式对不上训练脚本读不进去于是开始怀疑数据集质量。实际上VOC 的 XML、COCO 的 JSON、YOLO 的 TXT 三者描述的是同一批标注框只是坐标系和文件组织方式不同。这篇笔记就按「先理解三种格式的差异 → 用划分脚本切分 → 配好 YOLO 训练参数 → 排查常见报错」的顺序把这份安全帽佩戴目标检测数据集从压缩包一路推到能出第一版权重。适合刚拿到数据集的新手照着做也适合熟手直接跳到参数表和避坑章节看边界条件。2. 三种标签格式到底差在哪VOC、COCO、YOLO 的坐标系与目录结构2.1 为什么同一批标注要存三份安全帽佩戴检测的标注目标通常只有两类helmet佩戴和head未佩戴有些数据集会细分成person、helmet、no_helmet三类。不管几类标注的本质都是「一张图上有哪些框、框里是什么、框在哪」。VOC、COCO、YOLO 三种格式的差异集中在三处坐标表示、文件组织、以及是否把图片尺寸写进标签。VOC 格式用 XML每个图片对应一个同名.xml文件框的坐标是绝对像素值xmin, ymin, xmax, ymax写在bndbox里。COCO 格式用一个大的 JSON 文件所有图片、标注、类别都塞在images、annotations、categories三个数组里框的坐标是[x, y, width, height]绝对像素值并且每张图有独立的id做关联。YOLO 格式最轻每张图一个.txt每行一个目标格式是class_id x_center y_center width height全部归一化到 0~1 之间。这三种格式没有谁更「高级」只有谁更适合当前管线。YOLO 训练直接吃 TXTCOCO 格式常用于评估和跨框架迁移VOC 格式则是很多标注工具LabelImg 等的默认导出。数据集同时给三份省掉的是你自己写转换脚本的时间。2.2 用 Python 快速验证三套标签是否对齐拿到数据集后不要急着训练先写一段校验脚本确认三种格式描述的框数量一致、类别映射一致。下面这段代码读取一张图的 VOC XML 和对应 YOLO TXT把 YOLO 的归一化坐标还原成像素值和 XML 里的绝对坐标做比对。import xml.etree.ElementTree as ET from PIL import Image import os def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) boxes [] for obj in root.findall(object): name obj.find(name).text bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) boxes.append((name, xmin, ymin, xmax, ymax)) return w, h, boxes def parse_yolo(txt_path, img_w, img_h): boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cid, xc, yc, bw, bh map(float, parts) # 还原成 VOC 的绝对坐标 xmin (xc - bw / 2) * img_w ymin (yc - bh / 2) * img_h xmax (xc bw / 2) * img_w ymax (yc bh / 2) * img_h boxes.append((int(cid), xmin, ymin, xmax, ymax)) return boxes # 示例替换成你数据集里的实际路径 xml_file annotations/voc/000001.xml txt_file labels/yolo/000001.txt w, h, voc_boxes parse_voc(xml_file) yolo_boxes parse_yolo(txt_file, w, h) print(f图片尺寸: {w}x{h}) print(fVOC 框数: {len(voc_boxes)}, YOLO 框数: {len(yolo_boxes)}) for vb, yb in zip(voc_boxes, yolo_boxes): print(fVOC: {vb[0]} ({vb[1]:.1f},{vb[2]:.1f},{vb[3]:.1f},{vb[4]:.1f})) print(fYOLO还原: class{yb[0]} ({yb[1]:.1f},{yb[2]:.1f},{yb[3]:.1f},{yb[4]:.1f}))这段代码的关键在于parse_yolo里的还原公式xmin (xc - bw/2) * img_w。YOLO 的x_center和width都是相对整张图宽高的比例乘回去才能和 VOC 的绝对像素对齐。如果打印出来的框数不一致或者坐标偏差超过 2 个像素说明三套标签里至少有一套没对齐这时候直接训练只会得到一堆假阳性。参数上注意两点一是img_w和img_h必须从 VOC XML 的size里读不能硬编码因为数据集里图片尺寸可能不统一二是类别 ID 的映射VOC 用字符串类别名YOLO 用从 0 开始的整数你需要一份classes.txt来固定顺序否则helmet可能在一套标签里是 0在另一套里是 1。2.3 COCO JSON 的读取与类别映射检查COCO 格式的校验稍微麻烦一点因为所有信息都在一个 JSON 里。下面这段代码统计 COCO JSON 里的图片数、标注数并打印类别映射表。import json from collections import Counter with open(annotations/coco/instances.json) as f: coco json.load(f) print(f图片数: {len(coco[images])}) print(f标注数: {len(coco[annotations])}) print(f类别数: {len(coco[categories])}) # 类别映射id - name cat_map {c[id]: c[name] for c in coco[categories]} print(类别映射:, cat_map) # 统计每个类别的框数 counter Counter(ann[category_id] for ann in coco[annotations]) for cid, cnt in counter.items(): print(f{cat_map[cid]}: {cnt} 个框)跑完这段你应该能看到helmet和head或no_helmet的框数大致均衡。如果某一类只有几十个框而另一类有上千个那训练时就会出现严重的类别不平衡需要靠数据增强或者损失函数里的类别权重来补。COCO 的category_id不一定从 0 开始连续YOLO 训练前必须重映射成 0~N-1这一步在划分脚本里通常会处理。3. 用划分脚本切分训练集、验证集、测试集比例、随机种子与路径陷阱3.1 划分脚本一般长什么样数据集包里带的划分脚本核心逻辑就是读取所有图片文件名按比例随机分配到train、val、test三个列表然后把对应的标签文件复制或软链接到目标目录。下面是一个典型的划分脚本骨架我按常见做法补全了路径处理和随机种子。import os import random import shutil from pathlib import Path # 配置区按你的实际目录改 IMG_DIR Path(images) # 所有图片放这里 LABEL_DIR Path(labels/yolo) # YOLO 格式标签 OUT_DIR Path(dataset_split) # 输出目录 TRAIN_RATIO 0.8 VAL_RATIO 0.1 TEST_RATIO 0.1 SEED 42 random.seed(SEED) # 收集所有图片假设 jpg/png 混用 images sorted([p for p in IMG_DIR.iterdir() if p.suffix.lower() in (.jpg, .jpeg, .png)]) random.shuffle(images) n len(images) n_train int(n * TRAIN_RATIO) n_val int(n * VAL_RATIO) splits { train: images[:n_train], val: images[n_train:n_train n_val], test: images[n_train n_val:] } for split, files in splits.items(): img_out OUT_DIR / split / images lbl_out OUT_DIR / split / labels img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) for img_path in files: # 复制图片 shutil.copy(img_path, img_out / img_path.name) # 复制对应标签 lbl_path LABEL_DIR / (img_path.stem .txt) if lbl_path.exists(): shutil.copy(lbl_path, lbl_out / lbl_path.name) else: print(f[警告] 缺少标签: {lbl_path}) print(f训练集: {len(splits[train])} 张) print(f验证集: {len(splits[val])} 张) print(f测试集: {len(splits[test])} 张)这段脚本里最容易被忽略的是SEED 42。固定随机种子意味着你每次跑出来的划分结果一致方便复现实验。如果不固定换台机器或者重跑一次训练集和验证集的组成就变了模型指标波动你根本分不清是调参带来的还是数据划分带来的。TRAIN_RATIO、VAL_RATIO、TEST_RATIO三个比例加起来必须等于 1。1000 张图按 8:1:1 切得到 800 张训练、100 张验证、100 张测试。如果数据集本身类别分布不均比如某些场景下no_helmet样本很少那最好用分层抽样保证每个 split 里两类比例接近。简单做法是先按类别分组再各自切分但大多数划分脚本不内置这个逻辑需要你自己改。3.2 路径陷阱绝对路径、软链接与中文目录划分脚本跑完下一步是配 YOLO 的data.yaml。这里有个血泪经验YOLO 训练时读的是data.yaml里的train、val路径如果你写的是相对路径它相对于的是「启动训练命令时的工作目录」而不是data.yaml所在目录。很多人把data.yaml放在dataset_split下然后在项目根目录启动训练结果 YOLO 找不到图片报No labels found或者直接崩掉。稳妥做法是写绝对路径或者用path字段指定根目录再用相对路径拼。下面是一个data.yaml的示例path: /home/user/dataset_split # 数据集根目录绝对路径 train: train/images val: val/images test: test/images nc: 2 names: [helmet, head]nc是类别数names的顺序必须和 YOLO TXT 里的class_id严格对应。如果你在 2.2 节校验时发现helmet是 0、head是 1那这里就按这个顺序写。写反了模型也能训但推理时会把戴安全帽的人标成未佩戴这种错误在验证集指标上不一定明显因为两类框的形状可能很像。另一个坑是中文目录。YOLO 底层用 OpenCV 读图OpenCV 在某些版本下对中文路径支持不好会静默返回None然后训练时报「图片为空」。如果你的数据集路径里有中文要么改成纯英文要么在代码里用cv2.imdecode配合np.fromfile绕过去。最省事的办法就是一开始就别用中文目录。4. 配好 YOLO 训练参数从 data.yaml 到第一个权重文件4.1 训练命令与关键参数含义假设你用的是 YOLOv5 或 YOLOv8 这类常见实现训练命令的核心结构差不多。下面以命令行方式给出一个可复现的启动示例# 以 YOLOv5 为例YOLOv8 把 --weights 换成 modelyolov8n.pt 即可 python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data dataset_split/data.yaml \ --weights yolov5s.pt \ --project runs/train \ --name helmet_exp1 \ --cache逐项说明--img 640是输入分辨率安全帽检测里小目标不少640 是精度和显存的平衡点显存够可以上 1280但训练时间会翻倍。--batch 16是批大小1000 张图的数据集用 16 比较稳显存 8G 以下建议降到 8。--epochs 100是训练轮数安全帽这种两类任务100 轮通常能收敛但要看验证集 mAP 曲线如果 80 轮后还在涨就加到 150。--weights yolov5s.pt是预训练权重用 COCO 预训练的模型做迁移学习比从零训练快得多这也是为什么热词里「yolo预训练模型下载」一直有人搜。--cache把图片缓存到内存1000 张图占不了多少内存但能明显加快每个 epoch 的读取速度。如果你用的是 YOLOv8命令更简洁yolo detect train \ datadataset_split/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ projectruns/train \ namehelmet_exp1YOLOv8 把参数写成了keyvalue形式model指定预训练权重imgsz对应--img。两种写法没有本质区别选你环境里装好的那个版本就行。4.2 训练过程中该盯哪些指标启动训练后控制台会打印每个 epoch 的损失和指标。安全帽检测最该盯的是mAP0.5和mAP0.5:0.95前者是 IoU 阈值 0.5 时的平均精度后者是 0.5 到 0.95 每隔 0.05 取一次的平均值。如果mAP0.5能到 0.85 以上说明框的位置基本对了如果mAP0.5:0.95很低说明框的紧致度不够可能是标注框画得太松。另一个要盯的是cls_loss和obj_loss。cls_loss是分类损失如果它一直不降说明模型分不清helmet和head可能是两类样本外观太像或者类别标签有错。obj_loss是目标置信度损失如果它震荡得厉害通常是学习率太大或者 batch 太小。热词里有人搜「yolo训练中bn崩溃」这通常发生在 batch 小于 2 的时候BatchNorm 层没法计算有效的统计量解决办法是把 batch 调到 4 以上或者改用 GroupNorm。训练结束后权重文件会保存在runs/train/helmet_exp1/weights/下best.pt是验证集指标最好的那一版last.pt是最后一轮的。推理时优先用best.pt。4.3 用验证集跑一次推理确认模型真的学到了东西训练完不要只看指标拿几张验证集图片跑一次推理肉眼确认框的位置和类别。下面是用 YOLOv5 的detect.py做推理的命令python detect.py \ --weights runs/train/helmet_exp1/weights/best.pt \ --source dataset_split/val/images \ --img 640 \ --conf 0.25 \ --save-txt \ --project runs/detect \ --name helmet_val--conf 0.25是置信度阈值低于这个值的框不显示。安全帽检测里如果漏检比误检更严重可以把阈值降到 0.1如果误检太多就提到 0.4。--save-txt会把检测结果存成 YOLO 格式的 TXT方便你写脚本和真实标签做比对算一下实际漏检率和误检率。推理结果图会存在runs/detect/helmet_val/下。打开几张看看重点检查三种情况一是远处的小目标有没有被漏掉二是密集人群里框有没有粘连三是逆光或阴影下的安全帽有没有被误判成head。这三种情况是安全帽检测的经典难点如果验证集上表现不好说明训练数据里这类场景太少需要补数据或者做针对性增强。5. 避坑与排查安全帽数据集训练中最容易翻车的 5 个点5.1 现象训练启动即报「No labels found」原因data.yaml里的train路径指向的目录下没有labels子目录或者图片和标签没有按 YOLO 要求的「同名不同后缀」规则配对。YOLO 读标签时会把图片路径里的images替换成labels再把后缀换成.txt。如果你的目录结构是train/images/和train/labels/那没问题如果是train/下直接混放图片和 TXTYOLO 就找不到。解决按 3.1 节脚本输出的结构组织目录确保train/images/000001.jpg对应train/labels/000001.txt。如果标签在别处用软链接或者改data.yaml里的路径。5.2 现象训练 loss 正常下降但 mAP 一直是 0原因类别映射错了。YOLO TXT 里的class_id是 0 和 1但data.yaml里的names写成了[head, helmet]顺序反了。模型学到的「0 号类」其实是helmet但评估时按head去算自然对不上。解决回到 2.2 节的校验脚本打印 YOLO TXT 里每个class_id对应的实际类别然后按这个顺序改data.yaml的names。改完重新训练不要接着旧权重继续训。5.3 现象验证集指标很高但实际场景漏检严重原因训练集和验证集来自同一批图片的随机划分场景分布几乎一样。如果原始 1000 张图里大部分是白天、正面、近距离的工人那模型在验证集上表现好是正常的但换到夜间、侧面、远距离的工地监控画面就会大面积漏检。解决划分数据集时按场景分层比如白天/夜间、室内/室外、单人/多人各占一定比例。如果原始数据里某些场景缺失靠数据增强补随机调整亮度、对比度模拟光照变化随机裁剪模拟远距离小目标随机旋转模拟侧面角度。YOLO 内置的--augment参数可以开一部分但针对性的增强还是得自己写。5.4 现象训练到一半突然报显存不足原因--img设得太大或者--batch太大或者开了--cache但内存不够。1000 张图在 640 分辨率下batch 16 通常占 4~6G 显存如果同时开了--cache且图片是 4K 原图内存也会爆。解决先把--batch减半如果还不行就把--img降到 416 或 320。--cache可以关掉或者改成--cache ram只缓存到内存、不缓存到磁盘。显存实在紧张用--device 0指定单卡别用多卡并行。5.5 现象推理时框的位置对但类别标签全是helmet原因head类样本太少模型学会了「偷懒」——把所有框都预测成helmet因为这样在训练集上损失最小。这是典型的类别不平衡问题。解决在损失函数里给head类更高的权重或者在数据加载时对head类样本做过采样。YOLOv5 的--cls_pw参数可以调类别权重但更直接的办法是复制head类样本的图片和标签让两类框数接近 1:1。如果head类实在少考虑用 focal loss 替换默认的 BCE loss让模型更关注难分类的样本。6. 从 1000 张到可部署模型用混淆矩阵和 PR 曲线定位最后 10% 的精度训练完第一版模型指标卡在某个数上不去的时候别急着调学习率或者换网络结构。先画混淆矩阵和 PR 曲线看清楚模型到底错在哪。YOLOv5 训练结束后会自动生成confusion_matrix.png和PR_curve.png在runs/train/helmet_exp1/目录下。混淆矩阵的横轴是预测类别纵轴是真实类别对角线上的数字越大越好。如果head被预测成helmet的数量很多说明模型对「未佩戴」的特征学得不够这时候补head类样本比调参有效得多。PR 曲线看的是每个类别的查准率和查全率 trade-off。曲线下的面积就是 AP两类 AP 的平均就是 mAP。如果helmet的 AP 是 0.92head只有 0.75那瓶颈就在head类。把head类的样本单独拎出来看是不是标注框画得太小、太模糊或者和helmet的边界不清晰。安全帽检测里工人把帽子拿在手上、或者帽子挂在脖子上这种边界情况最容易标错也最容易让模型困惑。我自己的习惯是每次训练完先跑一遍验证集推理把漏检和误检的图各挑 20 张出来按场景分类逆光、遮挡、小目标、密集人群。然后针对占比最高的那一类场景去原始数据里找同类图片补进训练集。1000 张图听起来不多但如果每一轮都能精准补 50 张难例三轮下来模型在真实场景的漏检率能降一半。这个「挑错图 → 补数据 → 重训」的循环比盲目加 epoch 或者换更大的模型有用得多。最后说一个验证技巧把测试集图片按时间顺序排列模拟监控视频的连续帧然后看模型在连续帧上的检测结果是否稳定。如果同一顶安全帽在相邻两帧里一会儿被标成helmet、一会儿被标成head说明模型的时序一致性不好部署到视频流上会闪烁。解决办法是在推理后加一个简单的跟踪逻辑用前后帧的框做平滑或者训练时加入连续帧样本。这个坑我在实际项目里踩过当时模型指标很好看一上视频就露馅希望帮到你。本文还有配套的精品资源点击获取