☰
睡岗检测实战:基于YOLOv8的VOC数据集训练与ONNX部署指南
2026/9/26 12:26:24 网站建设 项目流程

简介:面向需要训练睡岗检测模型的算法工程师与研究人员,这套采用VOC标记格式的数据集覆盖了桌子上趴睡、埋头睡觉、座椅上靠睡、平躺等多种典型睡姿,适合用于安防监控、工厂园区等场景下的目标检测算法开发与评估。资源包整体大小约422.3MB,内含2000个XML标注文件,每个文件记录对应图像中的目标类别与包围框坐标,标注文件命名保留了原始视频录制帧的时间与来源信息,便于按帧追溯、筛选和清洗数据。数据集素材来源于真实监控录像中的关键帧,场景接近实际值岗环境,包含不同角度、不同光线下的样本,可帮助增强模型对复杂现场的适应能力与鲁棒性。已有936人学习下载,这份数据可用于迁移学习或作为预训练补充,也可通过脚本方便地转换为其他主流训练格式,适合作为睡岗检测项目起步或扩充数据的重要选择。对于需要快速搭建睡岗检测验证环境的开发者,具有较高的实用价值。

1. 睡岗检测数据集:6549张图里的四种睡姿,能撑起一个能上线的检测模型吗

「睡岗检测数据集」这个名字放在工业视觉项目里并不惊艳,但它恰好卡在多数团队最容易卡住的位置:数据有标注、场景单一、能直接开训。6549张图片,VOC标记,目标覆盖值班室最常见的四种睡姿——桌子上趴着睡、埋头睡觉、座椅上靠着睡、平躺着睡。做中控室、机房、门卫室的睡岗报警时,项目十有八九不是死在模型结构上,而是死在数据上:要么样本太少,要么背景太杂,要么标注格式还得自己重新洗一遍。这份数据的价值就在于把场景边界画清楚了:一个检测模型该见到的画面、该框出的目标、该区分的姿态,它都给到了。适合刚拿到YOLOv8、想在一周内跑通「数据→训练→部署」闭环的工程师,也适合需要给甲方快速出原型验证的集成商。下文从VOC格式拆解、YOLO训练参数、常见翻车排查、ONNX部署到现场验证,按落地顺序一路讲完。

2. 先把手里的数据看透:VOC目录结构、标注字段与四种睡姿怎么分布

2.1 从JPEGImages到Annotations:一张睡岗图的VOC标注长什么样

拿到VOC标记的数据集,第一件事不是急着训练,而是按VOC的老规矩把目录摸清楚。VOC格式通常维持三个核心目录:JPEGImages放原始图片,Annotations放对应的XML标注,ImageSets/Main放训练集和验证集的划分文本。如果你拿到的压缩包里只有前两个目录,说明划分文件需要自己生成,这是最常见的情况。

打开一张XML标注,结构是固定的annotation根节点,里面每个object代表一个目标。睡岗检测里一个object就是画面里的一个人,name是睡姿类别,bndbox给出目标框的左上角和右下角坐标。下面是一张典型的睡岗标注XML:

<annotation> <filename>sleep_001234.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>desk_pillow</name> <bndbox> <xmin>512</xmin> <ymin>380</ymin> <xmax>890</xmax> <ymax>720</ymax> </bndbox> </object> <object> <name>chair_recline</name> <bndbox> <xmin>1024</xmin> <ymin>500</ymin> <xmax>1450</xmax> <ymax>860</ymax> </bndbox> </object> </annotation>

这段XML里能看到两个关键信息:坐标是绝对像素值,没有做任何归一化;类别名直接写在name节点里。YOLO系训练不吃VOC格式,必须先把xmin/ymin/xmax/ymax转成归一化的中心点加宽高。转换之前先检查一遍size节点里的宽高与JPEGImages实际图片是否一致,很多标注工具导出的XML会把尺寸写死,图片被预处理缩放过之后坐标就全偏了。VOC的bndbox不限制目标必须完整落在画面内,所以边界值擦边甚至越界的情况在人工标注里不少见,转换脚本里要处理。

