简介:这是一份面向交通安全监控与自动驾驶场景的安全带佩戴目标检测数据集,适合计算机视觉初学者及算法工程师用于YOLO、Faster R-CNN等模型的训练与评测。压缩包共534个文件,约54.66MB,其中264张jpg图片配合132个xml标注构成VOC格式的JPEGImages与Annotations目录,便于在Pascal VOC工具链中完成标注查看;另有136个txt标签、划分好的train.txt/val.txt及1个yaml配置组成YOLO格式目录,可直接接入主流检测框架。内容涵盖驾驶室内人物、安全带及佩戴状态的精确框选,标注类别清晰一致,可支撑二分类目标检测与乘员状态识别等典型任务。当前已有311人学习下载,数据规模紧凑且格式标准,既适合课堂教学或论文实验,也能用于安全带佩戴检测系统的快速原型验证与算法迭代。借助这份数据集,开发者可快速验证模型在车载复杂环境下的表现。
1. 为什么「有无佩戴安全带」数据集值得认真对待:卡口图里的二分类难题
交通监控每天产生几千张卡口抓拍图,但有一样东西明明就在眼前、却很难被自动系统看见——那就是安全带。「有无佩戴安全带目标检测数据集.zip」这份压缩包要解决的,就是“车里有几个人、谁系了安全带、谁没系”这个看似简单、实际很容易翻车的二分类检测问题。它把不同光照、车型和视角的抓拍图整理成带标注的训练样本,可以直接喂给 YOLO、RT-DETR 这类目标检测框架,输出“有带”和“无带”两类目标框。对做交通违法自动取证的团队,它是处罚单据的自动依据;对做驾驶员监控和事故分析的团队,它是事后回放的关键证据。新手能拿它练一遍从解压到上线的完整流程,熟手则更该关注类别不平衡、夜间样本和标注歧义这些决定模型上限的细节。
2. 解压后先做数据体检:目录结构、标注协议与三类重复样本清洗
拿到 zip 的第一反应不是写训练脚本,而是先搞清楚压缩包里到底装了什么。标注格式决定了你能用哪一套训练管线:是走 YOLO 原生的 txt 标签,还是先写一个 VOC 转 YOLO 的转换脚本。这个过程跑不顺,后面的训练和调参全都会被带偏。
2.1 先看目录再看标注:VOC 与 YOLO 两种组织方式的差异
解开压缩包后我会执行这样几条命令,先建立目录认知:
unzip 有无佩戴安全带目标检测数据集.zip -d seatbelt_data cd seatbelt_data find . -maxdepth 2 -type d | sort注意这里-d参数是指定解压目标目录,避免 zip 里自带一层套一层的文件夹结构把路径搞乱;find只列两级目录是为了先看大类,而不是被几百个图片文件名淹没。如果机器上没有tree命令,也可以用ls -R | less做同等检查。
常见的安全带检测数据集会有两种组织风格:一种是images/加annotations/,标注以 XML 形式存储,属于 VOC 格式;另一种是images/加labels/,标注是纯文本,属于 YOLO 格式。还有少数数据集会直接给 COCO 的 JSON 文件。三者的差异决定了后续代码怎么写:
| 维度 | VOC XML | YOLO TXT | COCO JSON |
|---|---|---|---|
| 坐标类型 | 像素绝对坐标xmin,ymin,xmax,ymax | 归一化相对坐标cx,cy,w,h | 像素绝对坐标x,y,w,h |
| 类别表示 | 字符串,如with_seatbelt | 整数索引0或1 | 整数 ID 与类别表对应 |
| 读取成本 | 需解析 XML,略繁琐 | 一行一个目标,最轻量 | 单个大 JSON,需按 image_id 索引 |
绝大多数目标检测训练框架,比如 ultralytics 的 YOLOv8/YOLOv11,原生只认 YOLO TXT 格式。所以第一步要确认标注文件后缀:如果看到的是.xml,后面必然要写转换脚本;如果已经是一堆.txt,则优先检查类别索引是不是从 0 开始连续编号。安全带数据集通常就两个类,常见命名是with_seatbelt和without_seatbelt,少数会把“车身内部无人”也标成一类。这个看似简单的命名差异,会在转换脚本里直接影响类别索引的顺序,后面第 3 章会专门讲。
打开一个 VOC 标注 XML 看内部结构,也是确认字段完整性的好办法:
head -30 annotations/000001.xml重点看<size>节点里的宽高是不是和真实图片一致,以及<object>节点里有没有<truncated>、<occluded>这类困难样本标记。安全带数据集中,安全带被衣领、方向盘、安全气囊盖遮挡的比例并不低,这些标记字段直接关系到第 4 章里遮挡误判的处理。
2.2 数据体检三步:统计类别分布、检查空标注、处理视频抽帧重复
标注格式确认之后,先别急着转换。我会按顺序做三件体检,每件都对应一个真实的翻车隐患。
第一件是统计类别分布,用脚本把每个类别的目标数量拉出来:
import os from collections import Counter import xml.etree.ElementTree as ET annotation_dir = "seatbelt_data/annotations" counter = Counter() empty_files = [] for fname in os.listdir(annotation_dir): if not fname.endswith(".xml"): continue tree = ET.parse(os.path.join(annotation_dir, fname)) objects = tree.getroot().findall("object") if not objects: empty_files.append(fname) for obj in objects: name = obj.find("name").text.strip() counter[name] += 1 print("类别统计:", dict(counter)) print("空标注文件数:", len(empty_files)) print("空标注示例:", empty_files[:5])逻辑说明:Counter用来累计每个类别出现的次数,空标注文件单独记录而不是直接跳过,是为了后面判断“这张图是真的没人,还是漏标了”。安全带检测最常见的类别不均衡,就是with_seatbelt远多于without_seatbelt,因为规范行驶时系带率本身就高;这个数字如果差距超过 5 倍,后面训练时就要考虑过采样或类别加权。
第二件是可视化抽查,把标注框画到原图上,用肉眼确认框的贴合度:
import cv2 import matplotlib.pyplot as plt import xml.etree.ElementTree as ET img = cv2.imread("seatbelt_data/images/000001.jpg") tree = ET.parse("seatbelt_data/annotations/000001.xml") for obj in tree.getroot().findall("object"): box = obj.find("bndbox") x1 = int(float(box.find("xmin").text)) y1 = int(float(box.find("ymin").text)) x2 = int(float(box.find("xmax").text)) y2 = int(float(box.find("ymax").text)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, obj.find("name").text, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.axis("off") plt.show()说明:画框时把y1 - 6做了下界保护,防止文字画到图像外面;用cvtColor是因为 OpenCV 读进来是 BGR 顺序,直接交给 matplotlib 会偏色。抽查时重点看两类问题:框是不是把安全带整条斜线都包住了,以及有没有把座椅靠背的纹理误标成安全带。
第三件是查重复样本。安全带数据集很多来自监控视频抽帧,相邻两帧的图像几乎一样。如果训练集和验证集都混有同一段视频的帧,验证指标会虚高,模型上线面对新场景时立刻原形毕露。先做精确去重:
md5sum images/*.jpg | sort | awk '{print $1}' | uniq -c | sort -rn | headmd5sum算出每个文件的哈希,sort后uniq -c统计相同哈希出现次数,sort -rn按次数降序排列。输出里次数大于 1 的就是完全重复的文件。精确去重之后还要处理近似重复,相邻抽帧在不同编码下 MD5 不同,但内容几乎一样,这时可以算感知哈希:
import os from PIL import Image import imagehash # pip install imagehash hash_size = 8 images = sorted(os.listdir("seatbelt_data/images")) prev_hash = None near_dups = [] for name in images: h = imagehash.phash(Image.open("seatbelt_data/images/" + name), hash_size) if prev_hash is not None and h - prev_hash <= 4: near_dups.append(name) prev_hash = h print("疑似连续重复帧:", len(near_dups)) print(near_dups[:10])逻辑说明:imagehash.phash返回一个哈希值,可以直接做差,差值越小表示图像越相似。这里取阈值 4,是经验值——同一监控视频相邻帧之间的差异通常落在 0 到 2,不同场景切换时差异会跳到 20 以上。发现近似重复帧后,我会按文件名分组,每一组只保留一帧进训练集。这一条是很多团队踩过坑的地方:不处理重复帧直接训练,val mAP 能到 0.95,换到真实验证集立刻掉到 0.7,不是模型的问题,是数据划分的问题。
3. 把 VOC 标注转成 YOLO 格式并用 YOLOv8 训通:转换脚本与 4 个边界坑
体检做完,如果确认是 VOC 格式,下一步就是写转换脚本。这步看起来简单,实际坑不少。第 3 章把转换脚本、训练配置和最常见的边界坑一次说清。
3.1 转换脚本:类名到索引、坐标归一化与空 txt 的取舍
安全带数据集的标注框有它的特殊性:安全带是斜向的长条状目标,框的宽高比经常大于 3:1,坐标的小数精度直接影响目标框贴合度。下面这段脚本是我常用的转换方案:
import os import cv2 import xml.etree.ElementTree as ET from pathlib import Path VOC_DIR = "seatbelt_data/annotations" IMG_DIR = "seatbelt_data/images" OUT_DIR = "seatbelt_data/labels" # 类别顺序就是 YOLO 类索引,改这里必须同步改 data.yaml 的 names CLASSES = ["with_seatbelt", "without_seatbelt"] os.makedirs(OUT_DIR, exist_ok=True) for xml_name in os.listdir(VOC_DIR): if not xml_name.endswith(".xml"): continue stem = Path(xml_name).stem img = cv2.imread(os.path.join(IMG_DIR, stem + ".jpg")) if img is None: img = cv2.imread(os.path.join(IMG_DIR, stem + ".png")) if img is None: print("缺图:", xml_name) continue height, width = img.shape[:2] tree = ET.parse(os.path.join(VOC_DIR, xml_name)) lines = [] for obj in tree.getroot().findall("object"): name = obj.find("name").text.strip() if name not in CLASSES: print("未知类别:", name, xml_name) continue b = obj.find("bndbox") x1 = float(b.find("xmin").text) y1 = float(b.find("ymin").text) x2 = float(b.find("xmax").text) y2 = float(b.find("ymax").text) cx = (x1 + x2) / 2 / width cy = (y1 + y2) / 2 / height bw = (x2 - x1) / width bh = (y2 - y1) / height cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) lines.append(f"{CLASSES.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") # 空 txt 也要保留,保证每张图都有对应标签文件 with open(os.path.join(OUT_DIR, stem + ".txt"), "w") as f: f.write("\n".join(lines))逻辑说明:脚本用cv2.imread读取原图来拿真实宽高,而不是直接读 XML 里的<size>,因为有些数据集的<size>字段填得不准。坐标归一化后加min/max裁剪,是为了防止标注框越界的负坐标穿透到下一行。最后无论有没有目标都写出 txt 文件,空文件保留,避免 ultralytics 训练时图像和标签对不上。
这个脚本里藏着四个边界坑,挨个说明:
第一,类别顺序即是索引顺序。CLASSES列表的顺序决定 txt 第一列是 0 还是 1。常见翻车是转换时用了["with_seatbelt", "without_seatbelt"],之后整理data.yaml时又把without_seatbelt写在前面,导致 0 和 1 正好互换,训练出来的模型把系了安全带的全部判成没系,直接离谱。
第二,坐标越界必须收敛。原始 XML 里偶尔会出现xmin比xmax大,或者坐标超出图像边界的脏数据。脚本里统一裁剪到[0, 1]区间,至少保证不产生负宽度和越界框;如果某个框裁剪后宽高小于 0.001,说明原始标注已经损坏,应该在统计阶段就直接剔除。
第三,空 txt 文件不能省。YOLO 训练时是“一图对应一标签”的索引方式,缺了某个标签文件,数据加载器会跳过这张图,导致实际参与训练的图片数量比你预期少。保留空 txt 是最稳妥的做法。
第四,图片扩展名要检查。数据集中可能 JPG 和 PNG 混存,脚本里先找.jpg再补.png,如果两种都找不到就打印提示而不是静默跳过。这个提示信息在数据集很大的时候特别有用,能直接暴露文件命名不规范的批次。
转换完成后,检查一下输出:
ls seatbelt_data/labels | wc -l ls seatbelt_data/images | wc -l head -3 seatbelt_data/labels/000001.txt数量对得上,且 txt 内容是class cx cy w h格式,就可以进入训练配置了。
3.2 训练配置:数据目录、seatbelt.yaml 与参数取舍
ultralytics 系列要求数据集按固定目录结构组织。我先按比例切分训练集和验证集,再建立软链接:
cd seatbelt_data python - <<'EOF' import os import random import shutil random.seed(42) imgs = os.listdir("images") random.shuffle(imgs) os.makedirs("train", exist_ok=True) os.makedirs("val", exist_ok=True) val_count = int(len(imgs) * 0.1) for i, name in enumerate(imgs): target = "val" if i < val_count else "train" stem = name.rsplit(".", 1)[0] shutil.move(os.path.join("images", name), os.path.join(target, name)) shutil.move(os.path.join("labels", stem + ".txt"), os.path.join(target, stem + ".txt")) EOF这里设置random.seed(42)是为了让切分结果可复现,方便后面对比不同训练参数时保证数据一致。切分比例按 9:1 走,因为安全带检测对验证集的规模要求不高,更重要的是验证集要覆盖夜间样本——如果随机切分导致夜间图全进了训练集,验证结果就会虚高。
然后创建数据配置文件seatbelt.yaml:
path: ./datasets/seatbelt train: images/train val: images/val nc: 2 names: 0: with_seatbelt 1: without_seatbeltnc是类别数量,names的索引顺序必须和转换脚本里的CLASSES完全一致,这一点再强调一次。目录层级上,images/train和labels/train要并列,ultralytics 会自动把images替换成labels去关联标签。如果发现训练时标签全部为空的报错,优先检查这个目录结构。
训练命令如下,以 YOLOv8 为例:
yolo detect train \ data=seatbelt.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ project=runs/seatbelt参数说明:model=yolov8n.pt是 nano 版预训练权重,先拿它把流程跑通,确认数据没问题再换yolov8s.pt或yolo11s.pt。imgsz=640是起步值,如果后面发现安全带这种细长目标召回率低,可以拉到 960,代价是显存占用和训练时间同步上升。batch=16在 8GB 显存下比较稳妥,显存不足报CUDA out of memory时就降到 8。patience参数建议设成 20,验证集 mAP 连续 20 轮不涨就早停,省时间。
用 Python API 训练也是同等配置:
from ultralytics import YOLO model = YOLO("yolov8n.pt") model.train( data="seatbelt.yaml", epochs=100, imgsz=640, batch=16, lr0=0.005, patience=20, )lr0=0.005是基于预训练权重的保守学习率,适合数据集规模不大的场景。训练结束后运行验证命令:
yolo detect val \ model=runs/seatbelt/weights/best.pt \ data=seatbelt.yaml值得注意的是,best.pt是按验证集 mAP 保存的,不是最后一个 epoch 的权重。如果数据集中夜间样本足够多,最好在训练前就把train和val都按白天/夜间分层切分,否则最优模型很可能在白天场景上过拟合。
4. 常见问题排查:安全带检测的四个翻车现场与解决办法
安全带检测模型跑通容易,跑好很难。下面四个问题是我在类似数据集上反复遇到的现象和解决路径,每一条都对应着具体的参数调整或流程改造。
4.1 现象:白天 mAP 0.96,夜间直接腰斩到 0.6
现象:模型在白天卡口图上表现很好,检测框又稳又准,但到夜间测试集上精度骤降,大量漏检。原因很直接:数据集中夜间样本占比过低,监控摄像头夜间补光差、车窗反光严重,安全带的纹理特征在低照度下几乎不可见,模型没有见过这类分布。
解决:分两步走。第一步是采集时按时间段分层,训练集里白天和夜间的比例至少要达到 3:1,而不是靠随机抽帧自然分布。第二步是增加夜间模拟增强,把白天样本降亮度、加噪声,扩出夜间风格的训练数据:
import cv2 import numpy as np def night_aug(img): # 模拟夜间:降亮度 + 高斯噪声,标注坐标不受影响 gamma = np.random.uniform(0.4, 0.7) table = np.array([ ((i / 255.0) ** gamma) * 255 for i in range(256) ]).astype("uint8") img = cv2.LUT(img, table) noise = np.random.normal(0, 6, img.shape).astype(np.int16) img = np.clip(img.astype(np.int16) + noise, 0, 255).astype("uint8") return img这段增强用cv2.LUT做 gamma 调暗,gamma 越小图像越暗;高斯噪声模拟摄像头在低照度下的感光颗粒感。注意增强要离线做还是在线做取决于管线,但千万不能改动标注框坐标,因为亮度变换不改变目标位置。
4.2 现象:安全带斜线特征被座椅纹理和衣物吞掉,出现大量短框、跳框
现象:检测框要么只框住安全带的一半,要么在座椅靠背的竖条纹上反复横跳。原因:安全带是长宽比很大的斜向目标,在卡口全景图里可能只有十几个像素宽,模型很难把这种细长特征从座椅纹理里区分出来。
解决:常见做法是做两级检测,先用通用人体检测模型把每个乘员裁出来,再在裁剪区域上做安全带检测。这样安全带在输入图像里的尺寸被放大,特征更容易被提取:
from ultralytics import YOLO person_model = YOLO("yolov8n.pt") # 使用 COCO 预训练权重 belt_model = YOLO("runs/seatbelt/weights/best.pt") img = cv2.imread("camera_frame.jpg") person_results = person_model(img, classes=[0], conf=0.4) for box in person_results[0].boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 = map(int, box.tolist()) crop = img[y1:y2, x1:x2] belt_results = belt_model(crop, conf=0.3) # 裁剪后的检测框坐标需映射回原图坐标系代码里classes=[0]是只保留 COCO 类别里的 person 类,过滤掉其他 79 类干扰。裁剪后输出的坐标是相对裁剪图的,要映射回原图就得在x1, y1基础上加回偏移量。做两级检测会牺牲一些速度,单卡处理帧率会下降,但精度提升非常明显,尤其适合卡口图片这种非实时批量处理的场景。
另一个思路是提高训练分辨率。安全带检测对细节敏感,直接用imgsz=960或1280训练,比在 640 下硬调 anchor 更有效。试过之后你会发现,细长目标的召回率提升通常比mAP提升更明显,因为很多漏检框本来就是“差一点”的状态。
4.3 现象:“无带”类样本太少,模型倾向全部输出“有带”
现象:验证集 mAP 看起来不错,但业务方说漏报太严重——没系安全带的人很多没被报出来。原因:数据采集时系带率本身就高,without_seatbelt的样本数量可能只有with_seatbelt的十分之一。模型在类别不平衡下会偏向多数类,把低置信度的“无带”目标也压下去。
解决:与其只调损失函数,我更建议先做简单的重采样。把已有的“无带”目标从标注框里裁剪出来,随机粘贴到原始图上没有目标干扰的区域,同时生成对应的标注行:
import cv2 import numpy as np import xml.etree.ElementTree as ET # 从标注里取出一个无带目标,旋转后粘贴到另一张图上 negative_crop = cv2.imread("negative_sample.jpg") h, w = negative_crop.shape[:2] alpha = 0.6 * (negative_crop > 20).astype(np.float32) # 简单前景掩膜 bg = cv2.imread("background.jpg") x_offset, y_offset = 100, 200 bg[y_offset:y_offset+h, x_offset:x_offset+w] = \ (1 - alpha) * bg[y_offset:y_offset+h, x_offset:x_offset+w] \ + alpha * negative_crop说明:这里的alpha只是演示用的粗略掩膜,实战会用 grabcut 或语义分割拿到更干净的前景。复制粘贴增强要注意两点:一是粘贴位置不能和已有目标重叠,否则标注框互相覆盖;二是同一张图粘贴的负样本数量控制在 3 个以内,太多会造成背景纹理异常,模型学到的不是“无带”而是“粘贴痕迹”。
此外,训练参数里可以适当提高box和cls损失权重,或者在推理时把置信度阈值调低,让更多低置信度“无带”框进入人工复核队列。这个后手很实用:机器抓不全,那就让机器把存疑的都交给人,人工只做确认,而不是从头看整张图。
4.4 现象:衣领、冬季外套和座椅遮挡导致“有带”被误判为“无带”
现象:冬天穿高领毛衣或者系了围巾的驾驶员,明明系了安全带,模型却输出“无带”。原因:安全带被衣物完全盖住,图像里根本没有可供判断的斜线特征。模型只能靠“这个人看起来像没系”的弱线索猜测,误判自然高。
解决:把“看不见”和“真的没系”区分开。常见做法是在数据集里增加一个“遮挡/未知”类别,或者在训练时把这类样本从“无带”类里剔除。安全带被完全遮挡时,标注成without_seatbelt会让模型学到错误关联,标注成ignore又会让该区域直接失去监督。折中方案是保留为独立类,让模型输出三种结果,推理端再根据业务规则处理:
with_seatbelt,置信度高于 0.5,直接视为已系;without_seatbelt,置信度高且目标完整,进入违法取证;blocked或者无输出,进入人工复核,不自动判定。
这个方案不提升检测 mAP,但能显著减少误报工单。我一直觉得目标检测落地追求的不只是框得准,更要让业务方能解释“为什么这样判”。遮挡样本单独成类,就是在给模型一个合法的表达出口,而不是逼它在看不清的时候硬猜。
5. 上线前的验证与阈值调优:把模型从“能看”变成“敢用”
验证集 mAP 0.93 不代表业务上敢用。目标检测模型上线前,我一般会专门花半天做一件事:在固定模型权重的情况下,对置信度阈值做扫描,看精确率和召回率的曲线,而不是盯着单个 mAP 数字做决定。
做法很简单:用验证集跑一轮推理,把每个目标的类别、置信度、坐标全部导出,然后从 0.05 到 0.5 按步长 0.05 调整置信度阈值,分别计算对应的精确率和召回率。表格列出来之后,业务方可以直观地看到:阈值设 0.4,误报率降低、漏报稍有上升;阈值设 0.25,漏报最低但误报也最多。对安全带检测来说,违法取证场景更怕误报——把系了安全带的人判成没系,会直接产生错误罚单。所以我一般把默认阈值定在 0.4 附近,并把置信度 0.25 到 0.4 之间的“无带”目标送到人工复核队列,由工作人员二次确认后再做处罚。这一套流程下来,系统吞吐能力没降多少,业务可信度却高了一个量级。
验证阶段还有一个容易忽略的环节:确认模型的输入尺寸和卡口原图的比例关系。很多安全带数据集的原图是 1920×1080,直接缩放到 640 会让安全带只有十几个像素宽。如果前面两级检测流程暂时排不上,至少要在推理时采用更大imgsz,并对检测结果做坐标映射回原图,保证取证截图标注的位置和实际位置一致。
我上一次部署这类模型时翻过车:模型在验证集上表现很好,上线第二天把出租车前挡风玻璃上的一道反光误判成了“无带”目标。排查时才发现,那道反光在缩小的输入图上有一条很锐利的斜向亮线,特征和安全带太像了。后来我把推理阈值调到 0.45,并加了“目标框必须落在乘员胸口到腹部区域”的位置约束,误报才真正降下来。这次教训之后,我养成了一个习惯:任何目标检测模型上线前,先花半天调阈值和复核流程,而不是急着换更大模型。模型结构决定精度上限,但决定业务能不能用的,往往是阈值策略和兜底机制。希望这篇能帮到你少走这一步弯路。
本文还有配套的精品资源,点击获取