简介:本资源面向计算机视觉入门与进阶开发者、安全帽佩戴检测相关课程设计或项目实践者,提供一套真实场景下的YOLO安全帽佩戴目标检测数据集。图片场景丰富、标注质量高,使用labelimg标注,并同时提供voc、coco和yolo三种格式标签,可直接接入YOLO系列模型训练,省去格式转换与清洗成本。压缩包共约2000个文件,以1000个xml标注、990个txt标签为主,另含少量html教程、py脚本与yaml配置,整体约33.1MB,目录按格式分文件夹存放,结构清晰。资源附赠数据集划分脚本,可按需生成训练集、验证集、测试集,并配套Windows与Linux环境搭建及训练案例教程,帮助读者快速跑通从环境配置到模型训练的全流程。目前已有557人学习下载,适合希望快速上手目标检测实战、完成课程作业或验证算法效果的读者参考使用。
1. 安全帽检测数据集到底解决了什么工程问题
工地出入口的摄像头每天都在产生视频流,但真正能拦住「没戴安全帽就进场」这件事的,不是摄像头本身,而是一套能稳定跑起来的 YOLO 检测模型。而模型能不能训出来,七成取决于数据集。我见过太多团队在模型结构上反复折腾,最后发现是标注格式转错、类别对不上、训练集里混进了重复帧。这个标题里的东西——1000 张图片、voc/coco/yolo 三套标签、划分脚本、训练教程——本质上是一份「开箱即用」的检测工程起点。它适合两类人:一类是想快速验证安全帽检测可行性的算法工程师,另一类是手上有工地数据但不知道怎么组织成标准训练集的开发者。1000 张这个量级不算大,但足够跑通全流程、看清模型在真实场景下的边界,再决定要不要扩标。
2. 三种标签格式的差异与转换逻辑
2.1 VOC、COCO、YOLO 各自在描述什么
VOC 格式的核心是 XML,每张图对应一个.xml文件,里面用<object>标签逐个记录目标的类别名和边界框的xmin/ymin/xmax/ymax。它的坐标系是绝对像素值,原点在左上角。COCO 格式则把所有图片的标注塞进一个大的 JSON 文件,用images、annotations、categories三个顶层字段组织,边界框是[x, y, width, height]的绝对像素值,并且每张图有独立的id和file_name。YOLO 格式最轻量,每张图对应一个.txt,每行是class_id x_center y_center width height,全部归一化到 0 到 1 之间。
这三种格式没有谁更高级,只有谁更适合当前框架。VOC 可读性好,适合人工核对;COCO 是很多预训练模型的默认输入;YOLO 格式则是 Ultralytics 系列训练脚本直接吃的。数据集里同时给三套,省掉的是你自己写转换脚本的时间,但前提是你得知道它们之间怎么对齐。
2.2 用 Python 做 VOC 到 YOLO 的批量转换
假设你拿到的是 VOC 格式的标注,想转成 YOLO 格式训练。下面这段脚本处理的是单类别安全帽检测,所以class_id固定为 0。如果你的数据集里还有「未佩戴」这个类别,需要把类别映射表改成字典。
import xml.etree.ElementTree as ET import os from pathlib import Path # 类别映射:安全帽为 0,未佩戴为 1 CLASS_MAP = {"helmet": 0, "no_helmet": 1} def voc_to_yolo(xml_path, img_w, img_h, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text.strip() if cls_name not in CLASS_MAP: continue cls_id = CLASS_MAP[cls_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) # 归一化并转为中心点+宽高 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") Path(out_txt_path).write_text("\n".join(lines), encoding="utf-8") # 批量处理:假设图片和 XML 在同一目录,文件名一一对应 img_dir = "dataset/images" xml_dir = "dataset/annotations" out_dir = "dataset/labels_yolo" os.makedirs(out_dir, exist_ok=True) for xml_file in Path(xml_dir).glob("*.xml"): stem = xml_file.stem img_file = Path(img_dir) / f"{stem}.jpg" if not img_file.exists(): print(f"跳过 {stem}:找不到对应图片") continue # 用 PIL 读尺寸,避免手动填错 from PIL import Image with Image.open(img_file) as im: w, h = im.size voc_to_yolo(xml_file, w, h, Path(out_dir) / f"{stem}.txt")这段代码的关键点有三个。第一,CLASS_MAP必须和你的实际标注类别名完全一致,大小写敏感,多一个空格都会导致该目标被跳过。第二,图片宽高是从原图读出来的,不要用 XML 里可能存在的<size>字段,因为有些标注工具写进去的是缩放后的尺寸,和实际图片对不上。第三,归一化后的坐标如果出现负数或大于 1,说明标注框超出了图片边界,这种样本在训练时会导致损失异常,建议在转换阶段就过滤掉。
2.3 COCO 格式的读取与类别对齐
COCO 的 JSON 结构里,categories字段决定了class_id的映射关系。很多公开数据集给的 COCO 标注里,category_id是从 1 开始的,而 YOLO 训练时要求从 0 开始连续。如果你直接拿 COCO 的category_id当 YOLO 的class_id,会出现类别索引越界或者类别错位。
import json with open("annotations/instances.json", "r", encoding="utf-8") as f: coco = json.load(f) # 建立 category_id 到连续 id 的映射 cat_ids = sorted([c["id"] for c in coco["categories"]]) cat_id_to_idx = {cid: idx for idx, cid in enumerate(cat_ids)} print("类别映射:", {c["name"]: cat_id_to_idx[c["id"]] for c in coco["categories"]}) # 按 image_id 聚合标注 from collections import defaultdict anns = defaultdict(list) for ann in coco["annotations"]: anns[ann["image_id"]].append(ann) # 输出 YOLO 格式 for img in coco["images"]: img_id = img["id"] w, h = img["width"], img["height"] lines = [] for ann in anns[img_id]: cls_idx = cat_id_to_idx[ann["category_id"]] x, y, bw, bh = ann["bbox"] x_center = (x + bw / 2) / w y_center = (y + bh / 2) / h lines.append(f"{cls_idx} {x_center:.6f} {y_center:.6f} {bw/w:.6f} {bh/h:.6f}") out_name = Path(img["file_name"]).stem + ".txt" Path("labels_yolo", out_name).write_text("\n".join(lines), encoding="utf-8")这里最容易翻车的地方是bbox的格式。COCO 的bbox是[x, y, width, height],其中x, y是左上角坐标,不是中心点。我见过有人直接拿x, y当中心点去算,训出来的框全部偏移半个身位。另外,如果数据集里存在iscrowd=1的标注,建议在训练时忽略,因为这类区域通常是密集人群,边界框没有精确意义。
3. 数据集划分脚本与训练集组织
3.1 按 8:1:1 划分时怎么避免同一场景泄漏
1000 张图片如果随机划分,很容易出现同一个工地的连续帧同时出现在训练集和验证集里。模型在验证集上表现很好,一到新工地就崩,这就是场景泄漏。我的做法是先按视频来源或拍摄日期分组,再在组内随机抽,保证同一组的图片只出现在一个子集里。
import random from pathlib import Path from collections import defaultdict # 假设文件名里包含工地编号,如 siteA_001.jpg def group_key(path): return path.stem.split("_")[0] all_imgs = list(Path("dataset/images").glob("*.jpg")) groups = defaultdict(list) for img in all_imgs: groups[group_key(img)].append(img) train, val, test = [], [], [] for key, imgs in groups.items(): random.shuffle(imgs) n = len(imgs) n_train = int(n * 0.8) n_val = int(n * 0.1) train.extend(imgs[:n_train]) val.extend(imgs[n_train:n_train + n_val]) test.extend(imgs[n_train + n_val:]) # 生成 YOLO 训练所需的 txt 列表 for split_name, split_imgs in [("train", train), ("val", val), ("test", test)]: with open(f"{split_name}.txt", "w") as f: for img in split_imgs: f.write(str(img.resolve()) + "\n") print(split_name, len(split_imgs))这段脚本的核心是group_key函数。如果你的文件名没有工地编号,可以用图片的感知哈希做聚类,把相似度高的帧分到同一组。划分完成后,train.txt里写的是图片绝对路径,YOLO 训练时会自动去找同名的.txt标签文件,所以标签目录结构要和图片目录平行。
3.2 目录结构怎么摆才不会被训练脚本报错
Ultralytics 的 YOLO 训练脚本对目录结构有隐含要求。常见做法是:
dataset/ images/ train/ val/ labels/ train/ val/然后写一个data.yaml:
path: /abs/path/to/dataset train: images/train val: images/val nc: 2 names: ["helmet", "no_helmet"]注意path必须是绝对路径,train和val是相对path的路径。如果写成相对路径,训练脚本的工作目录一变就会找不到文件。另外nc要和names的长度一致,否则训练启动时就会报索引错误。
3.3 用预训练权重起步时的两个参数
如果你打算用 YOLO 的预训练模型做迁移学习,data.yaml准备好之后,启动命令通常是:
yolo detect train data=data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16这里model=yolov8n.pt会加载 COCO 预训练权重。安全帽检测和 COCO 里的person类别有重叠,所以 backbone 的特征提取能力可以直接复用。imgsz=640是安全帽检测的常用输入尺寸,再小会丢小目标,再大显存吃不消。batch=16在 8GB 显存的卡上比较稳,如果出现CUDA out of memory,先降到 8 再试。
4. 训练过程中最容易翻车的五个地方
4.1 损失函数不下降,但验证集指标在涨
现象:训练日志里box_loss和cls_loss震荡不降,但mAP50缓慢上升。原因通常是学习率设得偏高,或者数据增强过猛导致训练样本分布和验证集差异大。解决方法是先把学习率降到0.001,关掉mosaic增强跑 10 个 epoch 看损失是否稳定下降。如果稳定了,再逐步开增强。
4.2 混淆矩阵里安全帽和未佩戴互相误检
现象:混淆矩阵显示helmet和no_helmet之间有大量交叉。原因有两个:一是标注时边界框画得太松,把背景也框进去了;二是两类样本数量严重不均衡。解决方法是检查标注框是否紧贴目标边缘,同时对少数类做过采样,或者在损失函数里给少数类更高的权重。
4.3 训练到一半 BN 层崩溃,loss 变成 NaN
现象:前几十个 epoch 正常,突然 loss 变成nan,之后再也降不下来。这是典型的梯度爆炸。原因可能是某个 batch 里出现了宽高为 0 的标注框,或者学习率在后期没有衰减。解决方法是检查所有标签文件,过滤掉width或height小于 0.001 的行,同时在训练配置里加上lr0=0.01和lrf=0.01,让学习率从 0.01 余弦衰减到 0.01。
4.4 验证集 mAP 很高,但实际视频推理漏检严重
现象:验证集mAP50到 0.9,但拿工地视频跑推理,没戴安全帽的人经常漏检。原因是验证集和训练集来自同一批视频帧,场景高度相似,模型过拟合了。解决方法是重新划分数据集,确保验证集里有不同光照、不同角度、不同工地的样本。如果做不到,至少把验证集里的连续帧打散。
4.5 推理时框的位置整体偏移
现象:模型输出的框位置整体往一个方向偏。原因通常是训练时的图片尺寸和推理时的预处理不一致。YOLO 训练时会把图片 resize 到imgsz,推理时如果用了不同的 resize 方式,坐标映射就会错。解决方法是推理时也用imgsz=640,并且不要手动做 letterbox 之外的裁剪。
5. 从 1000 张到可部署模型的进阶技巧
1000 张图片训出来的模型,在实验室里看指标还行,但真要放到工地边缘盒子上跑,还得做几件事。第一是量化。把 PyTorch 模型转成 ONNX 再转 TensorRT,FP16 精度下推理速度能翻倍,mAP 掉不到 1 个点。第二是难例挖掘。用初版模型跑一遍未标注的工地视频,把置信度在 0.3 到 0.6 之间的帧抽出来人工复核,补进训练集。这个循环做两轮,比单纯加数据量更有效。
第三是类别定义要收敛。安全帽检测里,「戴了但没系下颚带」算不算违规?如果算,就得单独一个类别,但 1000 张里这类样本可能不到 50 张,训出来的分支会很不稳定。我的习惯是先把问题简化成「戴了」和「没戴」两类,等数据量到 5000 张以上再细分。
验证模型有没有真正学到东西,不要只看 mAP。我会做一个小测试:把验证集里所有安全帽的图片裁出来,只留安全帽区域,看模型还能不能正确分类。如果裁掉背景后准确率暴跌,说明模型学的是背景捷径,不是安全帽本身的特征。这个测试花不了十分钟,但能提前暴露部署后的翻车风险。
最后一个习惯:每次训练完,把data.yaml、训练命令、权重文件、验证集指标写进一个train_log.md。过两个月回头看,能省掉大量「当时这个参数为什么这么设」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取