2.2 四种睡姿的样本分布对模型上限的影响

标题里列了四种睡姿:桌子上趴着睡、埋头睡觉、座椅上靠着睡、平躺着睡。对应到模型训练,这四类不是平等的。趴在桌上睡和埋头睡觉在视觉上非常接近——都是头低下去、身体前倾,区别可能只在手臂是否贴在桌面上、头部是否被手臂遮挡;而座椅上靠着睡是头部后仰或歪向一侧,轮廓更接近正常坐姿;平躺睡在监控画面里通常是一个横向的大目标,与其他三类的特征差异最明显。

样本分布决定了模型的能力上限。如果6549张图里平躺睡只有两三百张,那这个类别就算训练出来,mAP也会虚高,因为验证集里平躺的样本同样少,碰巧检对一两个就拉高了指标。动手之前先用脚本统计每个类别的实例数量:

import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter ann_dir = Path("Annotations") counter = Counter() for xml_file in ann_dir.glob("*.xml"): tree = ET.parse(xml_file) for obj in tree.getroot().iter("object"): counter[obj.findtext("name").strip()] += 1 for name, count in counter.most_common(): print(f"{name}: {count}")

这段代码遍历所有XML,统计每个name出现的总次数。注意统计的是实例数不是图片数——一张图里可能同时有两个人,一个趴桌一个靠椅,这算两个实例。跑完之后你会得到四行数字,如果最少的类别不到最多的类别的三分之一,训练时就需要给少样本类别提权重,或者在数据增强里对它多做一些裁剪。睡岗检测还有一个容易被忽略的分布问题:同一张监控画面上,人的尺度变化很大。平躺睡在广角镜头里可能占半个画面,趴桌睡在远处工位上可能只有60×40像素。这直接影响后面imgsz参数的选取。

2.3 把VOC转成YOLO格式:转换脚本与四个边界坑

YOLOv8要求的标签是每个图片对应一个同名txt,每行一个目标,格式为class_id cx cy w h,其中cx cy w h全部归一化到0到1。转换脚本的逻辑很简单:读XML,解析bndbox,除以图片宽高做归一化。但我在实际转换两个睡岗数据集时都遇到过脏数据,所以脚本里必须做防御:

import os import xml.etree.ElementTree as ET from pathlib import Path class_names = ["desk_pillow", "head_down", "chair_recline", "lying_flat"] def voc2yolo(xml_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in class_names: print(f"跳过未知类别: {name} 在 {xml_path.name}") continue cls_id = class_names.index(name) bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) # 坐标钉在图片范围内,防止越界 xmin = max(0, min(xmin, img_w)) ymin = max(0, min(ymin, img_h)) xmax = max(0, min(xmax, img_w)) ymax = max(0, min(ymax, img_h)) if xmax <= xmin or ymax <= ymin: print(f"丢弃无效框: {xml_path.name} {name}") continue cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") return lines

这段脚本的逻辑分三层:第一层过滤掉不在预设类别表里的name;第二层把越界坐标强行钉回图片边界内;第三层丢弃转换后宽高为零的框。参数上img_w和img_h必须来自真实图片尺寸而不是XML里的size节点,我会在调用处用PIL读取图片再传给函数,这样能顺带发现图片文件缺失或损坏的问题。类别顺序class_names要和你后面写的dataset.yaml完全一致,YOLO按索引读类别,顺序错了模型训练出来就是张冠李戴。

转换过程里还有四个边界坑,按我踩过的频率排序:

  • 坑一:文件名大小写不一致。图片叫Sleep_001.jpg,XML叫sleep_001.xml,在Windows上解压没感觉,到Linux服务器上训练时YOLO按名字找标签找不到,那部分图片会被静默跳过。转换前先把所有文件名统一成小写。
  • 坑二:图片不是RGB三通道。有些监控截图保存成了灰度图或带Alpha通道的PNG,YOLO训练用cv2.imread读图时通道数不对会直接报错或训练出奇怪的结果。转换时用PIL强制转成RGB再存一遍。
  • 坑三:标注框把整个人框住了,但睡姿特征在头部。趴桌睡的标注框如果把整个身体都包进去,模型要学的「头埋在手臂里」这个特征只占框的一小块,训练时特征会被背景稀释。这种问题数据比例高的话,需要用脚本把框往头部方向收缩,或者重新标注。
  • 坑四:验证集和训练集有同一个人、同一工位的相似画面。睡岗数据通常在固定监控点采集,同一个人的连续帧会被随机分到两边,验证集mAP虚高。后面部署到新场景立刻打回原形。

