深夜跑完一趟长途,把车停进服务区的时候,我的眼睛已经酸到快睁不开。那一刻我意识到,市面上很多疲劳驾驶检测产品,和我自己研究时的体验差距有多大。基于深度学习的疲劳驾驶检测系统,不是简单拿个摄像头对着人脸、判断“眼睛闭没闭”就行,它要解决的是连续时间序列下的状态判定、复杂光照下的鲁棒识别、以及足够低的算力开销。这篇文章把我做这套系统的思路、指标、模型选型和踩过的坑全部写出来,希望对想做主动安全、车队管理或者智能座舱相关功能的开发者有帮助。
1. 疲劳驾驶检测的核心诉求:为什么非得用深度学习
1.1 从“打哈欠检测”到“连续状态判定”的进化逻辑
最早接触这个方向时,很多人给我描述的需求是“检测打哈欠”和“检测闭眼”。这两个需求听起来简单,实际上是个陷阱——你盯着一个人看,他眨眼的时候眼睛也是闭的,他和旁边人说话的时候嘴巴也在张大,你跟他说“哈欠检测”他只会觉得你很天真。真正的疲劳检测,要回答的问题不是“这一刻闭眼没有”,而是“过去一段时间里,这个人的警觉度是不是在持续下降”。
所以系统的目标从“单帧分类”变成了“状态估计”。一个合格的疲劳驾驶检测系统,至少要能连续输出三类状态:清醒、轻度疲劳、重度疲劳,并且能在重度疲劳出现时提前若干秒预警。这个提前量非常关键,疲劳不像碰撞,它是一个逐渐累积的过程,深度学习在这里的价值,就是利用时间序列信息把这些细微变化找出来。
1.2 传统CV方案的局限与深度学习方案的三个优势
我在早期原型里试过纯传统方案:先用肤色模型圈脸部区域,再用椭圆拟合眼睛轮廓,靠“眼睑闭合面积”判断闭眼。这套方案在实验室的白墙背景、均匀灯光下效果还行,一旦上路就崩——逆光时肤色模型直接失效,戴墨镜时眼睛区域全黑,夜里没补光时画面噪声能把椭圆拟合结果抖成筛子。
后来转向深度学习,核心原因有三个。
第一,特征提取的泛化能力明显更强。卷积网络能自动学习到“眼睛区域应该长什么样”,而不是靠人为规定的颜色或边缘规则,面对逆光、戴镜、肤色不一的情况,鲁棒性高出一个量级。
第二,能处理姿态变化。司机在驾驶途中不可能一直正脸对着镜头,头部旋转、低头看仪表盘都是常态。传统方法在头部转角度较大时,关键点检测基本失效,而基于深度学习的人脸关键点模型配合适当的训练数据,大角度姿态下依然能给出可用信息。
第三,时间序列建模的天然优势。疲劳本身就是时序概念,深度学习里的循环结构或者滑窗序列模型,能把连续几十帧的特征变化规律学到手,这是传统阈值法很难做到的。
2. 系统整体架构与关键指标拆解
2.1 一条完整的数据流长什么样
我最终采用的系统架构可以用一句话概括:单目可见光摄像头采集视频流,深度学习模型先做人脸检测与关键点定位,从关键点坐标算出若干疲劳指标,再用滑窗或序列模型输出疲劳状态分级,最后触发分级预警。
整个流程大致是:
- 视频帧采集:车载摄像头,默认帧率按20到30帧处理,分辨率不需要太高(640乘480到1280乘720足够)
- 人脸检测与跟踪:每帧先做人脸框定位,帧间用跟踪器衔接,避免每一帧都跑一遍全图检测
- 人脸关键点检测:输出眼睛、嘴巴、眉毛、下巴等关键点坐标,通常是68点或更密集的网格
- 疲劳特征计算:从关键点坐标计算眼睛闭合程度、嘴部开合程度、头部姿态角等连续指标
- 时间序列判定:把连续N秒的指标输入统计模块或序列模型,输出当前疲劳等级
- 预警策略:分级触发声音、震动或视觉提醒,同时抑制误报
这里面每一步都有独立的技术难点。人脸检测现在是相对成熟的部分,真正决定系统好不好用的,是后面这几步怎么做。
2.2 PERCLOS、EAR、MAR、头部姿态:四个疲劳指标各有各的脾气
疲劳状态不是单一指标能定义的,我的经验是至少融合三类信号:眼部信号、嘴部信号、头部姿态信号。
PERCLOS(单位时间内闭眼时间占比)是学术界最认可的疲劳指标。标准的PERCLOS定义又分P70和P80,其中P80指瞳孔被眼睑遮挡超过80%的时间占比,我们工程上简化处理为“眼睛处于闭合状态的时间比例”。实践里我习惯用单位窗口内闭眼帧数除以总帧数来近似。需要注意,眨眼时眼睛也会短暂闭合,所以闭眼持续时长这个信息比单纯闭眼占比更有区分度。
EAR(眼部纵横比)是计算PERCLOS的前置特征。它用眼周关键点坐标算一个比值,不需要图像像素分割,完全基于几何。具体算法是取眼睛外角点、内角点、上下眼睑点,计算垂直方向欧氏距离和水平方向距离的比值:
[ EAR = \frac{|p_2 - p_6| + |p_3 - p_5|}{2 |p_1 - p_4|} ]
睁眼时EAR在0.25到0.35之间,闭眼时骤降到0.1以下。这个比值天然对相机距离不敏感,因为距离变化会同时缩放分子分母——这是它最大的价值。
MAR(嘴部纵横比)用来捕捉哈欠信号,思路和EAR类似。取嘴部四个关键点的坐标算开口比例,哈欠时MAR明显升高且持续时间长,说话时虽然也会升高但往往是短促的脉冲。要区分这两个场景,必须靠时间维度上的持续长度,而不是瞬时值。
头部姿态能提供额外的强信号。犯困时司机普遍出现“点头”动作——头部缓慢下沉然后突然回正,或者长时间低头。通过人脸关键点计算头部的pitch、yaw、roll三个欧拉角,pitch角连续下沉并伴随回弹脉冲,就是很典型的疲劳点头模式。头部姿态还能验证眼部和嘴部信号的可信度:当人脸大角度侧转时,关键点坐标可靠性下降,此时眼部指标就不该作为主要判定依据。
2.3 阈值这件事:经验值起步,标定收尾
指标算出来之后,最朴素的做法是设阈值:EAR低于某个值超过多少秒算闭眼,MAR高于某个值超过多少秒算哈欠,pitch角下沉超过多少度算点头。
但是阈值不能拍脑袋定。我的流程是先用文献里常见的经验值起步(例如EAR小于0.2视为闭眼,PERCLOS大于0.4视为疲劳风险),再靠实际录制的场景数据做统计标定。录制数据时要把不同光线、不同人种、戴不戴眼镜的情况都覆盖到,然后画出每个指标的分布直方图,观察清醒和困倦状态下分布的重叠区域,把阈值设在重叠最小的位置。
另一个关键问题是个体差异。同一个人戴眼镜和不戴眼镜,EAR基准值能差0.05以上;单眼皮和双眼皮在闭眼判定上也存在差别。我那会儿从“一套全局阈值”改成“开机30秒自适应标定”,也就是系统启动后先采集驾驶员正常状态下的指标基线,再基于基线动态调整阈值,误报率下降非常明显。
3. 人脸关键点检测的模型选型与实现细节
3.1 候选方案对比:框架与模型怎么选
人脸关键点检测是整个系统里工作量最大的一环。我对比过几条路线,各有优缺点。
| 方案 | 关键点数量 | 推理开销 | 稳定性 | 适用阶段 |
|---|---|---|---|---|
| HOG加线性分类器加回归树 | 68点 | 极低,CPU可跑 | 正面好,大角度差 | 原型验证 |
| 基于深度网络的回归模型 | 68点到上百点 | 中等,需要GPU或NPU | 姿态适应性强 | 产品化主力 |
| 密集网格模型 | 数百点 | 较高 | 面部形变感知强 | 需要精细嘴眼边界时 |
这里我想重点说下密集网格方案。它输出的人脸网格点非常密集,嘴眼轮廓细节丰富,做EAR和MAR计算时精度确实更高。但代价也很现实:一方面推理帧率在车载嵌入式平台上容易掉到10帧以下,另一方面密集点存在一个工程隐患——坐标输出在小区域内有抖动,直接套用EAR公式时结果会毛刺很多,必须做平滑滤波。
3.2 关键点的工程化处理:对齐、坐标归一化与数据增广
拿到关键点坐标不是终点,工程化处理决定了算法能不能稳定工作。
首先是对齐。人脸检测框偶尔会抖动,直接使用原始坐标,指标曲线会出现和疲劳无关的突变。我的做法是先用标准人脸模板做相似变换对齐,把所有关键点转换到统一坐标系,再去计算EAR、MAR。对齐还能部分抵消摄像头安装角度带来的偏差,司机稍微偏头时指标也不会剧烈变化。
其次是坐标归一化。关键点坐标要除人脸框的宽度和高度,让特征不受画面中人脸大小影响。但要注意,EAR这类比值本身已经对尺度不敏感了,归一化主要是为后续输入神经网络模型时统一量纲。
最后是数据增广。训练模型时我用了很多图像增强:随机亮度扰动模拟逆光和隧道切换,随机旋转模拟头部倾斜,随机遮挡模拟方向盘遮挡和口罩,这些增广直接决定了模型在实车环境里的表现。很多人忽略亮度扰动,结果模型在实验室表现良好,一上夜路就看不清眼睛。
3.3 我在实际项目中怎么组合这些方案
我的结论是:原型阶段不要自己训模型,先用现成的开源检测器和关键点模型把完整流程跑通;产品阶段再考虑蒸馏、量化和定制化训练。
原因很简单,疲劳检测系统的瓶颈往往不在模型精度,而在数据、指标融合和决策逻辑。前期的核心任务是快速积累真实验场景下的指标分布数据,而不是纠结哪个关键点模型的精度高了零点几个百分点。
等到流程稳定,我会做三件事:第一,用自己录制的脏数据(夜间、逆光、墨镜、口罩)对开源模型做增量微调;第二,把大模型蒸馏成适合嵌入式部署的小模型;第三,在端侧用TensorRT或者NPU工具链做Int8量化,保证20帧以上的推理速度。
4. 疲劳判定的时间序列建模:单帧识别之外的另一半工作
4.1 为什么单帧结果不可靠
疲劳检测系统最容易翻车的地方,就是拿单帧结果直接下结论。我举三个真实场景你就明白了:
- 正常眨眼。眼睛一闭一睁只有两三百毫秒,单帧抓到眼睛闭合就直接判定疲劳,那你开车经过一棵树影晃一下都会疯狂报警。
- 低头看导航。司机低头两三秒,头部姿态显示pitch角很大,和疲劳点头很像,但人家只是在看路线。
- 说话和哈欠。说话时嘴巴也会张开,MAR瞬时值完全可能超过哈欠的阈值,持续时间才是区分关键。
所以单帧指标只能算“事件”,不能算“状态”。状态必须由一段时间内的事件统计得出。
4.2 滑窗统计与LSTM两种路线的取舍
我在系统里先后试过两条技术路线,各有各的适用场景。
第一条是滑窗统计路线。取最近5秒为一个窗口,统计窗口内的PERCLOS值、眨眼频率、平均EAR、MAR峰值次数、头部点头次数,再用一组逻辑规则做判定。这条路的好处是计算开销几乎为零,CPU顺带就能算完,规则透明,调试容易。缺点是需要人工设计特征组合和规则逻辑,不同场景之间的迁移能力较弱。
第二条是深度序列模型路线。把连续几十帧的EAR、MAR、头部姿态角组成一个时间序列窗口,输入到LSTM或TCN结构中,直接输出疲劳等级。这条路的优势是模型能自动学到“睁眼时间变短、眨眼频率变低、头部逐渐下沉”这类跨指标组合模式,比人工规则更细腻。代价是需要更多标注数据,而且模型在实车环境里的行为不如规则那么可解释。
我最终的方案是混合结构:先用滑窗统计提取事件特征,再把这些统计特征输入一个轻量分类器或小规模序列模型。这么做既保留了规则的稳定骨架,又让深度学习负责边界模糊的部分。
4.3 序列模型的训练数据与标签策略
序列模型的训练数据是整条链路里最耗时的部分。我的做法是搭建一个简易的驾驶模拟环境,让受试者按照规定流程进行“正常驾驶-轻度疲劳-重度疲劳”三个状态的录制。过程中用摄像头实时采集人脸信号和时间同步的标签。这里有个痛点:疲劳状态是主观的,所以在标注时我用了多级标签而不是二分类。
- 标签0:清醒,眼睛睁大,头部稳定,眨眼频率正常
- 标签1:轻度疲劳,眨眼变慢,偶尔闭眼超过0.5秒,有轻微点头
- 标签2:重度疲劳,频繁闭眼,头部明显下垂,呼吸节奏变化
标注策略上,我用滑动时间窗口给每一段序列打标签,而不是给单帧打标签。因为单帧的“闭眼”完全可能是正常眨眼,窗口内闭眼的密集程度才能反映疲劳状态。这个细节非常重要,很多新手把标签打到单帧上,训练出来的LSTM学到的是“闭眼检测器”,而不是“疲劳状态检测器”。
5. 真实道路场景下的稳定性和性能问题
5.1 光线、遮挡、口罩、眼镜——脏数据攻防战
跑完实验室测试,我信心满满地装到实车上,结果第一次夜间测试就被现实教育了。整理下来,主要有四个脏数据来源。
光线剧烈变化。白天进隧道是典型的场景,画面亮度在零点几秒内骤降好几个数量级。人脸检测框会一时间丢失,EAR和MAR全部变成无效值。对付这种情况,一方面要做图像自适应增强,另一方面要在决策逻辑里加入“置信度门控”——人脸检测置信度不足时,不让指标参与统计。
逆光和后视镜反光。逆光时人脸区域过曝,眼睛细节完全丢失。我在一次测试中发现,带偏振光的眼镜片会在某些角度反射出强光斑,人脸关键点模型误把光斑当成眼睛轮廓,EAR计算出奇葩值。最终靠的是数据增广加“眼睛区域局部曝光校正”,也就是先定位眼部小区域,再做局部直方图均衡化。
口罩和墨镜。口罩会让所有嘴部指标失效,墨镜会让眼部信息完全拿不到。这种情况下系统必须学会切换特征来源:没有嘴部信号时,就靠头部姿态和闭眼时长判定;没有眼部信号时,就靠头部姿态和方向盘操作间接推断。这套冗余设计,我建议从第一天就做进系统架构里。
5.2 边界条件与误报抑制
误报是疲劳检测系统落地时最招人恨的问题。一个系统检测不出来疲劳用户还能忍,但正常聊天、看导航、听音乐时频繁报警,用户第一反应就是拆掉它。
我采用了一套多级防误报逻辑:
- 时间连续性确认:短暂异常不触发判定,至少持续1.5到2秒的可疑状态才进入候选事件
- 事件组合确认:多个指标同时异常才升级疲劳等级,单一指标异常先记录,不报警
- 驾驶员交互重置:驾驶员在报警后有明显的抬头、睁眼、肢体动作时,系统自动重置告警计数器,避免连续打扰
- 场景感知抑制:车辆低速行驶、堵车停车等状态下,疲劳报警的优先级降低,因为此时风险不如高速行驶高
这套逻辑的核心思想是:疲劳识别要宁缺毋滥地报警,但一旦报警就要足够可靠。用户被误报打扰几次,整个系统就会被关掉;系统真正识别出疲劳却没有报警,用户又会怀疑它的价值。
5.3 算力预算与边缘部署策略
车载系统的算力预算非常紧张。市面上的车载设备,算力高的有专用NPU,算力低的甚至就是一颗普通手机级别处理器。我做的性能优化顺序如下:
- 分辨率裁剪。人脸检测不需要全分辨率帧,把原图缩小到合理尺寸后检测,检测框放大后再裁出眼部区域做关键点计算
- 模型量化。先做FP16,再尝试INT8,图像类模型在INT8下精度损失通常可控
- 模型蒸馏。把教师网络的知识搬到小模型里,精度和速度的平衡比直接用小模型好很多
- 帧率控制与跳帧。关键点模型没必要每帧都跑,可以两帧或三帧跑一次,中间帧用跟踪结果补齐
- 线程并行化。摄像头采集、人脸检测、关键点计算、指标统计并行处理,流水线化避免串行延时
我在某次部署时,用INT8量化把关键点模型的推理时间从约30毫秒压到约10毫秒左右,直接把整体帧率从12帧拉升到25帧。对视觉疲劳检测任务来说,帧率低于15帧时眨眼过程会丢帧,严重影响闭眼检测的连续性和精度。
6. 这几次迭代里我踩过的坑和调参心得
6.1 数据标注阶段埋下的雷
第一次训练序列模型时,我图省事,直接用现成的驾驶模拟数据标注工具生成标签。训练出来的模型在验证集上精度很高,上线后一塌糊涂。复盘发现是标签同步出了问题:模拟器数据和摄像头数据有约500毫秒的延迟,视频帧对应的实际驾驶状态标签整体错位了半秒,而疲劳判定又对时间窗口非常敏感。
从那以后,我做数据采集时强制要求三件事:所有信号统一时间戳、定期用“抬手瞬间”做标定对齐、标签数据单独人工复核。数据是否干净,直接决定模型的极限,模型结构反而是后话。
6.2 看似简单的阈值直接决定体验
另一个坑在阈值标定上。我用PERCLOS大于0.4作为疲劳判定条件,测试时发现某些司机在正常驾驶状态下,眨眼频繁、单次闭眼时间稍长,PERCLOS指标就虚高。后来把判定条件改成“PERCLOS大于0.4且持续闭眼超过2秒”,误报立刻降下来。
由此我总结了一个规律:指标数值加持续时长,比单一数值阈值抗噪得多。任何容易受正常行为干扰的指标,都要加上时间约束条件,这是疲劳检测系统最值得做的一步“工程调优”。
6.3 最后想分享的一件事
如果你也要做类似的疲劳驾驶检测系统,我的建议是先别急着上复杂模型。把数据采集方案、指标定义、阈值标定和防误报逻辑先搭起来,用最简单的人脸关键点模型跑通整条链路,拿到真实数据后再升级模型。这个方向的难点一直都不是“用多先进的算法”,而是“如何在两年三百六十五天的真实路况里,一遍二遍三遍地不误报不漏报”。
我见过不少团队把精力花在把检测模型从99%精度提到99.1%上,却忽略了系统在弱光、遮挡、个体差异导致指标失效时如何兜底。疲劳驾驶检测系统本质上是一套需要平衡漏报和误报的工程系统,先让它在脏数据里稳定工作,再考虑精度指标,这条路走起来会顺畅得多。