
简介面向司机安全带检测任务的高速监控视角真实抓拍数据集包含1176张已标注图片覆盖日常驾驶中的常见场景适合YOLO全系列算法直接训练也可用于毕业设计、科研课题及落地项目。资源共2000个文件其中1123个xml标签对应VOC格式877个txt标签对应YOLO格式标注内容为safety_belt单一类别精准度较高且已划分好训练集、验证集和测试集无需额外转换即可投入训练。压缩包整体约999.98MB目前已有609人学习下载。依托真实监控视角与精细标注基于YOLOv9训练该数据集可达到94.2%的准确率既能帮助初学者快速上手目标检测流程也能为研究者提供可靠的算法验证基准。1. 安全带检测数据集1176张监控真实抓拍样本VOC与YOLO双标签开箱即用安全带检测这类任务真正卡人的往往不是模型选型而是监控视角下的人体、安全带状态和标签格式三者如何对齐。这份数据集提供的就是这个场景下的真实素材1176张高速公路监控抓拍图覆盖主驾驶和副驾驶位保留了真实抓拍里的逆光、夜间低照度和遮挡情况同时给出VOC格式的XML标签与YOLO格式的txt标签。两套标签都按目标检测的标准结构组织YOLO全系算法都能直接吃进训练流程不需要你再做坐标换算和格式转换。对正在做毕业设计、需要一套能讲清楚流程和结果的同学或是在企业里验证安全带识别落地效果的工程师这套资源的直接价值是把最磨人的数据准备阶段压缩到解压即用。下面从标签格式、转换脚本、训练参数到常见翻车点完整拆开讲一遍。2. VOC与YOLO两套标签坐标体系、目录结构与转换脚本2.1 VOC标签的XML结构annotation如何描述一个目标VOC标注的核心是XML文件根节点annotation下挂着一串object节点每个object描述画面里的一个目标。安全带检测场景中object里的name写的是类别名bndbox存目标框的绝对像素坐标单位就是图像像素不经过任何归一化。打开一个典型的XML标注文件结构长这样annotation folderJPEGImages/folder filenamehighway_0012.jpg/filename size width1920/width height1080/height depth3/depth /size object nameperson_with_seatbelt/name bndbox xmin423/xmin ymin305/ymin xmax612/xmax ymax712/ymax /bndbox /object object nameperson_without_seatbelt/name bndbox xmin890/xmin ymin420/ymin xmax1075/xmax ymax766/ymax /bndbox /object /annotation真正影响训练结果的只有三个信息filename决定图片对应关系name决定类别bndbox决定回归目标。size下的width和height在从VOC转YOLO时格外重要归一化坐标必须用真实图片尺寸不能用固定值去凑。这类数据集的标签命名有几套常见习惯一种直接区分person_with_seatbelt和person_without_seatbelt另一种只标person再把安全带单独框出来。两种思路各有适用场景但监控视角下安全带经常被方向盘或B柱挡住单独框安全带很容易标成零碎小框训练时噪声很大。我一般优先选第一种也就是把人作为目标、状态写在类别里。2.2 YOLO标签的txt格式class_id加归一化坐标YOLO格式每个图像对应一个同名txt文件每一行描述一个目标。格式为class_id x_center y_center width height其中坐标全部除以图像宽高归一化到0到1之间0 0.269531 0.469343 0.098437 0.377953 1 0.511719 0.549119 0.096354 0.280741第一行含义是类别0中心点在图像横向26.9%的位置纵向46.9%的位置框宽为图像宽度的9.8%高为图像高度的37.8%。这个坐标系和VOC完全是两种语义VOC存的是左上角和右下角的绝对像素值YOLO存的是中心点和宽高的相对值。两类标签的差异用一张表看最快对比项VOC格式YOLO格式文件格式XMLtxt坐标单位绝对像素归一化到0~1坐标语义左上角点右下角点中心点宽高每行对应一个object节点一个目标额外配套无需要类别顺序文件很多新手直接把XML里的xmin、ymax拿来除以图像宽高凑数结果训练出来的框全部偏到图像边缘。因为中心点的x是(xminxmax)/2再除以width框宽是(xmax-xmin)再除以width角点和中心点混用必翻车。2.3 VOC转YOLO一个可直接运行的批量转换脚本拿到VOC标签后第一步是转成YOLO格式。我一般写一次性脚本处理整个Annotations目录不手工改任何XMLimport os import xml.etree.ElementTree as ET from PIL import Image # 类别名到class_id的映射顺序必须和数据集的names保持完全一致 class_map { person_with_seatbelt: 0, person_without_seatbelt: 1, person: 2, } voc_dir VOCdevkit/VOC2007/Annotations img_dir VOCdevkit/VOC2007/JPEGImages out_dir yolo_labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(voc_dir, xml_file)) root tree.getroot() # 用PIL读真实图片尺寸做归一化防止摄像头分辨率不一致导致坐标错位 img_path os.path.join(img_dir, root.find(filename).text) with Image.open(img_path) as img: w, h img.size lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # VOC角点坐标换算成YOLO中心点宽高全部除以真实图像尺寸 x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) out_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines))脚本逻辑分三段先建立类别名到数字ID的映射再用PIL读原图真实宽高最后把VOC角点坐标换算成YOLO中心点加宽高。class_map的顺序就是之后训练时data.yaml里names的顺序两处必须一致否则会出现训练正常、推理时类别混乱的怪问题。需要注意的是w和h来自PIL读取的当前图片尺寸而不是XML里记录的size。监控摄像头的分辨率可能随存储策略变化XML里的size偶尔和实际图像不一致以实际读取为准最保险。另外文件名通过os.path.splitext取同名前缀保证image和label一一对应转换完后原XML不要删留着做可视化核对。2.4 转换完成后的质量自检可视化画框与坐标分布标签转换完不要急着开train先在命令行挑十几张图把框画出来对比原图看框是否贴住身体上半部分。用OpenCV做可视化很快import cv2 img_path highway_0012.jpg label_path highway_0012.txt class_names [person_with_seatbelt, person_without_seatbelt, person] img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f.readlines(): cid, xc, yc, bw, bh map(float, line.strip().split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cid)], (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(check_visualization.jpg, img)这段代码把归一化坐标乘回图像宽高还原成像素坐标后画框。检查时重点看两类问题框是不是明显偏离目标主体以及安全带的框是否框住了相邻乘员。监控场景常有反光安全带带体可能断成两段如果框只框住其中一段说明原始标注存在截断问题要先修标签再进训练。除了可视化还可以统计所有txt里坐标的分布范围。比如检查所有目标的中心点x是否都落在0到1之间如果出现负数或大于1的值几乎可以断定转换脚本里分子分母用反了。3. 跑进YOLOv8训练流程数据划分、YAML配置与超参数调法3.1 先按YOLO目录规范整理train和valYOLOv8默认约定数据集根目录下分images和labels两个大目录各自再分train和val。先用一条命令把骨架建好mkdir -p datasets/seatbelt/{images/{train,val},labels/{train,val}}目录建好后把JPEGImages里的图片按比例复制进images/train和images/val对应txt同步复制进labels目录。划分脚本如下import os import random import shutil random.seed(42) # 固定随机种子保证每次划分结果一致 image_src JPEGImages label_src yolo_labels data_root datasets/seatbelt all_images [f for f in os.listdir(image_src) if f.endswith(.jpg)] val_count int(len(all_images) * 0.2) val_images set(random.sample(all_images, val_count)) for img in all_images: base os.path.splitext(img)[0] split val if img in val_images else train img_dst_dir os.path.join(data_root, images, split) label_dst_dir os.path.join(data_root, labels, split) os.makedirs(img_dst_dir, exist_okTrue) os.makedirs(label_dst_dir, exist_okTrue) shutil.copy(os.path.join(image_src, img), os.path.join(img_dst_dir, img)) label_file base .txt label_src_path os.path.join(label_src, label_file) if os.path.exists(label_src_path): shutil.copy(label_src_path, os.path.join(label_dst_dir, label_file))代码里random.seed(42)这行很关键不固定随机种子的话每次跑出来的训练集和验证集都不一样实验结果无法复现。1176张图按8:2划分大约得到940张训练图和236张验证图这个规模跑YOLOv8的nano模型足够。3.2 编写data.yaml路径、类别数与类别名的对应关系YOLOv8用yaml文件描述数据集路径、类别数量和类别名。在数据集根目录创建seatbelt.yamlpath: /data/datasets/seatbelt train: images/train val: images/val nc: 3 names: 0: person_with_seatbelt 1: person_without_seatbelt 2: personpath填写数据集根目录的绝对路径train和val是相对path的路径。nc必须等于names列表的长度names的index必须和标注txt里的class_id严格对齐。这里最容易出的问题就是类别顺序错位。假设txt里class_id 0是person_with_seatbelt但yaml里names[0]写成了person_without_seatbelt训练不会报错loss该降还是降最后推理时所有类别都刚好反着。检查方法很简单——打开一个txt文件看第一行第一个数字再去yaml里数一下对应序号是什么类别。3.3 训练命令与超参数imgsz、batch、freeze怎么搭配数据准备好之后直接用YOLOv8命令行起训练yolo detect train dataseatbelt.yaml modelyolov8n.pt epochs100 batch16 imgsz640 device0 patience15几个参数按实际场景调整modelyolov8n.pt用COCO预训练权重做迁移学习1176张的规模不适合从零训练imgsz640是常规监控画面的默认值如果目标普遍小于32像素建议拉到960显存占用会同步上涨batch16在8G显存配imgsz640时勉强够用拉960就要降到8patience15表示验证指标连续15轮不涨就早停避免无效等待。也可以用Python脚本方式启动训练方便动态调参和加回调from ultralytics import YOLO model YOLO(yolov8n.pt) model.train( dataseatbelt.yaml, epochs100, imgsz640, batch16, freeze10, nameseatbelt_run1, )freeze10表示冻结模型前10层梯度只更新后半段和检测头。小数据集配上迁移学习时这个参数能明显加快收敛如果验证集mAP不涨把freeze去掉再对比一轮。3.4 样本不平衡系带样本远多于未系带时的处理安全带检测最常见的分布问题是系带样本远多于未系带样本。如果未系带类别占比不到10%模型大概率会被带偏成“看到人就预测系带”。常见做法是给少类做过采样把未系带的图像复制两到三份混进训练集或者对少类样本做mosaic和mixup增强。我一般会先统计各类别目标数量再决定增强策略import os from collections import Counter label_dir datasets/seatbelt/labels/train counter Counter() for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f)) as fp: for line in fp: cid int(line.split()[0]) counter[cid] 1 print(counter)统计结果如果显示class 1的数量不到class 0的三分之一优先做过采样而不是硬调损失函数权重。盲调损失权重经常让模型在验证集上表现飘忽过采样更直观可控。4. 避坑手册标签错位、坐标偏移、空标签与类别失衡4.1 类别标签错位txt里的数字和yaml里的names对不上现象训练损失正常收敛推理时系带和未系带的预测结果经常互相换同一个检测框在相邻两帧里被打上不同类名。原因标注txt中的class_id和data.yaml里的names顺序不一致。比如txt中class_id 0对应person_with_seatbelt但yaml里names[0]写的是person_without_seatbelt模型一直在学习一个错乱的映射关系。解决先统计训练集txt里出现过的最大class_id必须小于nc。再抽查几个不同摄像头机位的txt确认数字对应的类别名最后统一yaml的names顺序。4.2 来自VOC转YOLO的坐标偏移框整体偏位现象画框检查预览一切正常训练后检测框整体向左上偏或框比目标小一圈置信度还很高。原因转换时用了XML里记录的size而不是图片实际尺寸。监控摄像头可能中途调整过分辨率XML的size和实际图像宽高对不上导致归一化坐标整体错位。解决转换脚本里强制用PIL读取每张图片的真实宽高。转换完成后统计所有txt的中心点x和y分布正常应该均匀散布在0到1区间内如果全部集中在某个小范围说明坐标换算出了系统性错误。import numpy as np centers_x, centers_y [], [] label_dir yolo_labels for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f)) as fp: for line in fp: parts line.strip().split() centers_x.append(float(parts[1])) centers_y.append(float(parts[2])) print(x范围:, np.min(centers_x), np.max(centers_x)) print(y范围:, np.min(centers_y), np.max(centers_y))这个脚本跑完只要看到x或y的范围明显偏离0到1说明标签文件本身有问题千万不要带着这种数据进训练。4.3 空标签文件导致训练质量下降现象训练日志里出现多个空标签警告某些epoch的loss值突变最终mAP忽高忽低。原因部分图像没有任何目标标注可能原标注就没标或者在转换时被过滤掉了。YOLO训练时这些图片虽然不报错但等于在样本分布里掺入了无监督噪声。解决遍历labels目录找出内容为空的txt同步删掉对应的图片或者补上合理标注。empty_files [] for f in os.listdir(label_dir): path os.path.join(label_dir, f) if os.path.getsize(path) 0: empty_files.append(f) print(空标签数量:, len(empty_files)) print(empty_files[:10])4.4 未系带类别recall过低类别不平衡的方向性偏移现象整体mAP看着还行但抽查发现未系带类别的recall只有0.3预测结果几乎全落到系带类别上。原因高速监控里系带是常态未系带是少数模型学会了偏向高频类别。mAP被高频类拉高掩盖了低频类的失效。解决对少类样本做针对性过采样把未系带图片复制进训练集或对包含未系带目标的小区域做裁剪增强。我一般会把少类数量补到多类的一半以上再重新看class-wise的mAP。4.5 混淆矩阵行列总和不是1别急着怀疑标注现象训练结束后打开confusion_matrix.png把单元格数值当百分比求和发现某一类行加起来超过100%另一类又不足100%怀疑是标签或算法出问题了。原因YOLO系列输出的混淆矩阵默认包含背景类且数值做过列归一化处理单元格表达的是该类目标被预测成各别类的比例行列求和不等于1是正常现象跟标注质量没有直接关系。解决不看总和只看两类迁移方向。重点观察person_without_seatbelt被预测成person_with_seatbelt的比例如果偏高说明模型对安全带状态的分辨能力不足需要增加难例或调整类别权重。5. 训练结果验证读PR曲线、混淆矩阵与置信度阈值判断模型是否可用5.1 训练产物里哪些文件值得重点看YOLOv8训练结束后会在runs/detect/exp目录下生成weights/best.pt、weights/last.pt、PR_curve.png、confusion_matrix.png和results.csv。我第一眼永远看results.csv的最后几行。results.csv里最关心的四列是metrics/mAP50(B)、metrics/mAP50-95(B)、metrics/precision(B)和metrics/recall(B)。如果val的mAP50已经不再上涨而val损失还在持续下降说明模型开始过拟合早停机制通常会在这个节点主动截断。如果mAP和loss同时震荡不平多半是batch太小或学习率偏高需要降低学习率重新跑。5.2 在测试集上跑推理置信度阈值怎么设训练完用best.pt对测试图做推理yolo detect predict modelbest.pt sourcetest_imgs/ imgsz640 conf0.25 saveTrueconf0.25表示只保留置信度大于25%的检测框。安全带检测场景我建议conf设0.2甚至0.15原因在下一节讲。saveTrue会把标注了框的结果图存到runs/detect/predict目录方便直接目检。推理结果如果出现大量重复框说明NMS阈值需要调整如果远距离小目标完全没有检出多半是imgsz偏低或数据里小目标占比太少。5.3 mAP50与mAP50-95安全带项目的取舍逻辑mAP50指的是IoU阈值0.5时的平均精度mAP50-95是把IoU从0.5到0.95分档计算后再平均。安全带目标在监控画面里属于中等偏小目标mAP50到0.85以上基本可用mAP50-95在0.5到0.7之间都算正常不要拿0.95档的标准去苛求这个场景。这里最关键的一点安全带检测的验收优先看未系带类别的recall而不是整体mAP。漏掉一个未系带的人比误报一个系带的人后果严重得多。所以在调推理阈值时我习惯把recall拉高哪怕precision低一点也可以接受。这个取舍在项目演示效果上也更站得住。5.4 误报与漏报的取舍置信度和NMS阈值联动调整监控场景的安全带检测宁可多报几个疑似未系带也不要把真正未系带的漏掉。实际操作中我把conf从0.25一路降到0.15每次降完在验证集上重新推理对比recall的提升幅度和误报数量。recall明显上升而误报增加不多就继续降recall平坦或误报暴增就停在这个阈值附近。NMS阈值一般保持默认但如果同一辆车上主副驾两个目标重叠严重可以考虑把NMS的IoU从0.45略微降到0.4减少相邻乘员区域的框被合并掉的情况。这种细调属于经验活没有固定标准每次改动都跑一遍验证集看指标再决定。6. 用置信度统计反向修正标签一份实测可用的校验脚本训练完第一轮先别急着调参先用一个技巧把标签问题筛出来对全部训练样本做一次推理按置信度从低到高排序低置信度样本里混着大量标注错误。这个方法帮我解决过一次很玄学的问题——某个数据集mAP卡在0.72上不去损失曲线也正常后来用这个方法筛出30张低置信样本人工复核发现19张的未系带标签标成了系带清洗后mAP直接跳到0.83。校验脚本的核心是这样一段逻辑from ultralytics import YOLO model YOLO(runs/detect/seatbelt_run1/weights/best.pt) results model.predict(sourcedatasets/seatbelt/images/train, imgsz640, conf0.15, saveFalse) suspicious [] for r in results: img_path r.path boxes r.boxes for box in boxes: conf box.conf.item() cls int(box.cls.item()) if conf 0.4: suspicious.append((img_path, cls, round(conf, 3))) suspicious.sort(keylambda x: x[2]) print(低置信度样本数:, len(suspicious)) for item in suspicious[:30]: print(item)脚本把置信度低于0.4的检测结果全部抽出来按置信度升序排列。人工看这些低置信样本时如果发现“模型实际检出了目标但标签类别和模型预测类名对不上”或者“目标本身就标错了位置”那问题多半不来自模型而是来自标签。这套流程后来被我固定成习惯拿到任何新的目标检测数据集第一步先跑一轮短训练第二步用上述脚本刷低置信度样本第三步人工过一遍再决定是否动标签。从那以后我几乎没再遇到过训练完发现标签全错、回头重跑整个流程的情况。希望帮到你。本文还有配套的精品资源点击获取