3. 用YOLOv8训练睡岗检测:数据配置、训练参数与一次翻车实录

3.1 配置dataset.yaml与目录组织

转换完标签后,目录结构建议整理成YOLO直接能吃的形状。我一般这样组织:

sleep_dataset/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ ├── dataset.yaml

images/train和labels/train下放同名同前缀的图片和txt标签,注意YOLO要求图片和标签文件名完全一致,只是扩展名不同。dataset.yaml是训练入口,告诉YOLO去哪里找数据、有几类、类名是什么:

path: /data/sleep_dataset train: images/train val: images/val names: 0: desk_pillow 1: head_down 2: chair_recline 3: lying_flat

这里path推荐写绝对路径,尤其当你用nohup在后台跑训练时,相对路径经常因为工作目录切换而找不到数据。names的顺序必须和转换脚本里的class_names索引一致,YOLO不会帮你校验名称,它只认索引。如果你加了test字段但没建对应目录,训练会直接报错退出,没有test目录就不要写这一行。

划分数据集时,我强烈建议按「监控点位」而不是按「文件随机」来分。同一路摄像头在不同时段的画面高度相似,随机划分会把同一场景的白天黑夜分到训练集和验证集里,验证集mAP能高到离谱,但换一路新摄像头就崩。做法是先从文件名的点位编号或目录前缀提取摄像头ID,按点位分桶,再把桶整体按比例分到train和val。睡岗检测这种场景固定的任务,验证集的意义是检验泛化能力,不是检验模型记住了哪些画面。

3.2 训练命令与关键参数:imgsz、batch、epochs、patience

YOLOv8的训练命令不长,但参数选择直接决定睡岗检测能不能收敛。我常用的训练命令如下:

yolo detect train \ --model yolov8m.pt \ --data dataset.yaml \ --imgsz 1280 \ --batch 8 \ --epochs 200 \ --patience 40 \ --device 0 \ --project sleep_exp \ --name run1 \ --seed 42

逐个说参数。--model用了yolov8m.pt而不是yolov8s.pt,原因是睡岗检测的小目标多——远处工位上的趴桌人可能只有几十个像素,m模型的浅层特征表达能力比s强一档。--imgsz 1280是这次训练最关键的决定,后面3.3节详细说。--batch 8是在单张24G显卡上的安全值,显存小就降到4,但别为了省显存把imgsz砍到640,睡岗小目标损失太大。--epochs 200配--patience 40的意思是:训练最多跑200轮,但如果连续40轮验证集mAP没有刷新最好记录,就提前停止。睡岗数据集只有6549张,通常90到130轮就收敛了,硬跑满200轮反而过拟合。--seed 42固定随机种子,YOLOv8的数据增强是随机性的,不固定种子的话两次训练结果不可复现,后面调参时没法判断是参数起了作用还是运气好。

这几个参数的关系用一张表说清楚:

参数推荐值说明
modelyolov8m / yolov8l小目标占比高,s模型特征提取不够
imgsz1280训练分辨率,监控画面小目标多,640会糊
batch4~8由显存决定,24G以上才考虑16
epochs150~200配patience用,睡岗数据很少跑满
patience30~50连续N轮验证指标不刷新就停
seed固定整数保证两次训练可比

3.3 第一次翻车:整图缩到640,趴桌睡直接糊掉

我第一次拿这个数据集训练时,走了默认路线,--imgsz 640,因为网上教程都这么写。训练完看验证集,mAP50有0.87,看着不错,但把模型放到现场监控画面上跑,人只要离摄像头超过五米,检测框就开始乱跳,趴桌睡和埋头睡几乎全漏。把现场画面截下来看了一下:1080P的监控画面里,远处工位上的人只有大约80×50像素,缩到640分辨率时这个人变成约30×20像素,特征完全糊掉了。

