交付群里有个车主直接甩了两句话:DMS能不能默认关掉?每次揉个眼睛就提醒疲劳驾驶,一天弹十几次,烦死了。这条消息之后的48小时里,我翻了后台的报警日志、事件抓拍、模型推理结果,又拉着测试同事在车上反复复现“揉眼睛”,最后基本确认了一件事:用户关掉DMS不是不需要它,而是我们把它用错了地方——在疲劳判断逻辑里,揉眼这个再普通不过的动作,被模型当成了闭眼疲劳。
这篇文章不聊宏大叙事,就把这个问题从现象到原理、从根因定位到优化落地完整拆一遍。如果你是做座舱感知、DMS/OMS算法、智能座舱体验的,或者正在被各种误报折腾得焦头烂额,这篇应该对你有用。
1. 用户关掉DMS:一次售后反馈牵出的误报漏斗
先说当时的背景。这个DMS功能已经量产上车一个季度,前期的疲劳报警准确率数据一直还行,至少在产品汇报PPT上是这样的——测试场景里各种戴眼镜、眯眼、打哈欠的case都能命中。但交付群里这条消息直接打了我一个措手不及,因为它在提醒我:实验室的准确率,和用户真实场景里的体验,完全不是一个维度。
我做的第一件事不是改算法,而是去拉数据。当时我们的DMS系统是具备云端事件回传能力的,报警事件、抓拍图片、特征值置信度都会脱敏后上传。我按车型、版本、时间拉了一个月的埋点,结果出来之后心里凉了半截:
- 开启过DMS的用户中,约有四分之一在两周内手动关闭了这个功能。
- 关闭操作的时间分布里,有明显的高峰期,集中在连续开车的第40到70分钟。
- 关闭前24小时内的平均报警次数,比持续开启用户高出接近3倍。
- 更扎心的是,有部分用户的报警事件截图里,抓拍画面上明显能看到手部动作——揉眼、擦脸、挠额头。
也就是说,这不是个例,而是一个规模化的误报漏斗:用户因为动作误判被反复打扰,最终选择把整个功能关停。关停之后,即便真的遇到疲劳状态,系统也不会再提醒了。这是最让我难受的地方——功能本身的价值被误报抹掉了,用户没有享受到DMS带来的安全兜底。
顺着这个漏斗继续挖,我发现误报警告的类型高度集中。把所有报警事件按触发快照逐帧标注,揉眼相关动作占比超过一半,剩余的是打哈欠、说话、吃零食、调整眼镜这类伴随手部移动的case。真正符合疲劳长时闭眼特征的报警,反而占比不高。
所以结论从一开始就清楚了:DMS不是被用户抛弃的,是被“单帧瞬时误判”逼走的。后面所有工作,都围绕这个结论展开。
2. DMS的疲劳判断逻辑:为什么“揉眼”会被当成“闭眼”
要解决问题,先得搞清楚DMS在车上到底是怎么工作的。DMS的全称是Driver Monitoring System,驾驶员监控系统,核心任务就是通过摄像头捕捉驾驶员的面部状态和头部姿态,实时推断驾驶员是否处于分心或者疲劳状态。绝大多数量产方案走的是红外摄像头加补光方案,这样在夜间、逆光、隧道里都能稳定成像。
从功能架构上看,DMS的核心链路大致分四层:
- 图像采集与预处理:IR摄像头抓取驾驶员面部图像,做人脸框检测,校正光照和姿态。
- 面部关键点定位:输出眼睛、眉毛、嘴巴、鼻尖等关键点坐标,以及头部六自由度姿态角。
- 状态特征提取:基于关键点计算眼睑开合度、眨眼频率、瞳孔特征、嘴巴张合度、头部倾斜角度等。
- 疲劳/分心判定:把特征输入分类器或阈值规则,输出疲劳等级和报警信号。
疲劳判断最常用的一个指标是PERCLOS——单位时间内眼睛闭合时间所占的比例,这是学术界和工业界公认的疲劳表征指标。通俗讲就是在一段时间里,眼皮完全盖住眼球的帧数越多,疲劳概率越高。此外还会综合连续闭眼时长、打哈欠频率、头部低垂角度等信号,做多维度打分。
理解了这套逻辑,再回头看揉眼误判,你就能看到问题出在哪了。
揉眼这个动作,在摄像头画面里实际上是一系列让算法“难受”的瞬间叠加:手掌大面积覆盖脸部区域,把眼睛、鼻子这些关键点全挡住了;手部按压眼睑时,眼皮的形状被压得跟用力闭眼非常像;揉完眼睛后,因为刺激导致的短暂眯眼和眨眼,又和疲劳状态下的眼睑下垂很接近。更要命的是,揉眼动作往往伴随低头或侧头,头部姿态角瞬间变化,这在很多模型里会被当作“驾驶员支撑不住头部”的证据。
人在揉眼时,手部遮挡、眼睑变形、头部偏转这三个特征一把全占了,模型不判疲劳才怪。我后来在工程复盘里,把真实疲劳和揉眼动作的特征对比拉了一张表,越看越觉得这是个典型的感知盲区:
| 特征维度 | 真实疲劳表现 | 揉眼动作表现 | 模型视角下的混淆点 |
|---|---|---|---|
| 眼睑开合度 | 持续低开合度,闭眼时间长 | 按压时眼睑闭合、随后快速眨眼 | 都会被识别为“闭眼” |
| 手部位置 | 很少长时间遮脸 | 手部覆盖眼部区域 | 眼部关键点置信度下降/丢失 |
| 持续时间 | 数秒至数十秒,渐进式 | 2到5秒的短时动作 | 单帧特征相似度极高 |
| 头部姿态 | 持续低头,恢复缓慢 | 揉眼时抬头或侧头 | 姿态角变化被归为疲劳倾向 |
| 恢复模式 | 闭眼状态维持后缓慢恢复 | 揉完后快速恢复常态 | 阈值窗口捕捉到瞬时状态 |
这就暴露了一个更深层的问题:很多DMS疲劳模型把“眼睛闭合”当成一个静态的单帧事件来处理,而忽视了“时间上下文”和“手部遮挡上下文”。眼睛被手遮住,和眼睛主动闭合,在单帧图像上就是近乎一样的。模型在单帧上加再多的注意力机制,也很难把这两个动作区分开,除非你在数据、模型结构或策略规则上做出明确约束。
当时我们的模型用的是2D关键点加CNN特征提取,对遮挡的鲁棒性本来就一般,再加上训练数据里几乎没有“揉眼”这类手部遮挡样本,模型对这个场景的认知完全是盲区。总结下来就是三个字:没见过。
3. 误报根因定位链路:从一段报警日志到模型缺陷
确定方向后,我们没有急着改代码,而是走了一条完整的定位链路。这里强烈建议所有做感知算法的朋友,遇到误报先沉淀定位方法,别上来就调阈值,不然你只是在掩盖问题。
定位链路的第一步是回放报警事件。我们的后台可以把每一次报警对应的触发前后共15秒视频和抓拍帧拉下来。我随机抽了80条揉眼误报事件,一条一条看,然后把报警时刻前3秒、报警时刻、报警后3秒的状态分别标注出来。
标注完我发现一个规律:几乎所有的误判都发生在“手部完全覆盖眼睛”的那一帧前后。模型给出的眼睛闭合概率在那一帧会飙到0.9以上,但随着手移开,闭合概率立刻回落到正常水平。也就是说,误报的核心触发条件不是持续闭眼,而是“眼睛区域被遮挡导致特征丢失,模型把丢特征当成睁不开眼”。
第二步是单帧特征分析。我把报警帧的中间层特征图拉出来,对齐到人脸上看模型到底在关注什么区域。这里当时用的是Grad-CAM可视化,你可以理解为看模型是哪些特征“一锤定音”做了疲劳判断。结果显示,掺入揉眼的报警帧里,模型主要激活的区域集中在手背和眼部轮廓的交界处。换句话说,模型根本没有提取到“眼睑状态”的正确语义,它是靠“眼睛那一带出现了异常纹理”来判疲劳的。这是一个非常危险的信号,意味着模型学到的特征和人的直觉理解完全对不上。
第三步是数据分布体检。我们把训练集和线上误报样本做了相似度对比,结论让人头大:
- 训练集里“闭眼”样本大多是干净的闭眼,没有手部遮挡,也没有揉眼带来的眼周皮肤褶皱。
- 训练集里“手部遮挡”样本极少,几乎没有“手碰眼睛”这个类别。
- 线上实际场景里,用户揉眼、擦汗、挠痒时产生的画面形态,和训练分布完全不同。
这个分布上的错位直接解释了模型行为:它把“眼睛区域异常”当成了“疲劳”,而不是把“眼睛闭合”当成“疲劳”。
最后一步是策略层面验证。我拿这一批线上误报帧去跑当时的报警判定流程,把每个环节的分数打印出来:疲劳分数、闭眼帧数、触发时刻。结果发现当时疲劳判定用的策略是“最近10秒内接近100帧里,闭眼概率大于0.8的帧数达到一定数量就报警”。揉眼动作虽然持续时间不长,但手遮眼睛的那两三秒里,帧数足够密集,轻松达到触发条件。再叠加头部姿态异常加分、面部关键点置信度低导致的分数抖动,直接就跨过了报警门槛。
至此,根因定位完成。整理下来是三层问题:
- 数据层面:训练集缺少揉眼、手部遮挡、眼周异物等负样本,模型对“眼部被遮挡”无抵抗力。
- 模型层面:单帧CNN模型无法利用时间上下文区分“瞬态遮挡”和“持续闭眼”,手部区域干扰特征被误学习。
- 策略层面:报警逻辑过于灵敏,对短时高置信度闭眼缺乏冷却与持续验证机制。
4. 三管齐下的优化方案:数据、模型、策略全都得动
问题拆到这三层之后,优化方向就清楚了。我直接说结论:只调策略阈值是能降误报,但会把真实疲劳的检出率也一起拉下来;只加数据也能缓解,但模型结构不改,遮挡场景还是会有漏网之鱼。所以我们的实际做法是三条线并行,按优先级推进。
4.1 数据层:把“揉眼”从干扰变成行业标准样本
数据是最基础也是见效最直接的一步。当时我们做了一套针对性的数据补采方案:
- 组织真人在实车环境下模拟各种手部动作:揉眼、快速眨眼、擦脸、挠头、摸眼镜、用手背蹭额头。
- 覆盖不同光照条件,白天逆光、黄昏侧光、夜间红外、进出隧道瞬间。
- 覆盖不同人脸特征:戴近视镜、戴墨镜、戴口罩、有刘海、络腮胡。
- 把遮挡时长也做区分,从1秒以内的快速遮挡到5秒以上的持续遮挡。
补采之后,关键是标注规范。这里的坑在于,标注人员很容易把“手遮住眼睛”标成“闭眼”,因为画面里确实看不到眼球。我们的标注标准是单独增加一个“眼部遮挡”类别,并明确三类的关系:眼部可见且闭合,才叫闭眼;眼部被手部或异物遮挡,叫遮挡;眼部可见但眼睑形态异常,属于揉眼衍生状态。三者同时存在的场景也要单独标出来。
数据量上,我们补了大约3万张真实的揉眼/遮挡标注帧,又用图像混合、随机遮挡、光照抖动做了2倍的数据增强,最后把负样本比例从原来的不足3%提升到接近20%。这个比例没必要无限加,因为负样本过多会让模型变得过于保守,反而漏报真实疲劳,20%在这个场景下算是平衡点。
4.2 模型层:给模型补上“时间上下文”和“手部信息”
数据补上来后,模型结构也必须改。我们当时做了两个关键改动,后来验证都非常有效。
第一个改动是引入时序建模。具体做法是把单帧输入改成连续帧序列,用轻量级GRU或者3D卷积的方式提取时序特征。模型不再只看“当前这一帧眼睛是否闭合”,而是会综合前几帧的变化趋势:如果眼睛从正常到闭合再到恢复,且整个过程只有两三秒,模型会倾向于判断为瞬态遮挡;如果眼睛长时间处于低开合度,且伴有头部姿态持续下沉,才判定为疲劳。
这个变化的本质是:疲劳是一个“状态”,而揉眼是一个“事件”。状态具备持续性,事件是短时的,用时间窗口去看,两者的边界一下子就出来了。当时我们用的是8帧时序输入,约0.3秒到0.5秒的上下文窗口,已经能把大部分快速揉眼动作滤掉;后续又尝试16帧,效果进一步提升,但模型量级和延迟略有上升,所以最后落地选了8帧。
第二个改动是增加手部关键点检测与遮挡感知分支。头部和手部在同一个图像坐标系下,模型单独输出一组手部关键点,并把“手部是否与面部区域相交”作为辅助特征送入疲劳分类头。这样模型在提取到眼部低开合度特征的同时,也会看到“这里有一只手掌”,两个信号互相打架,模型就知道这不是单纯的疲劳闭眼。
这个多任务学习的设计,代价是模型参数量增加大概20%到30%。但DMS系统运行在座舱域控制器上,通常有独立的AI算力,和智驾共用芯片的场景需要单独评估算力余量。我们当时跑在8TOPS左右的平台上,整体单帧推理耗时依然能控制在20毫秒内,满足实时性要求。
4.3 策略层:报警逻辑加上冷却机制和持续验证
模型再强,策略也得跟上。报警策略的目标是减少对用户的无效打扰,它和模型精度是互补关系,一个管“判得准”,一个管“想清楚了再说话”。
我们在报警触发逻辑上做了四处调整,这些都是可以直接抄作业的。
第一,设定最短持续判定时间。疲劳分数超过阈值的状态需要连续维持至少3秒才允许触发报警,揉眼那种一两秒的瞬态遮挡直接被按住。
第二,加入冷却时间。一次报警触发后,5分钟内不再重复报警,除非疲劳分数进一步升高到更严重级别。这个冷却机制非常关键,能避免用户在被提醒后还在揉眼睛时,系统连环弹窗导致情绪爆炸。
第三,对“眼睛闭合”的置信度做滞后处理。简单说就是同一个闭眼状态需要连续N帧获取到高置信度的眼部关键点特征,如果中间出现关键点丢失或手部遮挡,判定规则自动暂停累积。这等于在规则层面给模型加了一个“确定性验证”。
第四,增加融合验证条件。疲劳报警不再只看眼睛闭合,还要结合头部姿态持续下沉、打哈欠频率、方向盘操作频率等多模态信号。单一信号不再具备“一票触发”的权限。
策略前后对比,我放一张简化判定逻辑示意:
# 优化前的简化判定逻辑:单帧看瞬时闭眼 if eye_close_prob > 0.8 and head_pose_score > 0.6: trigger_alarm() # 优化后的简化判定逻辑:时序+冷却+多模态 if high_fatigue_score_hold_3s() and not recent_alarm(): if eye_close_prob_continuous_10frames() and hand_occlusion_score < 0.4: if head_drop_confirm() or yawning_confirm(): trigger_alarm()逻辑里的每个条件都有明确的物理含义,不是随手写的。核心思想是让报警系统成为一个“需要多个独立证据同时支持”的决策体,任何一个环节的瞬间异常,都不足以打扰驾驶员。
5. 版本灰度后的验证结果与边界情况复盘
优化完成后,我们走了一轮灰度验证。流程是:内部测试车队先跑,再放到少量公测用户车上,最后全量推送。内部测试阶段我们预设了四十几类场景,重点覆盖揉眼、擦脸、打哈欠、眯眼、贴面膜、吃面包、打电话、低头看手机、长时间闭眼等。这里说说实测结果和一些边界情况。
5.1 内部测试与公测数据的直观变化
先看两组硬数据。第一组是模拟车库环境下的标准场景测试,我们固定揉了200次眼睛,并伴以各种姿态变化,优化前每次揉眼基本都会触发一次报警,优化后200次里只触发了3次,这3次还都是揉眼时间特别长、手指整个包住眼部超过5秒的极端情况。第二组是公测用户车辆的真实数据,版本灰度后,CAC(综合报警准确率)提升了大概19个百分点,误报率直接下降了接近七成。更直观的一个数字是:用户手动关闭DMS的比例,在灰度版本覆盖期内降到了前一个月的一半以下。
数据显示最明显的变化是报警事件的分布结构:之前被揉眼/擦脸动作占据的误报长尾,基本被压平了;剩下还在报警的case,真实疲劳的比例明显上升。说明模型和策略调整后,报警的整个语义重心回到了“疲劳”本身,而不是“脸前有东西”。
5.2 边界情况:墨镜、口罩和暗光下的妥协与坚持
验证过程中也暴露了不少新问题,这里点名几个容易翻车的场景。
墨镜场景是个典型。驾驶员戴着墨镜揉眼睛,手部遮住墨镜边框的反光区域,模型更容易把遮挡当作闭眼。我们在墨镜样本上做了专项增强,并在模型里加入了“墨镜检测”辅助分支,让模型知道“这块区域本来就不是裸露的眼眶”,判断逻辑会更谨慎。但坦率说,墨镜场景下的疲劳检出性能天然受限,任何DMS模型都做不到完全精确,我们的目标是误报不打扰,漏报不致命。
口罩场景也值得一说。疫情之后戴口罩开车很常见,口罩会把嘴巴和鼻子的特征全部遮掉,打哈欠检测被迫完全依赖眼部信息。我们做过一次统计,在口罩场景下,疲劳检出率比正常场景下降约12%。这个缺口目前没有完全补上,主要靠头部落下和持续闭眼两个特征支撑,也提醒我们在宣传上不能把DMS描述成“全工况零误报”。
暗光下的表现则出乎意料地稳。因为我们用的就是红外摄像头加主动补光,夜晚反而是DMS的舒适区,算法几乎没有感知压力。反而黄昏逆光的时候,外部杂光干扰红外成像,眼部特征提取偶尔会跳点,这些case目前靠策略里的“置信度滞后机制”兜底,实测下来误报率也在可接受范围内。
还有一个小细节:驾驶员打喷嚏。打喷嚏瞬间会闭眼、低头、面部扭曲,跟疲劳特征撞得厉害,做完优化后这个场景的误报也基本清零了,原因是打喷嚏的动作持续时间极短,没法满足“持续3秒评分”的门槛。
5.3 复盘:这套优化方案的核心方法论
整轮迭代做完,我最深的感受是:算法侧的准确率提升,不能只靠模型一个大招,数据、模型、策略是三位一体的。数据决定了模型的天花板,模型决定了特征表达的上限,策略决定了产品体验的最后一道闸门。任何一层单独优化,都会被另外两层拖住。
另外也想给做类似功能的同行一个建议:在你训练的早期,就把“用户真实动作”的数据分布建立起来。揉眼、擦脸、打喷嚏、喝水、吃零食、调整眼镜,这些日常小动作不会出现在标准数据集里,但恰恰是它们决定了你的功能会不会被用户关掉。与其等误报大量冒出再补救,不如一开始就采集覆盖。
DMS这个产品,真正难的地方从来不是判断“睡着的人长什么样”,而是判断“正常的人什么动作不能打扰”。能分清这两件事,才算把疲劳监测做成一个用户愿意一直开着的功能。