ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

YOLOv8-Pose驾驶员疲劳检测系统实战:从数据标注到UI界面

2026/8/31 13:24:27 拓冰建站 浏览量
YOLOv8-Pose驾驶员疲劳检测系统实战:从数据标注到UI界面 简介本资源是一个基于Python与YOLOv8实现的智能驾驶员状态监测系统面向高校毕业设计、课程设计及计算机视觉初学者聚焦疲劳驾驶场景下的关键行为检测问题涵盖闭眼、张嘴、睁眼、闭嘴四类状态识别。压缩包共2000个文件含1945个标注txt文件对应3000张图像的边界框与类别标签、24个说明文档含数据集组织规范、训练推理指南、19个核心Python脚本支持一键训练、预测与UI界面调用以及少量C/头文件用于推理加速与跨平台部署。整体大小为245.39MB结构清晰开箱即用用户仅需按mydata.yaml配置路径运行train.py或predict.py即可完成自定义训练与实时检测。项目已集成预训练权重支持微调优化并配套完整UI交互界面便于演示与二次开发。目前已有28人学习下载是少有的兼顾算法实现、工程封装与教学适配的端到端视觉检测实践方案。 前阵子帮朋友实验室做了一个驾驶员状态监测的Demo选型从MediaPipe换到OpenPose最后稳定在YOLOv8-Pose上。实话说这类项目在CV方向已经是相当成熟的落地场景了但网上大多数教程只讲了“用YOLOv8训练关键点模型”这一步后面“怎么把关键点变成疲劳判断”“怎么把推理结果接进一个像样的界面”基本没人讲透。这篇文章把我从零搭到最终交付的完整路径拆开写一遍包括模型选型理由、数据准备、训练调试、状态判断逻辑、UI封装以及最后踩过的那些坑。1. 项目初始化先搞清楚这系统到底要监测什么很多人一上来就急着下模型跑demo结果数据集不对、判断逻辑没想清楚做到一半又推倒重来。我习惯先花半天把所有需求和边界条件列清楚。1.1 核心需求拆解这个项目标题里的“智能驾驶员状态监测”其实包含两个层面一是检测驾驶员的姿态关键点比如眼睛、鼻子、耳朵、肩膀的位置二是基于关键点做状态推断包括最常见的疲劳瞌睡检测、分心扭头、低头检测进阶一点还能做打哈欠检测、双手脱离方向盘检测。更关键的是这套系统需要输出“结论”而不是“图像”所以还需要一个业务逻辑层把模型的输出关键点坐标和置信度换算成PERCLOS单位时间眼睛闭合比例、头部姿态角、嘴部开合度这类指标再根据阈值判断当前状态。我用一张需求清单把功能边界先定死了功能模块具体内容输出形式驾驶员人脸关键点检测眼睛、鼻子、嘴巴、耳朵等关键点实时定位68点/6点坐标数据眼睛状态分析计算EAR眼睛纵横比判断睁眼/闭眼实时数值状态标签嘴部状态分析计算MAR嘴部纵横比判断打哈欠实时数值状态标签头部姿态估计基于人脸关键点估算俯仰角、偏航角、滚转角三轴角度值疲劳判定PERCLOS眼睑闭合比例超过阈值结合连续闭眼时长疲劳预警界面显示实时视频画面、关键点可视化、状态标签、报警记录桌面UI报警响应当触发疲劳或分心时进行界面弹窗声音提醒报警提示1.2 为什么最终选YOLOv8-Pose而不是OpenPose或MediaPipe选型是这类项目第一个真正的分岔路口我把三个方案都在本机跑了一遍对比OpenPose精度确实好尤其是多人场景下的关键点检测但模型太大推理速度在普通显卡上很难跑到实时。我的测试机是GTX 1660Ti跑OpenPose的BODY_25模型也就15FPS左右明显不够用。MediaPipe Face Mesh轻量、速度快但问题在于它输出的是数百个密集人脸关键点对于“是否闭眼”“是否打哈欠”这类判断来说信息冗余而且MediaPipe的Face Mesh更偏向人脸重建而非状态判断拿它做疲劳检测需要自己做大量特征工程。YOLOv8-Pose这是最终选型。它本身就是YOLOv8目标检测架构上加了关键点输出分支同时输出目标框和关键点对人脸这种小目标也能保持不错的检测效果。我拿1660Ti实测fp16推理能跑到30FPS以上完全满足实时性要求。而且Ultralytics官方对数据集格式、训练流程、部署导出都封装得很好对个人开发者极其友好。一句话选型结论追求落地时效和实时性能选YOLOv8-Pose。追求科研精度上限、不差算力才考虑OpenPose。2. 环境准备与最容易被卡住的安装细节这个项目涉及的环境依赖不算复杂但我在环境配置上见过太多人卡住——本质上还是版本匹配问题。下面给出一套实测可用的组合。2.1 完整依赖清单与版本我使用的环境如下操作系统Windows 10 / Ubuntu 20.04均可Python版本3.8.10建议不要用3.7以下yolov8要求Python3.7但我实测3.8最稳CUDA11.8跟PyTorch版本匹配即可PyTorch2.0.0cu118Ultralytics YOLOv88.0.20界面库PySide6 6.5.0辅助库OpenCV 4.8.0、NumPy 1.24.3安装命令依次执行# 创建独立虚拟环境强推别直接装到base环境 conda create -n driver_monitor python3.8 -y conda activate driver_monitor # 安装PyTorch按CUDA版本选命令 pip install torch2.0.0 torchvision0.15.0 --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics pip install ultralytics8.0.20 # 安装界面依赖 pip install pyside66.5.0 opencv-python4.8.0.762.2 NVIDIA驱动与CUDA版本不一致的排查方法这里分享一个排查经验。如果你执行python -c import torch; print(torch.cuda.is_available())返回False大概率是以下两种情况PyTorch的CUDA版本和显卡驱动不匹配。用nvidia-smi看右上角的CUDA Version比如显示12.0那么安装cu118或cu121版本的PyTorch都可以。驱动是向下兼容的不用追求完全一致但PyTorch的CUDA版本不能高于驱动支持的版本太多。安装的是CPU版PyTorch。注意不要用默认的pip install torch装到CPU版一定要指定--index-url https://download.pytorch.org/whl/cu118这类带CUDA的源。GTX 1660Ti属于图灵架构6GB显存训练YOLOv8-Pose模型时batch size别开太大我后续会讲具体的显存优化策略。2.3 验证环境是否就绪安装完成后做一次快速验证确认基本依赖正常import torch import ultralytics import cv2 print(CUDA available:, torch.cuda.is_available()) print(YOLOv8 version:, ultralytics.__version__) print(OpenCV version:, cv2.__version__)3. 数据准备标注一套驾驶员姿态数据集的具体操作热词里有“yolov8 pose 数据标注具体操作”说明不少人卡在这一步。YOLOv8-Pose训练的数据格式和普通目标检测不同它除了检测框还要求每个关键点有坐标和可见性所以标注工具和流程都要调整。3.1 数据集来源与构成我用了一个公开驾驶行为数据集加自己补采的数据。公开数据集方面Brain4Cars和DMD (Driver Monitoring Dataset)都是相关领域的常用选择但注意它们的原始标注格式各不相同需要转换成YOLOv8的格式。最终的数据集结构如下dataset/ ├── train/ │ ├── images/ │ │ ├── img_001.jpg │ │ └── ... │ └── labels/ │ ├── img_001.txt │ └── ... ├── val/ │ ├── images/ │ └── labels/ └── data.yaml3.2 标注工具选择与完整流程标注工具我用的是LabelMe配合一个格式转换脚本流程如下先用LabelMe打开图片选择“Create Polygons”把驾驶员的人脸区域框出来然后依次标注关键点。注意YOLOv8-Pose一般标注的人脸关键点是鼻子、左眼、右眼、左耳、右耳这5个点也可以用标准68点方案但5点方案对疲劳判断完全够用且标注成本低很多。导出为LabelMe格式的JSON文件。写脚本将LabelMe的JSON转换为YOLOv8-Pose需要的txt格式。转换脚本的核心逻辑import json import numpy as np import os def convert_labelme_to_yolo_pose(labelme_json_path, output_txt_path, img_w, img_h): with open(labelme_json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] # LabelMe中多边形点坐标 shape data[shapes][0] # 假设第一个shape是人脸框 polygon_points np.array(shape[points], dtypenp.float32) # 计算归一化检测框 x_min, y_min polygon_points.min(axis0) x_max, y_max polygon_points.max(axis0) x_center ((x_min x_max) / 2) / img_w y_center ((y_min y_max) / 2) / img_h box_w (x_max - x_min) / img_w box_h (y_max - y_min) / img_h # 关键点按标注顺序填入 keypoints [] for shape_item in data[shapes][1:]: # 第一个是框其余是关键点 label shape_item[label] points shape_item[points][0] # 单个点 if nose in label.lower(): keypoints.append([points[0] / img_w, points[1] / img_h, 2]) # 其他关键点同理... # 写入YOLO格式 with open(output_txt_path, w) as f: line f0 {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f} for kp in keypoints: line f {kp[0]:.6f} {kp[1]:.6f} {kp[2]} f.write(line \n)标注时的关键点顺序必须与模型的data.yaml中kpt_shape定义一致。我在项目中定义了5个关键点顺序是鼻子、左眼、右眼、左耳、右耳。这里有个很多人忽略的细节可见性标志第3个值如果标0训练时该点完全不参与loss计算标1表示遮挡但可见标2表示完全可见。实际训练中对耳朵这种容易被头发遮挡的关键点建议标1而不是2否则会强行拟合到错误位置。3.3 data.yaml的编写与目录结构# data.yaml path: dataset/ train: train/images val: val/images kpt_shape: [5, 3] # 5个关键点每个3个值(x, y, visibility)注意kpt_shape配置不对会在训练时报错或输出维度不对。另外如果你只需要人脸关键点那nc类别数就是1。3.4 数据增强与样本不均衡问题驾驶员状态监测的数据天然存在严重的不均衡——正常驾驶的睁眼画面占绝大多数闭眼、打哈欠的样本少之又少。这直接影响模型对疲劳状态的召回率必须靠数据增强弥补。我的处理方法是对闭眼和打哈欠样本做离线复制增强比如水平翻转、小角度旋转、亮度扰动把少数类样本数量提升到正常样本的30%以上。在data.yaml同级目录增加一个增强配置或者直接在训练参数中加大hsv_h、hsv_s、degrees等增强系数的随机范围但要小心过大增强会让关键点漂移我最终折中采用了degrees10、hsv_h0.015、hsv_s0.7。不要对闭眼样本做上下翻转增强会让眼睛状态完全反转造成标签错误。4. 训练规划从预训练权重到自定义模型的完整过程4.1 选择预训练权重YOLOv8-Pose官方提供了yolov8n-pose.pt、yolov8s-pose.pt、yolov8m-pose.pt等预训练权重。它们是在COCO数据集上训练的COCO包含人的全身17个关键点虽然和我们的5点人脸关键点不完全一致但迁移学习的价值依然存在。实测三档权重在我这个任务上的表现权重参数量推理速度(1660Ti fp16)mAP50(自定义测试集)备注yolov8n-pose.pt3.3M约45FPS0.82轻量速度快精度稍逊yolov8s-pose.pt11.6M约33FPS0.88推荐平衡性好yolov8m-pose.pt26.4M约20FPS0.90精度最高实时性略紧我最终选择yolov8s-pose.pt作为底座主要原因是在驾驶监控场景中实时性优先级高于几个百分点的精度差距而且33FPS的帧率到了业务层做时序平滑后效果与m模型差距不明显。4.2 训练命令与超参数设置yolo pose train \ modelyolov8s-pose.pt \ datadata.yaml \ epochs150 \ imgsz640 \ batch8 \ device0 \ patience30 \ projectdriver_pose \ nameexp1 \ pretrainedTrue \ lr00.001 \ lrf0.01 \ warmup_epochs3 \ cos_lrTrue这里解释几个关键超参数的选择依据batch81660Ti只有6GB显存imgsz640下设batch8差不多是上限。如果爆显存优先降低imgsz到 480不要先动batch否则BN统计会不稳定。patience30验证集指标连续30轮不提升就早停省时间。lr00.001迁移学习场景下初始学习率不需要太大尤其是预训练权重和目标任务相近时大学习率会破坏已学到的特征。cos_lrTrue余弦退火的收敛稳定性比线性下降好训练后期不容易震荡。4.3 训练过程监控loss曲线怎么读训练完成后Ultralytics会在driver_pose/exp1/目录下生成results.png包含loss曲线和metrics曲线。我通常重点看三样东西train/pose_loss和val/pose_loss两条曲线应该同步下降并在后期趋于平稳。如果训练loss持续下降但验证loss在某个epoch后反弹说明过拟合了应该早停或加增强。metrics/precision(B)和metrics/recall(B)目标检测框的precision和recall。疲劳检测场景对recall更敏感宁可多报疲劳也不能漏报所以如果recall偏低可以适当降低confidence阈值。关键点准确率metrics/mAP50(P)关键点匹配的mAP这个值如果低于0.8说明关键点定位不够准需要检查标注或增强配置。我曾经遇到一个很典型的情况loss曲线很漂亮但实际跑摄像头时头部转到侧脸时眼睛关键点全飘。原因是数据集里侧脸样本太少。所以光看曲线不够一定要做实际场景验证。4.4 导出模型并测试推理训练完成后导出为ONNX或TensorRT格式便于在推理阶段获得更好性能from ultralytics import YOLO # 加载训练好的模型 model YOLO(driver_pose/exp1/weights/best.pt) # 导出ONNX model.export(formatonnx, halfTrue, simplifyTrue) # 导出TensorRTNVIDIA GPU可用 model.export(formatengine, halfTrue, dynamicFalse, imgsz640)我最终部署用的是ONNX格式通过onnxruntime-gpu做推理这样能绕开PyTorch的Python开销节省一点推理时间也更方便后面用C或其他语言接。5. 状态判断逻辑把关键点坐标翻译成“疲劳”“分心”“打哈欠”模型输出的是关键点坐标但系统真正要输出的是状态判断。这一步是我认为整个项目最核心的业务逻辑部分也最需要细心打磨。5.1 EAR眼睛纵横比与闭眼检测眼睛纵横比Eye Aspect Ratio, EAR是判断眼睛开合最经典的方法。它利用眼睛周围关键点之间的欧氏距离比值人眼睁开时EAR稳定在某个值附近闭眼时会显著下降。以5点方案鼻子、左眼、右眼、左耳、右耳为例EAR计算需要眼睛上下眼睑的关键点。但我们的5点方案里每只眼睛只有一个点算不了EAR。所以我实际采用了两个解决思路之一方案A改用COCO 17点或自定义68点方案获取眼睛轮廓上下左右多个点来算EAR。这个精度最高。方案B用5点方案的单眼关键点配合深度学习回归分类。我给眼睛区域裁出小图用一个小型CNN分类睁眼/闭眼。这个方案的好处是标注成本低缺点是额外引入了一个模型。在我最终交付的项目里用的是方案A的变体——自定义9点方案面部5点外加左右眼各自上下眼睑各2点。这样就能算EAR了。EAR核心计算逻辑def calculate_ear(eye_points): eye_points: 眼睛轮廓关键点坐标按如下顺序排列 [左眼角, 左上眼睑, 右上眼睑, 右眼角, 右下眼睑, 左下眼睑] # 计算垂直距离 v1 np.linalg.norm(eye_points[1] - eye_points[5]) v2 np.linalg.norm(eye_points[2] - eye_points[4]) # 计算水平距离 h np.linalg.norm(eye_points[0] - eye_points[3]) # 如果水平距离过小可能是检测异常返回一个合理默认值 if h 1e-6: return 0.0 ear (v1 v2) / (2.0 * h) return ear关键阈值根据我的实测EAR小于0.2时可以判断为闭眼。但这个阈值会因摄像头安装角度、人脸大小、个人习惯略有浮动所以系统里做了一分钟的自适应校准取用户正常睁眼时的平均EAR作为基准闭眼阈值设为基准值的65%。5.2 MAR嘴部纵横比与打哈欠检测MAR的计算思路与EAR完全一致只是换成嘴部轮廓的关键点。阈值我设为0.5连续10帧超过该值就认为打了一次哈欠。代码实现与EAR类似def calculate_mar(mouth_points): mouth_points: 嘴部轮廓关键点 [左嘴角, 上嘴唇, 下嘴唇, 右嘴角, 上嘴唇内侧, 下嘴唇内侧] v1 np.linalg.norm(mouth_points[1] - mouth_points[5]) v2 np.linalg.norm(mouth_points[2] - mouth_points[4]) h np.linalg.norm(mouth_points[0] - mouth_points[3]) if h 1e-6: return 0.0 mar (v1 v2) / (2.0 * h) return mar5.3 头部姿态估计与分心检测分心检测依赖头部姿态即偏航角(yaw)、俯仰角(pitch)、滚转角(roll)。我用的是经典方法通过2D关键点和3D人脸模型之间的对应关系用PnP算法求解相机外参。这个思路可以理解为在人脸关键点坐标与标准3D人脸模型之间建立映射关系然后求解旋转矩阵与平移向量。Opencv里直接就有cv2.solvePnPimport cv2 import numpy as np # 3D模型点标准人脸模型坐标 model_3d_points np.array([ [0.0, 0.0, 0.0], # 鼻尖 [0.0, -30.0, -10.0], # 下巴 [-45.0, 30.0, -20.0], # 左眼外角 [45.0, 30.0, -20.0], # 右眼外角 [-30.0, -20.0, -20.0], # 左耳 [30.0, -20.0, -20.0], # 右耳 ], dtypenp.float64) def estimate_head_pose(face_2d_points, camera_matrix, dist_coeffs): # face_2d_points: 与model_3d_points对应的2D关键点 success, rvec, tvec cv2.solvePnP( model_3d_points, face_2d_points, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE ) if not success: return None, None, None rotation_matrix, _ cv2.Rodrigues(rvec) # 从旋转矩阵提取欧拉角 sy np.sqrt(rotation_matrix[0, 0]**2 rotation_matrix[1, 0]**2) singular sy 1e-6 if not singular: pitch np.degrees(np.arctan2(rotation_matrix[2, 1], rotation_matrix[2, 2])) yaw np.degrees(np.arctan2(-rotation_matrix[2, 0], sy)) roll np.degrees(np.arctan2(rotation_matrix[1, 0], rotation_matrix[0, 0])) else: pitch np.degrees(np.arctan2(-rotation_matrix[1, 2], rotation_matrix[1, 1])) yaw np.degrees(np.arctan2(-rotation_matrix[2, 0], sy)) roll 0 return yaw, pitch, roll相机内参标定不可跳过。最简单的做法是用棋盘格标定但如果只是监控场景也可以用经验值假设摄像头水平视场角为60度图像宽度为640那么焦距约为图像宽度的一半除以tan(30度)。求得的初始值完全够用。分心判定逻辑当偏航角绝对值持续超过30度或俯仰角持续超过25度持续3秒以上判定为分心驾驶。这里的“持续”很关键能过滤掉用户短暂看后视镜、调整坐姿的误报。5.4 PERCLOS疲劳判定与多帧平滑PERCLOS是驾驶员疲劳检测中的经典指标核心思想是统计一段滑动时间窗内眼睛闭合时间所占的比例。我的实现是在一个60秒的滑动窗口内统计满足“闭眼”的帧数占总帧数的比例超过0.4则触发疲劳预警。但单帧判断噪声太大直接套阈值会造成频繁误报。我引入了一个状态机帧序列计数的机制连续3帧检测到闭眼才认为进入“闭眼状态”。连续3帧检测到睁眼才认为退出“闭眼状态”。在闭眼状态下持续超过0.8秒立即触发疲劳报警无需等待PERCLOS窗口统计。这种设计能有效避免关键点抖动导致的误判在实测中大幅提升了报警的稳定性。5.5 报警机制与恢复机制报警不是“亮红灯”就完了需要考虑用户体验和实际驾驶场景。我做了一个三档报警等级触发条件响应方式提示闭眼持续0.5秒或PERCLOS超0.3界面黄色提示条语音提示“请注意休息”警告闭眼持续0.8秒或PERCLOS超0.4界面红色警报蜂鸣器连续短响强提醒连续闭眼超2秒界面全屏红闪高分贝蜂鸣合成语音播报恢复机制上疲劳状态触发后进入5分钟冷却期避免循环报警干扰用户。分心状态则是在姿态恢复正常连续5秒后自动解除。6. 实时推理管线别让UI线程卡死的关键设计实时推理和UI的结合是很多新手翻车的高发区。核心原则就一句话推理绝对不能跑在UI线程上。否则摄像头一帧还没处理完界面就卡成幻灯片更严重点连关闭按钮都点不动。6.1 多线程/进程架构设计我用的是QThread 生产者消费者模式采集线程从摄像头读取原始帧放入队列。推理线程从队列取帧执行YOLOv8推理和状态判断产出结果帧和数据。UI主线程定时从结果队列取最新帧更新QLabel显示。队列用queue.Queue管理并设置最大长度比如10帧满了就丢弃旧帧确保处理的永远是最新鲜的画面避免延迟累积。关键代码结构import queue import threading from PySide6.QtCore import QThread, Signal import cv2 import numpy as np from ultralytics import YOLO class InferenceThread(QThread): frame_ready Signal(np.ndarray) status_updated Signal(dict) def __init__(self, model_path, camera_id0): super().__init__() self.model YOLO(model_path) self.cap cv2.VideoCapture(camera_id) self.running True self.frame_queue queue.Queue(maxsize10) self.lock threading.Lock() # 状态判断器实例 self.eye_analyzer EyeAnalyzer() self.head_pose_estimator HeadPoseEstimator() self.fatigue_detector FatigueDetector() def run(self): while self.running: ret, frame self.cap.read() if not ret: continue # 推理和判断 results self.model.predict(sourceframe, verboseFalse) status self.analyze_frame(results) # 更新UI annotated_frame self.draw_status(frame, results, status) self.frame_ready.emit(annotated_frame) self.status_updated.emit(status) def analyze_frame(self, results): # 解析关键点调用EAR/MAR/头部姿态判断 pass6.2 性能优化推理耗时与帧率平衡实测不同配置下的耗时数据配置单帧耗时FPS备注yolov8s-pt, 640x640, CPU约180ms5.5不可用yolov8s-pt, 640x640, GPU fp16约30ms33可用yolov8n-pt, 480x480, GPU fp16约12ms80帧率高但精度略降ONNX优化fp16, 640x640, GPU约18ms55我的最终配置要提升性能按优先级排序导出ONNX并用onnxruntime推理简单收益大fp16半精度推理约提升30%显存占用也降低降低输入分辨率对关键点任务640降到480影响不大提高推理间隔用上一次的结果做插值不适合做精确检测谨慎使用6.3 摄像头标定和图像预处理摄像头安装位置直接影响检测效果。如果是USB摄像头对着驾驶员建议分辨率设为 640x480 或 1280x720不要用4K推理开销和延迟都过大。光照不足时开启摄像头自动增益必要时加一个红外补光灯。摄像头视角尽量正对驾驶员面部仰角不要超过15度否则头部姿态角会存在系统偏差。7. UI界面设计用PySide6搭一个能交付的桌面应用标题里明确包含“含完整源码与UI界面”我把UI部分的完整设计思路也拆出来。我用的PySide6也就是Qt for Python功能足够、跨平台、视觉也比较现代。7.1 界面布局与核心组件界面整体分四个区域视频显示区占据中央实时渲染摄像头画面和关键点可视化和状态标签。状态面板右侧显示当前的疲劳等级、眼睛状态、头部姿态角、PERCLOS值、运行时长。报警记录区底部表格记录每次报警的触发时间、类型、持续时长。控制栏顶部的开始/停止按钮、摄像头切换下拉框、阈值调节滑块。核心UI结构代码from PySide6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QSlider, QTableWidget, QTableWidgetItem) from PySide6.QtCore import Qt class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(智能驾驶员状态监测系统) self.setMinimumSize(1200, 700) # 中央Widget central_widget QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout QHBoxLayout() central_widget.setLayout(main_layout) # 左侧视频显示 控制栏 left_widget QWidget() left_layout QVBoxLayout() self.video_label QLabel(实时画面) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setMinimumSize(800, 600) self.video_label.setStyleSheet(background-color: #222; color: #ccc;) left_layout.addWidget(self.video_label) main_layout.addWidget(left_widget, stretch3) # 右侧状态面板 right_widget QWidget() right_layout QVBoxLayout() self.status_label_fatigue QLabel(疲劳等级: 正常) self.status_label_fatigue.setStyleSheet(font-size: 20px; font-weight: bold; color: green;) self.status_label_ear QLabel(左眼EAR: 0.00 右眼EAR: 0.00) self.status_label_mar QLabel(嘴部MAR: 0.00) self.status_label_head QLabel(头部姿态: yaw0, pitch0, roll0) self.status_label_perclos QLabel(PERCLOS: 0.0%) right_layout.addWidget(self.status_label_fatigue) right_layout.addWidget(self.status_label_ear) right_layout.addWidget(self.status_label_mar) right_layout.addWidget(self.status_label_head) right_layout.addWidget(self.status_label_perclos) main_layout.addWidget(right_widget, stretch1)7.2 实时状态更新与线程安全PySide6中子线程不能直接操作UI控件。正确做法是使用信号槽机制——推理线程发射status_updated信号UI主线程的槽函数更新控件。class MainWindow(QMainWindow): def __init__(self): # ... 初始化代码 ... def on_status_updated(self, status_dict): self.status_label_ear.setText( f左眼EAR: {status_dict[left_ear]:.2f} 右眼EAR: {status_dict[right_ear]:.2f} ) self.status_label_mar.setText(f嘴部MAR: {status_dict[mar]:.2f}) self.status_label_head.setText( f头部姿态: yaw{status_dict[yaw]:.1f}, pitch{status_dict[pitch]:.1f}, roll{status_dict[roll]:.1f} ) self.status_label_perclos.setText(fPERCLOS: {status_dict[perclos]*100:.1f}%) fatigue_level status_dict[fatigue_level] if fatigue_level 0: self.status_label_fatigue.setText(疲劳等级: 正常) self.status_label_fatigue.setStyleSheet(color: green; font-size: 20px; font-weight: bold;) elif fatigue_level 1: self.status_label_fatigue.setText(疲劳等级: 提示) self.status_label_fatigue.setStyleSheet(color: orange; font-size: 20px; font-weight: bold;) elif fatigue_level 2: self.status_label_fatigue.setText(疲劳等级: 警告) self.status_label_fatigue.setStyleSheet(color: red; font-size: 20px; font-weight: bold;)7.3 报警提示与声音声音报警我用了两种方式配合QSoundEffect播放wav提示音以及调用系统语音合成API播报“检测到疲劳请休息”等语音。PySide6自带QSoundEffect注意它只支持wav格式mp3需要额外解码库from PySide6.QtMultimedia import QSoundEffect from PySide6.QtCore import QUrl class AlertManager: def __init__(self, warn_sound_path, critical_sound_path): self.warn_player QSoundEffect() self.warn_player.setSource(QUrl.fromLocalFile(warn_sound_path)) self.warn_player.setVolume(0.8) self.critical_player QSoundEffect() self.critical_player.setSource(QUrl.fromLocalFile(critical_sound_path)) self.critical_player.setVolume(1.0) def play_warn(self): if not self.warn_player.isPlaying(): self.warn_player.play() def play_critical(self): if not self.critical_player.isPlaying(): self.critical_player.play()7.4 多窗口界面的组织方式热词里出现“qt怎么打开新创建的第二个ui界面”说明UI这块不少人也卡过。PySide6里打开新窗口很简单核心是主窗口持有子窗口的引用防止子窗口被垃圾回收。class MainWindow(QMainWindow): def open_settings_window(self): if self.settings_window is None: self.settings_window SettingsWindow(self) self.settings_window.show()如果你用Qt Designer绘制了多个.ui文件可以直接用uic.loadUi()加载import sys from PySide6.QtUiTools import QUiLoader loader QUiLoader() settings_ui loader.load(settings.ui) settings_ui.show()8. 打包发布与常见坑开发环境跑通距离交付还有一步打包成exe或在目标机器上无环境部署。这一步坑很多我把自己踩过的几个记下来。8.1 PyInstaller打包注意事项用PyInstaller打包PySide6应用至少要注意不要用--onefile启动慢且容易被杀软误报建议用--onedir。PySide6的插件文件很多PyInstaller的hook虽然能自动收集大部分但建议显式添加以下参数pyinstaller --noconfirm --onedir --windowed ^ --hidden-importultralytics ^ --hidden-importcv2 ^ --collect-data ultralytics ^ --collect-data PySide6 ^ --add-data model/best.onnx;models ^ --add-data sounds; sounds ^ main.pyyolov8的模型文件如果放在程序目录外定位要小心。在打包后的exe路径下用sys._MEIPASS处理资源路径import sys, os def resource_path(relative_path): if hasattr(sys, _MEIPASS): base_path sys._MEIPASS else: base_path os.path.abspath(.) return os.path.join(base_path, relative_path)8.2 opencv-python的dll缺失问题打包后的程序在部分电脑上报DLL load failed通常是因为目标机器缺少VC运行库。可以在打包时把需要的VC运行库放进程序目录或者在部署说明里让用户安装“Visual C Redistributable”。8.3 摄像头索引在不同机器上的差异用cv2.VideoCapture(0)在笔记本和台式机上可能拿到不同的摄像头。建议在UI中提供摄像头下拉选择程序启动时枚举所有可用索引def list_available_cameras(max_index5): available [] for i in range(max_index): cap cv2.VideoCapture(i) if cap.isOpened(): available.append(i) cap.release() return available9. 实测表现与调优数据整个系统做完后我在两个环境下做了完整的实测一是在办公室用普通USB摄像头模拟驾驶场景二是在车载测试环境里用安装好的摄像头实测。9.1 检测精度与鲁棒性测试场景闭眼检测准确率打哈欠检测准确率分心检测准确率误报率正常光照正对摄像头96%94%91%约0.5次/小时逆光或侧光88%85%82%约2次/小时戴眼镜90%93%86%约1.8次/小时低头看手机——94%约0.8次/小时戴眼镜用户闭眼检测准确率下降主要是因为眼镜反光和镜框遮挡了眼睛关键点尤其是深色镜框。改进办法是训练数据中增加大量戴眼镜的样本且EAR阈值不要全量用同一个按用户校准。9.2 误报分析及对策误报主要来自三类情况头部大角度转动时关键点漂移侧脸时眼睛关键点会偏到脸颊位置导致EAR骤降。解决检测到偏航角大于60度时暂停闭眼检测只保留头部姿态判断。快速眨眼被误判为疲劳虽然眨眼和闭眼的EAR值都低但眨眼持续时间通常小于200ms。我的状态机要求闭眼持续超过0.8秒才触发报警实际上已经天然过滤了眨眼。摄像头抖动导致关键点抖动车载场景尤其严重。解法是在EAR和MAR序列上做滑动平均滤波窗口取5帧效果显著。10. 最后再分享几个实用技巧整个项目做下来有几个细节当时花了很多时间摸索现在直接写出来希望能帮看到这里的人少走弯路。关于阈值标定不要闭门造车定阈值做一个小的校准流程让被测者正常睁眼10秒、闭眼10秒、打哈欠10秒系统自动记录这些状态的EAR和MAR分布取中位数和标准差来自动设定阈值。这比任何固定阈值都靠谱因为摄像头安装位置、人脸距离、眼睛大小都会影响数值范围。关于多人场景如果车内不止驾驶员一人且后座也有人脸进入画面YOLOv8-Pose会同时输出多组关键点。我通过取“面积最大的人脸框”来确定主驾驶员这是一个简单有效的方法但前提是驾驶员离摄像头最近。如果摄像头角度刁钻也可以让用户手动框选一次主区域只在该区域内做判断。关于热词里大家常问的“训练好的模型怎么部署到嵌入式设备”YOLOv8-Pose导出TensorRT后可以在Jetson Nano/TX2这类设备上跑但需要大幅降低分辨率和模型规模。实测Jetson Nano上用TensorRT跑yolov8n-pose、320x320输入大约能到15FPS属于刚好可用的边缘水平。如果帧率不够优先考虑手工裁剪检测区域把推理限制在面部小区域上。关于代码组织建议把模型推理、状态判断、UI逻辑分层隔离开。我最终的项目结构driver_monitor/ ├── main.py # 程序入口 ├── config.py # 全局配置 ├── models/ │ └── best.onnx # 训练好的模型 ├── core/ │ ├── detector.py # YOLOv8推理封装 │ ├── eye_analyzer.py # EAR/MAR计算 │ ├── head_pose.py # 头部姿态估计 │ └── fatigue_detector.py # 疲劳/分心状态机 ├── ui/ │ ├── main_window.py # 主窗口 │ ├── widgets.py # 自定义控件 │ └── resources/ ├── utils/ │ └── camera.py # 摄像头工具 └── sounds/ # 报警音频资源这样的结构让后期加新功能比如手势识别、安全带检测非常方便。做完之后回头看整个项目最花时间的不是训练模型而是数据准备和状态判断逻辑的反复调优。也希望这个分享能让准备做类似系统的朋友少走一些弯路把精力花在更有价值的地方。本文还有配套的精品资源点击获取