原因不在模型,而在训练分辨率。YOLOv8默认的640输入尺寸是为COCO那种目标占画面比例较大的数据集设计的,监控场景目标占比常常不到百分之一。解决方法是把imgsz提到1280,同时把--rect参数加上,让YOLO按图片原始宽高比做矩形训练而不是强制拉伸成正方形。1280输入下显存占用会翻倍,batch从16降到8,但小目标的recall提升非常明显。如果显存只有8G,还有一个折中方案:保持训练imgsz 640,但用--crop配合--mosaic让模型多看到局部放大的目标,不过效果比直接上1280差一截。另一个思路是把监控画面按区域切块训练,比如把1080P画面切成左上、右上、左下、右下四个640×540的块,变相提高目标占比,但部署时也要按同样方式切块推理,逻辑复杂一些,只建议在模型必须保持小体积的嵌入式设备上这么干。

4. 睡岗检测训练的常见问题排查:翻车现场与参数补救

4.1 现象:趴桌睡被识别成埋头睡,两类边界乱跳

训练完第一次跑现场视频,最常见的翻车是趴桌睡和埋头睡互相误判。从画面上看,这两个姿态的共性太强:头都是低的,都是静止的,区别只在手臂姿态和头部遮挡程度。模型学到的特征如果主要集中在「头的位置低」上,那低头看手机、趴桌闭眼、埋头打盹都会被归到同一类。

原因分两层:第一层,数据分布的锅——训练集里这两类的样本比例不均衡,模型倾向于把模糊样本分到数量多的那一类;第二层,标注口径不统一——标注员把「手臂叠在桌上、脸贴手臂」标成趴桌,把「头垂着、手臂没在画面里」标成埋头,边界本身就是模糊的。

解决的第一个手段是在数据层面加负样本。睡岗检测最怕的不是睡姿检不出,而是把正常低头动作误报成睡岗。我会从现场监控里找一些「低头看手机」「弯腰整理桌面」「伏案写字」的片段,标注为normal或直接作为背景类加入训练。如果不想动数据集,第二个手段是后处理——把趴桌和埋头合并成一个大类「低头静止」,报警逻辑上这两类本来就都算离岗风险,不区分反而更稳。我用后者,报警准确率立刻提了一截。

4.2 现象:白天检得好好的,夜间值班室漏检严重

睡岗检测最常见的部署环境是7×24小时的值班室。数据集的6549张图如果以白天光照为主,模型在夜间低照度画面上的表现会断崖式下跌。夜间监控画面要么整体偏暗,要么开了红外补光后变成灰度感很强的图像,暗部噪声大,模型在训练时没见过这种分布。

原因很直接:域偏移。检测模型对光照敏感度远超人的直觉,一张曝光正常的图和一张欠曝的图在特征空间里距离很远。解决分三条路:第一,训练时加光照增强——把亮度、对比度、伽马变换的增强概率从默认值调高。YOLOv8的增强参数是hsv_h、hsv_s、hsv_v,我通常把hsv_v从默认的0.4调到0.8,让模型见过更宽的亮度范围;第二,如果现场有红外摄像头,就采集一段夜间红外视频,用半自动标注工具补一批夜间样本做微调(fine-tune),这是最有效的手段;第三,部署时做预处理——推理前对暗帧做自适应直方图均衡化(CLAHE),把输入图像拉亮再送进模型。实测第三条对暗光漏检的改善最直接,代价是每帧多了几毫秒计算量。

4.3 现象:训练loss正常下降,验证mAP却剧烈波动

训练时看着box_loss和cls_loss一路下降,但每轮验证的mAP像过山车,上一轮0.83下一轮0.61。这种波动在睡岗这种小数据集上很常见,原因通常不是模型问题,而是验证集本身太小或分布不均匀。

