简介:面向智能安防、机房值班室及远程监控场景的睡岗检测数据集,采用VOC格式标注,覆盖趴桌睡、埋头睡、靠椅睡、平躺睡等多种典型睡姿,适用于目标检测、姿态识别和异常行为分析等任务。包内共2000个XML标注文件,对应6549张图片,压缩后约422.3MB;标注文件按原始图片名一一命名,包含类别与边界框信息,可直接接入YOLO、Faster R-CNN等常见检测框架进行训练和评估。目前已有936人学习下载,主要面向计算机视觉算法工程师、安防系统开发者及高校相关专业学生。借助这批规范数据,可快速搭建睡岗检测模型,区分不同睡眠姿态,并通过数据增强、难例挖掘等手段提升模型在夜间、强光或遮挡条件下的鲁棒性;同时,清晰的VOC标注结构也便于二次改造为COCO或YOLO格式,为后续告警联动、考勤统计等上层应用提供基础数据集支持。
1. 睡岗检测数据集:6549 张 VOC 标注图能帮你把值班室监控从「人盯屏」变成「算法盯屏」
值班室监控有个老问题:再敬业的保安也没法全天盯住七八块屏幕。睡岗检测要做的事,就是把「画面里有没有人在睡觉」交给算法判断——有人趴在桌上、埋头打盹、靠着椅背睡或者直接躺平时自动报警。标题里这个数据集正好覆盖这四种睡姿,6549 张 VOC 格式标注图,属于"yolov8训练自己的数据集"场景里少见的对口数据,比从 COCO 里挑人形再标注要省得多。
它适合做智慧工厂、变电站、机房、保安岗值班监控的工程师。通用目标检测数据集里的样本是车、人、猫狗,而这里每一张图的标注目标都是真正要报警的睡姿,场景和业务完全一致,拿来可以直接进训练链路。
下面按一套完整落地流程讲:先看懂 VOC 标注本身,再把 XML 转成 YOLO 能吃的 txt,然后是训练参数与指标验证,最后是五条会让你翻车的坑和上线前的验证方法。目标只有一个:让你拿到这个数据集就能复现出一条可用的睡岗检测模型。
2. 拆解 VOC 标注与四种睡姿:先搞清楚数据集里有什么,再谈训练
2.1 VOC 标注格式与目录结构:XML、JPG 和标签映射
VOC(PASCAL VOC)是最老牌的目标检测标注格式,它的规则是「一张图配一个同名 XML」。图片统一放JPEGImages,标注放Annotations,这是几乎所有 VOC 数据集的标准目录组织方式,标题里这个 6549 张的数据集也沿用这套结构。不管哪个版本,XML 里真正要关心的核心字段就下面这些:
<annotation> <filename>0345.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>desk_sleep</name> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>520</xmin> <ymin>310</ymin> <xmax>890</xmax> <ymax>640</ymax> </bndbox> </object> </annotation>逻辑说明:size里的宽高是坐标归一化的分母,训练时靠它把绝对像素坐标换算成 0~1 的相对值;object可以出现多次,一张图里有两个人睡就有两个<object>;bndbox的四个值分别是左上角 x、左上角 y、右下角 x、右下角 y。参数说明:difficult这个字段经常被忽略,它的含义是「这个框遮挡严重、标注者自己都没把握」,训练时建议过滤,验证时保留,能更真实地反映模型在困难样本上的表现。
另外,XML 里偶尔还有pose、segmented这些字段,睡岗检测是纯框任务,用不上,解析时直接跳过即可。还有一个容易看漏的点:有些 VOC 数据集会自带ImageSets/Main下的 train.txt、val.txt,如果你拿到的版本里有,划分数据集时优先用官方划分,别自己重抽。
2.2 四种睡姿的类别定义与样本分布
标题里面提到的四种睡姿,放在监控画面里对应的视觉特征差别很大,这也决定了模型能不能把它们分开:
| 睡姿 | 画面里的视觉特征 | 常见误判 |
|---|---|---|
| 桌子上趴着睡 desk_sleep | 头脸完全趴在桌面上,手臂前伸或交叉,面部不可见 | 和埋头睡混淆 |
| 埋头睡觉 head_down | 坐姿,头低垂到胸前,面朝下,肩背呈弓形 | 被当成正常低头看文件 |
| 座椅上靠着睡 lean_sleep | 头后仰或侧靠椅背,面部基本可见,眼睛闭合 | 被当成正常靠坐休息 |
| 平躺着睡 lying_sleep | 在沙发、行军床上平躺或侧躺,全身可见 | 被当成空床或蹲着 |
我见过有人直接拿「人」检测模型来检测睡岗,结果惨不忍睹——正常坐着的人全被框出来报警,完全没法用。原因就是普通行人检测关注的是「人形」,而不是「姿态」。而这四类睡姿里,lean_sleep是最难的一类,它的姿态和正常靠坐高度相似,只靠目标框根本分不开,需要模型学到头部后仰角度和躯干放松程度这些细粒度特征。
样本分布上,这类值班室数据集通常趴桌睡和埋头睡数量多,躺睡因为现场床位机位少,框数明显偏少。6549 张图并不等于 6549 个目标,一张宽景画面里同时出现两个睡岗人员很常见,所以统计必须以「目标数」为单位,下面这个脚本就是干这个的。
2.3 用脚本统计标签分布:先看类别均衡再谈训练
import os import xml.etree.ElementTree as ET from collections import Counter def analyze_voc(xml_dir): cls_count = Counter() # 每个类别的目标总数 diff_count = Counter() # 每个类别的 difficult 数量 aspect_ratios = [] # 所有框的宽高比 for name in os.listdir(xml_dir): if not name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, name)) root = tree.getroot() size = root.find("size") w, h = int(size.find("width").text), int(size.find("height").text) for obj in root.findall("object"): label = obj.find("name").text.strip() diff = obj.find("difficult") if diff is not None and int(diff.text) == 1: diff_count[label] += 1 continue cls_count[label] += 1 box = obj.find("bndbox") xmin = float(box.find("xmin").text) xmax = float(box.find("xmax").text) ymin = float(box.find("ymin").text) ymax = float(box.find("ymax").text) aspect_ratios.append(((xmax - xmin) / w) / ((ymax - ymin) / h)) return cls_count, diff_count, aspect_ratios counts, diffs, ars = analyze_voc("Annotations") for label, n in counts.most_common(): print(f"{label}: {n} 个目标 / difficult {diffs.get(label, 0)} 个") print("框宽高比中位数: %.2f" % sorted(ars)[len(ars) // 2])逻辑说明:脚本先把每张图的size读出来作为归一化基准,再逐个object统计标签名和 bndbox,difficult=1的框单独计数、不进训练统计。参数说明:xml_dir要指向 Annotations 目录;如果 XML 里的标签是中文,label打印出来就是中文,不影响统计,但后面转格式时必须先做映射。
这个脚本的输出直接决定三件事:类别严重不均衡时给少数类加权或复制增强;宽高比分布极端说明画面里躺睡占比高,输入分辨率策略要调整;difficult 占比高则说明标注噪声大,训练时要考虑加 label smoothing。我一般拿到任何数据集第一件事就是跑它,跑完才开始调参数。
3. 把 VOC 转成 YOLO 格式:转换脚本与四个边界坑
3.1 坐标归一化与类别映射:XML 转 TXT 的标准做法
YOLOv5 和 YOLOv8 训练时要求标签是「每张图一个同名 txt」,每一行对应一个目标,格式固定为:class_id x_center y_center width height,后四个值都是相对图片宽高归一化的 0~1 小数。VOC 的 bndbox 是绝对像素坐标,所以转换本质上只有两件事:坐标归一化、类别字符串转数字 ID。
类别映射先定死,后面所有环节都用这张表:
| 数据集标签(以英文为准) | 对应睡姿 | YOLO class_id |
|---|---|---|
| desk_sleep | 桌子上趴着睡 | 0 |
| head_down | 埋头睡觉 | 1 |
| lean_sleep | 座椅上靠着睡 | 2 |
| lying_sleep | 平躺着睡 | 3 |
实际拿到手的 XML 里标签名可能是中文或别的写法,所以脚本里维护一个NAME_TO_ID字典,不要硬编码顺序,转换前先打开一个 XML 确认 name 字段的真实值。
import os import xml.etree.ElementTree as ET NAME_TO_ID = { "desk_sleep": 0, "head_down": 1, "lean_sleep": 2, "lying_sleep": 3, # 如果标签是中文,在这里补映射: "趴桌睡": 0, "埋头睡": 1 ... } def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in NAME_TO_ID: continue difficult = obj.find("difficult") if difficult is not None and int(difficult.text) == 1: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 边界保护:标注偶尔会超出图片范围,先 clamp 再判断有效性 xmin = max(0.0, min(xmin, img_w)) xmax = max(0.0, min(xmax, img_w)) ymin = max(0.0, min(ymin, img_h)) ymax = max(0.0, min(ymax, img_h)) if xmax <= xmin or ymax <= ymin: continue cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{NAME_TO_ID[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) xml_dir = "Annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for fn in os.listdir(xml_dir): if fn.endswith(".xml"): voc_to_yolo(os.path.join(xml_dir, fn), os.path.join(out_dir, fn.replace(".xml", ".txt")))逻辑说明:脚本先读size作为归一化分母,再遍历所有object,difficult=1的框直接跳过不写进训练标签;所有坐标先 clamp 到图片范围内,再判断宽高是否有效,无效框丢弃。参数说明:NAME_TO_ID的映射必须和后面 data.yaml 的names顺序完全一致,顺序错一位,训练出来的所有类别预测就整体错位,这是最隐蔽的坑之一。txt 里保留 6 位小数足够,不需要更高精度。
3.2 训练/验证集划分与数据组织
YOLOv8 默认的目录结构是datasets/场景名/images/train、images/val、labels/train、labels/val。转换完的标签要和图片按文件名一一对应,然后做随机划分。
import os import random import shutil random.seed(42) # 固定种子,保证每次划分结果可复现 img_root = "JPEGImages" label_root = "labels" train_img = "datasets/sleep/images/train" val_img = "datasets/sleep/images/val" train_lab = "datasets/sleep/labels/train" val_lab = "datasets/sleep/labels/val" for d in [train_img, val_img, train_lab, val_lab]: os.makedirs(d, exist_ok=True) imgs = [f for f in os.listdir(img_root) if f.lower().endswith((".jpg", ".png", ".jpeg"))] random.shuffle(imgs) val_count = int(len(imgs) * 0.15) val_files = set(imgs[:val_count]) for img in imgs: label = img.rsplit(".", 1)[0] + ".txt" if not os.path.exists(os.path.join(label_root, label)): print("无标签文件:", img) continue if img in val_files: shutil.copy(os.path.join(img_root, img), os.path.join(val_img, img)) shutil.copy(os.path.join(label_root, label), os.path.join(val_lab, label)) else: shutil.copy(os.path.join(img_root, img), os.path.join(train_img, img)) shutil.copy(os.path.join(label_root, label), os.path.join(train_lab, label))逻辑说明:按图片划分而不是按目标划分,避免同一张图里的多个目标同时出现在训练和验证集里造成数据泄漏。参数说明:val_ratio取 0.15,6549 张图大约分 980 张做验证,对检测任务足够;random.seed(42)必须放在 shuffle 前面,否则每次运行结果不同,后续复现对比会很痛苦。
提示:如果数据集自带
ImageSets/Main/train.txt和val.txt,直接读取文件列表做划分,比随机抽更接近发布方的原始设定。验证集原则上不动,训练集后续增删样本都不影响对比基线。
3.3 转格式时最容易翻车的四个边界坑
先说最容易遇到的坐标越界。有些标注框是标注人员在图上手工拖出来的,放大缩小时偶尔会拖出图片边缘,导致xmax比图片宽度还大。转换时不 clamp,归一化后框宽超过 1,训练 loss 会异常,甚至会出 NaN。解决方式见上面的脚本注释,clamp 后再判断xmax <= xmin直接丢弃。
第二个是空 txt。部分 XML 里没有任何object,转换后生成 0 字节 txt。YOLOv8 训练时能容忍这种背景图,但老版本 YOLOv5 偶尔会报 "No labels found in..." 的警告。稳妥做法是把空 txt 挪到独立目录,训练时只放有标注的部分,避免干扰。
第三个坑是同名图片覆盖。如果原始数据不是单一 JPEGImages 目录,而是按场景分了子目录,不同子目录里可能有同名 JPG,直接复制到统一目录后标签和图片会配错。解决方法是复制时用xml 文件名 + 序号重新命名,保证全局唯一,我当时就是没检查重名,训练完发现验证集 mAP 诡异,查了半天才发现是一张图片被另一张覆盖了。
第四个是中文标签与编码问题。Windows 下读中文 label 的 XML 容易遇到编码不一致,name字段读出来乱码,映射字典匹配不上,目标被全部跳过,训练集显示 0 个标签。解决:统一在NAME_TO_ID里做中文到英文的映射,脚本用ET.parse默认的编码读取,文件保持 UTF-8,不要在转换脚本里混用 GBK。
4. 用 YOLOv8 训练睡岗检测模型:参数设置与效果验证
4.1 数据配置 YAML 与训练命令
处理完标签,下一步就是配置数据描述文件。YOLOv8 用 YAML 告诉训练器「数据在哪、类别叫什么」,注意names的顺序必须和第三章的NAME_TO_ID完全一致:
# sleep.yaml path: /data/sleep_detection # 数据根目录,建议绝对路径 train: images/train val: images/val names: 0: desk_sleep 1: head_down 2: lean_sleep 3: lying_sleep逻辑说明:path写绝对路径最省事,相对路径容易和 Ultralytics 的工作目录搞混;train和val是相对path的目录。参数说明:names不能写成列表之外的格式,这个字段在 Ultralytics 8.x 里被解析成类别名索引,如果类别数对不上会直接报错。
训练命令:
yolo detect train \ data=sleep.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ patience=15 \ project=run_sleep \ name=exp1参数说明:model=yolov8n.pt会加载 COCO 预训练权重做迁移学习,不是从头训练,这能显著缩短收敛时间,也决定了你对检测任务的理解能复用多少;patience=15表示连续 15 个 epoch 验证集指标不提升就早停,对 6549 张图的数据集来说非常实用;device=0指定第一块 GPU,没有 GPU 时改成device=cpu,但 100 轮会非常慢,建议先用小数据集试跑。
4.2 关键参数怎么调:imgsz、batch、epochs、模型尺度
imgsz是睡岗检测里最值得花时间调的参数。默认 640 是速度和精度的平衡点,但这个数据集的监控画面里,远端机位的人体可能只有几十像素高,640 输入下细节丢失严重。如果发现小目标漏检,把imgsz提到 960 或 1280,mAP50 通常能涨 2~5 个点,但推理时间约按分辨率平方增长。要部署在 CPU 盒子上就先 640,别贪。
batch受显存限制。8G 显存跑yolov8n加imgsz=640可以用 16,换yolov8s或imgsz=1280就要降到 4~8。batch 太小(2~4 时)BN 统计不稳定,loss 曲线会明显抖动,这时候先降分辨率或换小模型,而不是硬扛大 batch。
epochs和patience是一对搭档。6549 张图属于小数据集,100 轮足够,关键是有 patience 帮你自动停。如果 60 轮就因为早停结束,去看run_sleep/exp1/results.csv里 val 指标是不是已经平台期,是的话直接采用这版就行,不用非跑满 100 轮。
模型尺度选择上有个血泪经验:别一上来就上yolov8m或yolov8l。睡岗检测场景单一、目标是大尺度的人体,yolov8n和yolov8s的效果差距没有想象中大,但推理速度差距很大。服务器部署选 s,边缘盒子选 n,先跑通再升级。
4.3 验证指标怎么读:mAP50 与召回优先的评估口径
训练结束后不要只盯着 mAP 一个数字。睡岗检测的业务口径是「别漏」,漏一次就是一次真实睡岗事故,所以评估时要优先看 recall。正常情况下,这套数据训出来的模型应该达到:mAP50在 0.85 以上、recall@0.5在 0.9 以上才值得上线。如果 recall 上不去,问题基本出在类别混淆或小目标,而不是模型容量。
指标文件都在run_sleep/exp1/目录下。results.png看整体趋势,confusion_matrix.png看具体哪些类别互相错检,F1_curve.png会告诉你哪个置信度阈值下 F1 最高。我一般先看混淆矩阵,再决定要不要合并类别,最后才调阈值,顺序反了容易被整体指标带偏。
还有一个容易被忽略的检查点:val_batch*_pred.jpg的可视化图。打开它看预测框是不是贴住人的躯干和头部,而不是套住一整张桌面或椅子。框太松说明定位没学好,框太紧可能漏了垂下的手臂,这些是 mAP 数字体现不出来的质量问题。
5. 睡岗检测模型训练避坑指南:五个常见问题与排查记录
5.1 现象:训练到 40 轮左右 loss 突然变成 NaN
原因:数据里存在极窄或极宽的异常标注框,或者某张图片实际尺寸和 XML 里记录的size不一致。比如原始图被旋转后重新保存,宽高对调,归一化分母用错,坐标就会飞到离谱范围,梯度爆炸直接把 loss 打成 NaN。
解决:转换前全量校验一次图片和标注。用cv2.imread读每张图拿到真实宽高,和 XML 里的width/height对比,不一致的直接剔除;同时遍历 bndbox,宽或高小于 1 像素的框删掉。这一步听着繁琐,但能省掉后面一整天的排查时间。
5.2 现象:趴桌睡和埋头睡互相错检严重
原因:这两类的语义边界本来就模糊。头低垂到胸口算埋头,头完全趴到桌面算趴桌,不同标注人员对「趴」的理解不一致,导致同类样本的视觉分布很散。训练出来模型在两个类之间反复横跳。
解决:先看混淆矩阵里这两个格子的具体数值。如果错检率超过 20%,直接把desk_sleep和head_down合并成一个大类「坐姿瞌睡」,重新训练。睡岗检测的目的是报警,不是学术上的细粒度分类,少一个类往往能换回 2~3 个点的 mAP 和明显更低的误报率。这个决定越早做越好。
5.3 现象:监控远端的小目标漏检,特别是靠椅睡
原因:摄像头对着整个值班室,坐在远处的人可能只有 30~40 像素高。imgsz=640输入下缩得更小,模型学不到足够的人体细节。这种情况在验证集里表现为「近处全检出来,远处全漏」。
解决:imgsz提到 960,训练时保持默认 mosaic 增强,能显著改善小目标;推理时也不要先把视频流压到 640 再送模型,按原分辨率缩放。如果边缘设备撑不住高分辨率,另一个思路是在人员常驻区域加一路近景摄像头,用小分辨率的近距离画面做二次确认,比硬怼分辨率划算。
5.4 现象:夜间和背光场景误报率明显升高
原因:数据集样本以白天为主,夜里显示器屏光、走廊背光会把画面亮度压得很低,模型把椅子靠背纹理、桌面反光当成目标。整体 mAP 看着不错,但夜间的 precision 惨不忍睹。
解决:做离线增强补夜间分布。随机 gamma 从 0.5 到 1.5、亮度抖动 0.2、加轻度高斯噪声,都是模拟监控夜视的常用手段。增强后单独统计夜间子集的召回率和误报率,不要只看整体指标。如果现场能抽到夜间真实录像,直接抽帧回填训练集是最靠谱的方案。
5.5 现象:训练指标不错,部署到盒子上只有 3~5 帧
原因:imgsz用了 1280,或者模型选了 m/l,边缘 CPU 或 NPU 根本吃不消。这是最常见的「实验室满意、现场翻车」案例。
解决:部署侧先用yolo export model=best.pt format=onnx导出,再转 OpenVINO 或 TensorRT;imgsz降到 640,能接受再降就做 int8 量化。睡岗报警本身允许 2~3 秒延迟,帧率掉到 8~10 也能用,关键是别在最大分辨率上死磕。导出前用一个真实视频片段测端到端延迟,别只看单张推理时间,框后处理和数据前后编解码都会吃掉时间。
6. 让睡岗检测模型更抗造:数据增强、阈值选择与上线验证
6.1 用 Ultralytics 内置增强参数模拟监控场景
训练命令里可以直接叠加增强参数,不需要改代码:
yolo detect train \ data=sleep.yaml \ model=yolov8n.pt \ epochs=100 imgsz=640 batch=16 device=0 \ hsv_h=0.01 hsv_s=0.3 hsv_v=0.4 \ degrees=0 translate=0.1 scale=0.2 mosaic=1.0参数说明:hsv_v=0.4是亮度抖动,专门对付监控画面的光照波动;degrees=0必须设 0,睡姿是有方向语义的,旋转 90 度后「躺睡」和「靠椅睡」会彻底混淆;mosaic=1.0保持开启,对增强小目标鲁棒性帮助最大。
6.2 用一段真实值班视频做端到端验证
指标再好也要拿真实录像说话。方法:录 5~10 分钟值班室画面,包含至少 2 次正常活动和 2 次打盹场景,抽帧后跑模型:
ffmpeg -i monitor_room.mp4 -vf fps=1 frames/%05d.jpg yolo detect predict model=best.pt source=frames/ conf=0.2 save_txt=True逻辑说明:fps=1每秒抽一帧,5 分钟视频就是 300 帧,人工逐帧标记「睡/正常」的成本可控;conf=0.2是比默认 0.25 更低的阈值,睡岗场景下宁可多报警,也不漏报。参数说明:抽帧密度根据现场动作速度调整,人员移动快就fps=2,基本静止的值班室fps=1足够。
报警逻辑上,加一个冷却时间:连续 3 帧检出同一个位置目标才触警,能压掉大量单帧抖动误报;触发后 30 秒内不重复报警,避免同一事件刷屏。
6.3 我的习惯与这个方案的上限
说实话,睡岗检测的技术难度不在模型,而在数据一致性。换一个值班室,机位高度、灯光角度、桌椅颜色全变了,模型效果就可能打折。所以我的固定习惯是:拿到任何新数据集,先跑统计脚本看分布,再转格式训一版基线,然后看混淆矩阵决定要不要合并类别,最后才碰增强参数。这个顺序能帮你把「数据集的问题」和「模型的问题」分开,不至于在错误的维度上瞎调。
还有一点提醒:检测到睡姿只是第一步,真正要落地还要接报警推送、复核界面和值班记录,这些在工程里比模型本身更花时间。6549 张图训出来的模型做单一场景的睡岗报警是完全够用的,但别指望它是万能黑匣子,边界在于场景变化后需要持续补充新样本。我的教训是上线后前两周持续收集新画面、每周回填一次训练集,比任何调参都管用。希望帮到你。
本文还有配套的精品资源,点击获取