资讯动态

疲劳检测实战:从关键点检测到PERCLOS指标的系统化实现与部署优化

发布时间:2026/10/17 3:35:00 来源:尧图企业网站定制
疲劳检测这个方向我前前后后折腾了大半年踩了不少坑也积累了一些实实在在的经验。很多初学者一上来就奔着深度学习模型去了上来就训一个啥都能干的端到端网络结果数据不够、场景复杂最后效果反而不稳定。这个项目其实更适合用“拆解”的思路去做先把“疲劳”这个抽象概念拆成眼睛闭合频率、嘴巴张合、头部姿态这些可量化的信号再逐个击破最后融合判断。这篇就完整复盘一下这套实验方案从数据准备到模型训练再到部署优化把关键细节和踩过的坑都写清楚。1. 项目思路与方案选型1.1 为什么放弃端到端方案最早我确实尝试过直接用视频帧训练一个三维卷积网络把“疲劳/清醒”当二分类做。想法很直接但实际操作下来问题一堆首先是数据量根本不够视频样本动辄几万帧标注疲劳程度本来就是件主观且耗时的事几轮标注下来人先疲劳了其次是模型解释性太差它到底在看眼睛还是看嘴还是看姿势完全是个黑盒出了问题根本没法排查。当时我拿少量样本训了一版测试集准确率看着还行大概85%左右但拿到真实场景一测误报率直接起飞。后来冷静分析了一下疲劳检测这个任务和通用图像分类不太一样它强依赖时序上下文和局部细节。打个比方一个人靠在椅背上闭眼两秒钟和一个人正常眨眼两帧在单帧图像上差距很小但放到时序里就完全不同。端到端网络要把这种时序敏感性学出来需要的数据量和训练成本远超项目承受范围所以果断放弃。1.2 核心指标怎么选回到物理世界的可观测信号疲劳的典型表现无非这么几类眼皮下垂、眨眼频率下降或闭眼时间变长、打哈欠、头部无意识下垂。对应到计算机视觉里就是人脸关键点检测加几何特征计算。最经典的血缘特征有两个一个是眼睛纵横比EAR用来描述眼睛张开程度另一个是嘴部纵横比MAR用来描述嘴巴张合程度。前者配合闭眼时长阈值可以算出单位时间内的闭眼比例PERCLOS这是驾驶疲劳领域用得最广的指标。后者则用来识别打哈欠作为辅助判据。还有一类信号是头部姿态当人开始点头打瞌睡时头部俯仰角会发生规律性变化。但我不建议一开始就把姿态也加进来变量越多调参越痛苦。第一版先做眼部和嘴部跑通了再叠加姿态迭代节奏会舒服很多。我这套流程走下来最终的方案是EAR和PERCLOS为主判据MAR为辅助判据头部位姿作兜底。1.3 整体流程框架整个实验流程其实可以分成五段人脸检测、关键点检测、几何特征计算、时序窗口判定、输出告警。人脸检测负责在画面里把人框出来关键点检测负责锁定眼睛和嘴部的位置几何特征计算把关键点坐标转换成有物理含义的数值时序窗口判定负责在时间维度上平滑和累积最后根据规则输出对应的疲劳等级。这五段划分清楚之后每一段都能独立实验、独立调优。比如人脸检测掉了帧是换模型还是换输入分辨率关键点抖动严重是加滤波还是调回帧率判定阈值不合适是调整秒数还是调整百分比。这种模块化的思路后期排查问题的效率会高很多。2. 数据准备与实验环境2.1 数据集怎么搭疲劳检测实验的数据来源通常分两块开源数据集和自己采集。开源数据集专门带疲劳标注的其实数量有限当时我主要用了几个公开的人脸关键点数据集做预训练再结合自己录制的疲劳模拟视频做微调。关键点数据集的标注格式得统一起来68点、106点、人脸框坐标这些字段要提前对齐不然送进模型之前还得写一堆转换脚本。自己采集数据时有两个问题特别容易被忽略。第一个是光照白天窗前、黑夜灯光下、手机补光灯前肤色和阴影完全不一样模型很挑光照。第二个是遮挡戴眼镜的、戴口罩的、有刘海儿的这些都得覆盖到。我自己的做法是找实验室几个同学帮忙在上午、中午、傍晚、夜间四个时间段分别录制每人录3到5段每段5分钟左右的行为视频涵盖正常状态、频繁眨眼、闭眼停留、打哈欠、点头犯困这几类动作。录制的时候不需要刻意摆拍太久真实状态下的自然动作最好。关键点标注工作可以借助半自动工具先跑一版检测结果再人工修正严重偏移的帧比纯手动逐帧标注效率高不少。最终我整理出大概一万两千张图像用于微调其中约三成带有不同程度的遮挡保证模型在复杂场景下不至于崩掉。2.2 预处理和数据增强预处理里最重要的步骤就是对齐人脸。检测出人脸框之后先按两眼中心点做旋转校正把脸摆正再缩放到固定尺寸输入关键点模型。这一步如果不做同一个人的同一动作在画面不同位置时模型输出的关键点会漂移得很厉害后续算EAR的时候数值噪声会非常大。数据增强我分了三个梯队几何变换小幅旋转、平移、缩放、光度扰动亮度、对比度、高斯噪声、遮挡模拟随机的矩形遮罩和黑色圆斑。这里有个经验遮挡模拟不要做得太狠遮掉眼睛区域会让模型学到“看不见眼睛就认为疲劳”反而把判定规则带偏了。增强幅度要适可而止以不破坏关键点语义为准。2.3 实验环境配置计算资源方面我当时用的是一块消费级显卡显存8GB数据集规模不算大训练一个轻量关键点模型一次大概40分钟。推理阶段的环境则直接放到了CPU上因为这类检测最终要跑在边缘设备上不能依赖GPU。所以从模型选型开始我就刻意控制了参数量避免后期部署时发现速度不达标再返工。软件栈相对常规Python、OpenCV、PyTorch这三件套就够了。人脸检测部分可以用现成的轻量检测器关键点模型是自己改造的后面会详细说结构。推理时考虑到性能我后来把模型转换成了ONNX格式用OpenCV的DNN模块来跑省去了PyTorch的运行开销。3. 模型训练与指标计算3.1 关键点检测模型结构关键点模型我没有用太重的结构参考了移动端轻量网络的思路主干用深度可分离卷积堆叠最后接一个全连接层直接回归所有关键点的坐标。输入图像是64x64像素输出68个点的x、y坐标一共136个值。选择64x64的分辨率是经过权衡的再大一点精度能提一些但推理时间明显变长再小一点关键点定位偏差就会影响后续EAR的稳定性。训练损失函数用的就是坐标点的均方误差但这里有个细节需要注意眼睛区域的关键点坐标数值很小嘴巴和下巴区域数值很大如果直接用原始坐标做MSE损失网络会把优化重点放在大幅度值的关键点上眼睛区域的误差反而容易被忽略。我当时的做法是对每个点的误差按权重加权眼睛轮廓的8个点权重设为1.5眉毛和嘴部设为1.2其他点设为1.0。这个改动不用增加任何计算量但对眼睛状态判定的精度提升非常明显。训练过程采用了两阶段策略先在公开关键点数据集上预训练到收敛再用自采数据微调。预训练阶段前5个epoch的学习率是3e-3之后衰减到1e-3微调阶段直接降到5e-4。优化器用的Adam批量大小64总共训练了40个epoch左右。监控指标除了验证集上的坐标误差我还额外打印了眼睛区域点的平均误差这个更能反映疲劳检测关心的核心区域是否够准。3.2 疲劳指标的计算逻辑拿到68个关键点之后下一步就是计算EAR和MAR。以右眼为例取六个点眼头、眼角、上眼皮两个点、下眼皮两个点。EAR的定义是纵向距离的均值除以横向距离def eye_aspect_ratio(eye_points): # eye_points: 6个关键点的坐标 vertical_1 dist(eye_points[1], eye_points[5]) vertical_2 dist(eye_points[2], eye_points[4]) horizontal dist(eye_points[0], eye_points[3]) return (vertical_1 vertical_2) / (2.0 * horizontal)正常情况下人的EAR值大约在0.25到0.35之间闭眼时会掉到0.1以下。这个数值在不同人之间有差异但对同一个人的同一摄像头设置下比较稳定。所以实验时我做了个自适应校准开机后先采集这个人前30秒正常状态下的EAR基准值再根据这个基准动态调整判定阈值比全局固定阈值靠谱得多。MAR的计算方式类似取嘴部外轮廓左右两个角和上下唇的四个点纵向距离除以横向距离。嘴巴完全闭合时MAR接近0.15张大嘴打哈欠时会超过0.6这个区分度还是挺明显的。不过嘴部特征有个偶发干扰就是说话嘴里不停说话时MAR也会波动很大所以处理时我做了个简单的时域约束嘴巴持续张开超过一定时间才判定为哈欠而不是单帧超阈值就报警。PERCLOUS的计算则是最核心的它统计的是单位时间内眼睛闭合帧数占比。这里要处理一个问题就是眨眼和闭眼的区别。正常人一秒眨眼一到两次每次持续100到150毫秒如果按100毫秒一帧算也就一两帧的事。疲劳时闭眼会持续300毫秒以上甚至几秒。所以我把闭眼帧定义改成“EAR低于阈值且持续超过3帧”用这个过滤普通眨眼。def compute_perclos(ear_list, frame_total): closed_frames 0 streak 0 for ear in ear_list: if ear ear_threshold: streak 1 else: if streak 3: closed_frames streak streak 0 if streak 3: closed_frames streak return closed_frames / frame_total3.3 时序窗口的选取判定疲劳不能只靠某一帧得观察一段时间内的累计特征这个观察窗口的大小很关键。窗口太短比如两秒正常的眨眼也会被算进闭眼时间误报很高窗口太长比如一分钟报警太慢真到犯困出事的时候还没触发失去预警意义。我做了一组对照实验分别用5秒、10秒、15秒、30秒窗口测试。结果显示10秒窗口在延迟和准确率之间平衡最好。具体到PERCLOS当10秒内闭眼时间占比超过40%时也就是大约4秒以上处于闭眼状态判定为疲劳一级连续两个窗口都超过40%判定为疲劳二级这时候输出声光提醒。哈欠判据是10秒内检测到至少2次持续张开超过1秒的嘴部动作会作为辅助证据累加到疲劳评分里。窗口滑动的方式也有讲究用滑窗比分段独立窗口更稳。分段窗口在边界处容易产生突变上一段刚判完疲劳下一段又清零重来用户感知很突兀。滑窗则是每帧滚动更新最近的10秒数据判定结果平滑连续报警状态一旦进入不会因为某帧恢复正常就立刻解除有效防止反复报警的抖动。4. 实验结果与指标分析4.1 评价指标怎么定疲劳检测实验的评价指标不能只看准确率因为正常状态的数据在真实场景里占绝大多数无脑判“正常”就能拿到95%以上的准确率完全没有意义。所以我把重点放在这几个指标上误报率、漏报率、平均检测延迟、正检率。误报率清醒状态下被判定为疲劳的比例。这个指标直接影响产品体验误报太多用户会直接关掉系统。漏报率疲劳状态下没有被检测出来的比例。这个指标与安全性直接挂钩。平均检测延迟从疲劳状态开始到系统输出报警的时间。延迟越高预警价值越低。正检率疲劳状态中触发报警的比例与漏报率互补。测试时我准备了4段总长20分钟的模拟视频其中清醒段和疲劳段交替出现每个状态持续1到2分钟。测试人员按照脚本执行动作比如正常眨眼、刻意闭眼5秒、做连续打哈欠动作然后统计系统状态和实际状态的匹配情况。4.2 实验结果汇总经过多轮调参和测试最终系统的指标如下表所示。这个结果是在室内稳定光照、摄像头正对脸部的条件下测得的大概能说明系统的性能基线。指标数值说明疲劳状态正检率94.2%疲劳段内成功触发报警的比例清醒状态误报率5.6%清醒段内错误触发报警的比例平均检测延迟3.8秒从闭眼动作开始到报警输出单帧推理耗时CPU18毫秒关键点模型在CPU上的推理综合帧率25 FPS包含人脸检测的全流程帧率整体看正检率是可接受的但误报率还是偏高细看误报样本基本集中在戴眼镜反光和侧脸角度大的片段。眼镜反光那几帧关键点检测把镜片边缘误认成眼皮轮廓EAR被强行压低了于是判定成闭眼。针对这个现象我在后面部署优化部分专门做了处理。4.3 特征贡献的消融分析我还做了一组消融实验验证不同特征组合的重要性。只依赖EAR闭眼判据正检率能到90%但误报率明显上升加入哈欠判据之后疲劳段的检出更可靠因为打哈欠往往比闭眼出现得更早再叠加头部位姿信息之后侧脸低头状态的漏报下降了不少。有意思的是头部位姿单独用效果并不好点头动作在正常聊天中也经常出现单靠俯仰角判定疲劳会误伤。但它和闭眼特征叠加效果很好当头部持续下垂且眼睛处于闭合状态时疲劳置信度可以给得很高而头部姿态正常但眼睛闭合也可能只是普通闭眼休息置信度给中等。这种按特征置信度加权融合的策略比简单阈值投票稳健得多。5. 实时部署与常见问题排查5.1 性能优化思路实验原型是用PyTorch直接跑的测试没问题之后部署优化就提上日程了。首先把关键点模型导出成ONNX格式推理引擎换成了OpenCV的DNN模块。这一步就省掉了PyTorch全套运行库的加载时间内存占用也降了一大截。实测模型推理从40毫秒左右降到了18毫秒效果非常直观。人脸检测部分用的也是轻量检测器但它在CPU上仍然是最耗时的模块。优化措施是降低检测频率采用动态检测策略当上一帧检测到人脸时下一帧直接用跟踪算法延续人脸位置隔5帧再重新检测一次。这样既保证了对人脸移动的跟随又大幅减少了检测器的调用次数。整套流程在普通办公笔记本CPU上跑到了25 FPS左右基本满足实时需求。多线程方面把视频采集、人脸检测、关键点推断、疲劳判定分成了四个线程用队列传递数据。采集线程控制帧率避免过度占用CPU检测和推断线程共享一个工作池判定线程只处理最新的结果。需要注意的是采集和检测之间不能用阻塞队列否则某一帧处理慢了会拖垮整个流程的帧率。我用的策略是丢弃旧帧始终处理最新图像。5.2 眼镜反光与光线变化眼镜反光这个问题非常恶心镜片边缘的反光会在关键点附近形成伪轮廓而且反光的位置随着头部转动不停变化导致EAR值忽高忽低。最终我用了两个对策第一是训练数据里加入大量戴眼镜的样本模型逐渐学会忽略镜框边缘的干扰第二是加了一个中值滤波对连续5帧的EAR值做平滑滤掉单个异常帧的跳变。两个对策叠加之后戴眼镜场景的误报率下降了一多半。光照变化是另一个考验窗户边逆光时整个脸部会过曝关键点基本找不到夜间只有屏幕亮光时人脸很暗也会影响检测。解决方案是接入一个简单的自动曝光补偿模块基于人脸区域的平均亮度动态调整摄像头的曝光参数让人脸的亮度和对比度维持在一个舒适的区间。这个模块成本很低、收益很高强烈建议加上。阴暗环境下的补充方案是启用红外摄像头。红外模式下光照色彩信息基本没用但关键点的灰度轮廓依然清楚检测效果不受影响。如果项目预算允许优先选自带红外补光的摄像头夜间体验提升非常明显。5.3 排查速查表现象可能原因排查方法检测不到人脸人脸太小或画面过曝检查人脸框尺寸和曝光参数EAR值频繁跳到0眼镜反光造成误判加中值滤波检查训练集中是否有遮挡样本侧脸时关键点漂移数据集缺少大角度样本采集增加左右转头数据适当加旋转增强报警抖动频繁窗口太短或阈值过紧增加窗口长度放松闭眼阈值加入滞回区间CPU占用过高检测器频繁调用检测间隔调整跟踪模块启用5.4 其他容易踩的坑摄像头帧率不稳定是个隐蔽问题。采集帧率如果忽高忽低PERCLOS计算的时间基准就不对因为每一帧代表的时间长度不一样。我踩过这个坑测试时用一台帧率波动大的摄像头疲劳判定完全不准数据看起来闭眼很频繁但实际是帧率抽风导致的。解决办法是给每帧打上精确的时间戳统计时用真实时间长度而不是帧数。多目标场景也要考虑。画面里出现多张人脸时系统只取最大的人脸作为检测主体这样能避免把远处模糊的人脸也纳入计算。如果以后要做多人同时监测需要为每个人维护独立的状态缓存复杂度会上升到另一个量级现阶段不建议一上来就做。6. 个人复盘与扩展方向6.1 实验教训总结整个项目做下来一个很深的体会是“数据比模型重要”。关键点模型的网络结构并不复杂真正拉开效果差距的是训练数据的覆盖面和标注质量。项目中期有一版模型结构改得很花哨但测试效果不升反降回补数据之后反而明显改善。扎实的数据基础是一切花哨技巧的前提。阈值标定要按人自适应。EAR和PERCLOS的绝对值存在明显的个体差异有人天生眼睛大、EAR基准值就高有人眼睛小、闭眼和睁眼的差距本来就不大。固定阈值方案上线后误报会很高一定要做开机自适应校准采集每个人清醒状态下的正常数据来动态确定阈值。这个改动能把跨用户误报率降低30%左右。6.2 后续可以扩展的方向这套框架的模块耦合度很低每一块都能独立升级。关键点检测模型可以换成参数更小的新结构人脸检测也能替换成效率更高的算法规则判定部分则可以逐步引入轻量级时序分类器做端到端的决策融合。如果后面有机会采集更长周期的数据还可以加入用脑电信号做真值校准的对照实验验证视觉特征的准确性边界。我以前把手头这套系统接到了一个低功耗开发板上跑稍微裁剪了一下模型宽度帧率也能稳在15帧左右。如果对部署体积敏感可以考虑对模型做量化处理权重压到8位整数之后体积砍到四分之一推理速度能再提一截代价是精度掉个零点几个百分点对疲劳判定影响很小。最后说一个实操建议就是日志记录一定要做好。每帧的EAR值、MAR值、判定状态、人脸位置都要落一份结构化日志。后头调参的时候只有翻日志才能定位到具体是哪些样本导致误报空对空猜原因效率极低。加了日志之后我调参的时间至少省了一半很多时候看一眼日志就知道问题出在哪不需要反复跑实验猜了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