我排查的顺序是:先看验证集图片数量和类别分布是否合理。6549张图按9:1划分,验证集只有650张左右,如果其中某个睡姿只有二三十个实例,那这个类别的AP本身方差就大。解决方法是把划分比例改为8.5:1.5,尽量保证验证集每类超过50个实例;再检查数据增强的随机性——训练轮次少的时候,最后几轮的增强尺度差异直接反映在验证指标上。另一个容易被忽略的原因是没有固定seed,我早期训练时不加--seed参数,两次一模一样的训练命令都能跑出完全不同的曲线,根本没法判断调参效果。固定seed之后,波动还在但可解释性大大提升。

4.4 现象:训练直接OOM,显存爆掉

CUDA out of memory在睡岗训练里十有八九是因为imgsz和batch的乘积超出了显存。1280的imgsz加batch16,在24G显存上勉强能跑,8G卡基本必爆。

解决方式优先降batch而不是降imgsz。把--batch 8改成--batch 4,如果还爆,就用--batch 2配合--gradient-accumulation 2,相当于每两个小batch攒一次梯度,效果接近batch4但显存占用减半。YOLOv8命令里没有单独的梯度累积参数,需要改配置文件里的accumulate字段,或者在训练脚本里通过--batch 4配合数据加载的workers值。还有一个野路子:把训练分辨率降到960而不是1280,兼顾小目标和显存,实测960对五米外的趴桌睡识别率比640高不少,又比1280省一半显存。如果连960都跑不动,就该考虑换卡了,睡岗检测的监控画面分辨率摆在那里,模型输入尺寸是绕不开的硬门槛。

5. 部署落地:从ONNX导出到OpenCV前向推理的最小闭环

5.1 导出ONNX并检查输出张量

训练完拿到best.pt,部署阶段通常不直接跑PyTorch,尤其现场是工控机只有CPU、或者接了海康/大华的NVR时,ONNX是最通用的中间格式。导出命令一行:

yolo export model=best.pt format=onnx opset=12 simplify=True

导出后先别急着写推理代码,用Python把ONNX的输入输出结构打印出来确认一下:

import onnx model = onnx.load("best.onnx") graph = model.graph for inp in graph.input: print("输入:", inp.name, [d.dim_value for d in inp.type.tensor_type.shape.dim]) for out in graph.output: print("输出:", out.name, [d.dim_value for d in out.type.tensor_type.shape.dim])

打印结果应该是输入[1, 3, 1280, 1280](训练时指定的分辨率),输出[1, 7, 8400]。这里7的含义是4个类别加3个坐标相关量(中心点x、中心点y、宽高、再加置信度,实际YOLOv8输出排布是cx, cy, w, h, class0_conf, class1_conf, ...),8400是1280分辨率下三个尺度特征图上的候选框总数。如果输出维度和你后面写的解码代码对不上,先回来检查这里,别急着怀疑模型不对。

5.2 用OpenCV跑一次前向推理

现场工控机上大概率不会装PyTorch全家桶,一个OpenCV加一个ONNX Runtime就能扛起推理。下面是一个最小可用的OpenCV DNN推理代码:

import cv2 import numpy as np net = cv2.dnn.readNetFromONNX("best.onnx") img = cv2.imread("frame_001234.jpg") img_h, img_w = img.shape[:2] # 缩放并填充到1280x1280 input_size = 1280 blob = cv2.dnn.blobFromImage(img, 1/255.0, (input_size, input_size), swapRB=True, crop=False) net.setInput(blob) outputs = net.forward()[0] # shape: (7, 8400) # 后处理:解码候选框 conf_thresh = 0.35 boxes, scores, class_ids = [], [], [] for i in range(outputs.shape[1]): x, y, w, h = outputs[0, i], outputs[1, i], outputs[2, i], outputs[3, i] class_scores = outputs[4:, i] class_id = np.argmax(class_scores) score = class_scores[class_id] if score < conf_thresh: continue # 坐标还原到原图尺寸 x = (x - w / 2) * img_w / input_size y = (y - h / 2) * img_h / input_size w = w * img_w / input_size h = h * img_h / input_size boxes.append([x, y, w, h]) class_ids.append(class_id) scores.append(float(score)) # NMS去重 idx = cv2.dnn.NMSBoxes(boxes, scores, conf_thresh, 0.45) for i in idx: x, y, w, h = boxes[i] cv2.rectangle(img, (int(x), int(y)), (int(x+w), int(y+h)), (0, 255, 0), 2) cv2.putText(img, str(class_ids[i]), (int(x), int(y-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

