交付群里有个车主直接甩了两句话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这个产品真正难的地方从来不是判断“睡着的人长什么样”而是判断“正常的人什么动作不能打扰”。能分清这两件事才算把疲劳监测做成一个用户愿意一直开着的功能。