这段代码的关键点在两个地方:blobFromImage的crop=False表示不裁剪直接缩放,配合模型训练时的letterbox策略;坐标还原时用的是等比缩放关系而不是直接除以1280。如果你的训练代码里用了--rect矩形训练,推理时还需要额外处理宽高比对齐,否则检测框位置会偏。NMS的0.45是IoU阈值,现场画面中多人交错时,这个值调到0.5能减少相邻目标的框被合并。

5.3 报警联动:连续N帧命中与置信度双阈值

检测模型单帧输出直接触发报警,是睡岗检测部署最常见的误报来源。监控画面里偶发的转头、抬手、光影变化都可能让模型在某一帧输出一个高置信度框。我一般会加一个时间维度的确认逻辑:

class SleepAlarm: def __init__(self, n_frames=10, min_conf=0.4, alarm_conf=0.6): self.n_frames = n_frames self.min_conf = min_conf self.alarm_conf = alarm_conf self.hit_history = [] # 每帧是否连续命中 def update(self, detections): hit = any(d["score"] >= self.min_conf for d in detections) self.hit_history.append(hit) if len(self.hit_history) > self.n_frames: self.hit_history.pop(0) # 连续N帧命中且至少一帧高置信度才报警 if len(self.hit_history) == self.n_frames and all(self.hit_history): if any(d["score"] >= self.alarm_conf for d in detections): return True return False

这里的逻辑是双阈值:min_conf过滤掉明显噪声,alarm_conf要求在连续命中窗口内至少出现一次高置信度检测。n_frames取10到15帧,按25fps折算约0.4到0.6秒,既滤掉单帧闪烁,又不会让真正的睡岗漏报。睡岗本身是长时间静止状态,报警慢一两秒完全可以接受,但误报一次要人工去现场确认,代价高得多。这个连续帧确认机制是我部署任何一个检测模型都会加的后处理防线。

6. 验证模型别只信mAP:用现场回放视频做帧级抽测与阈值标定

模型训完、部署完,最后一步是验证它到底能不能用。mAP是训练阶段的指标,它只能告诉你模型在验证集上的平均表现,但验证集的画面和你实际部署的现场往往不是同一路摄像头。我会用一段现场监控回放视频做帧级抽测,方法很笨但有效:从回放视频里按时间段分层抽帧,白班抽200张、夜班抽200张、凌晨抽200张,保证覆盖不同光照,再逐帧跑推理,统计检测结果和真实情况的对表。

# 从回放视频每30秒抽1帧,存到 test_frames/ ffmpeg -i night_shift.mp4 -vf "fps=1/30" -q:v 2 test_frames/frame_%05d.jpg

抽完帧后,跑一遍模型,把每帧的检测框和置信度都落盘成一个CSV,然后对着回放视频人工核对几件事:睡姿目标有没有漏检、正常坐姿有没有被误报、置信度分布大概落在什么区间。我习惯把置信度按0.2到0.9分成几档统计命中数,这样能直观看到阈值放在多少最合适,而不是拍脑袋定0.5。标定阈值时记住一个原则:睡岗报警宁可少报一次也不该乱报,误报一次让人跑过去看,两三次之后这个系统就被甲方关掉了。所以我会把报警阈值压到比验证时高出一个档位,再叠加连续帧确认,把单帧误报彻底滤掉。

这几年做了好几个检测类项目,我最大的教训是拿到检测模型先别急着看mAP,先找一段现场视频按帧跑一遍,让模型暴露在真实光照、真实角度、真实噪声里跑上十分钟。睡岗检测数据集给了你一个很好的起点,但真正能用的系统,永远是数据和现场反复对齐调出来的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询