☰
基于计算机视觉的驾驶员疲劳检测:从Haar到PERCLOS的工程实践
2026/10/5 14:10:08 网站建设 项目流程

简介:文档为一种基于计算机视觉的驾驶员疲劳检测方法及系统的发明专利申请公开文本,由扬州大学科研团队提出,面向智能驾驶、交通安全及计算机视觉方向的研究者与开发者,解决行车过程中驾驶员疲劳状态难以实时监测的问题。资源包仅含1个docx文件,大小约15KB,完整呈现专利公开文本,涵盖权利要求书、说明书、摘要及附图。已有155人学习下载。专利内容从摄像头采集驾驶员脸部视频出发,详述了基于类Harr特征的AdaBoost分类器人脸定位、landmark驱动的眨眼与打哈欠检测,以及图片预处理结合PERCLOS算法进行疲劳判定的完整技术路线,并给出了可融合心率、脑电图等信号构建综合检测系统的扩展方向。读者可借助文档快速理解非接触式疲劳检测的系统架构、核心算法流程及专利文本撰写范式。

1. 货车司机在方向盘上睡着前几秒,眼睛闭合时间会比正常状态长出一截,基于计算机视觉的驾驶员疲劳检测方案盯的就是这个信号。这个专利描述的链路并不复杂:摄像头采集脸部视频、类 Haar 特征的 AdaBoost 分类器定位人脸、landmark 做眨眼和打哈欠检测、再按 PERCLOS 准则判定疲劳等级并预警。整套流程完全非接触,不需要驾驶员佩戴任何传感器,实时性也能满足车载工况。不管你是正在做计算机视觉大作业的高校学生,还是想在驾驶监控项目里嵌入一个 DMS 模块的从业者,这条技术路线都是最容易被复现的切入点。

这类方案的价值在于,它没有把“疲劳检测”当成一个单独的分类问题,而是拆成了人脸检测、关键点检测、眼睛状态统计三段,每一段都有现成算法和成熟库支撑。换句话说,你不需要从头训练任何模型,按这条链路拼起来就能得到一个可运行的原型。接下来我会按专利的实际流程,把每一步的实现方式、参数调法和踩坑经验都过一遍,最后讲清楚怎么用一段离线视频验证这套系统到底靠不靠谱。

2. 人脸定位:类 Haar 特征与 AdaBoost 分类器的落地取舍

2.1 类 Haar 特征为什么能当人脸探测器

人脸检测是这个系统里的第一道闸门,它的稳定性直接决定后面所有步骤的输入质量。在深度学习还没有完全统治目标检测的年代,Viola-Jones 检测器就是人脸检测的事实标准,而它用的正是类 Haar 特征配合 AdaBoost 分类器。类 Haar 特征的本质是一组黑白矩形模板,计算某个矩形区域内像素和与另一个区域的差值,用来表达图像的灰度对比关系。人脸结构天然适合这种表达:眼睛区域通常比脸颊暗,鼻梁区域比两侧亮,这些固定的灰度规律不需要大量计算就能捕获。

AdaBoost 在其中的角色是从成千上万个候选矩形特征里迭代选出少量“弱分类器”,每一个弱分类器只负责判断一个特征是否匹配,叠加上百个之后就变成一个强分类器。OpenCV 里封装好的级联分类器 Cascade Classifier 正是这套思想的工程实现。它先在大尺寸窗口上快速筛选,绝大多数窗口在早期阶段就被淘汰,只有少数候选窗口进入后续的多级验证,因此速度非常快,在 CPU 上跑实时视频流毫无压力。

从选型角度看,这套方案在嵌入式设备上的优势非常明显。疲劳检测系统要装在车里,面对的往往不是高性能 GPU,而是一块 ARM 芯片或者旧款笔记本。深度学习人脸检测精度更高,但模型推理的算力开销和内存占用都不小。类 Haar 特征检测器虽然模型精度略有上限,但它的推理本质上是积分图加比较运算,没有卷积和矩阵乘,几百毫瓦级功耗的处理器都能流畅运行。专利选择这个算法,在当时的技术条件下是合理且高效的工程决策。

2.2 用 OpenCV 跑通人脸检测:代码与参数含义

无论你最后要部署到什么平台,第一步都是在本地把检测链路跑通。OpenCV 内置了基于 AdaBoost 训练好的级联模型文件,加载方式非常简单。以下代码是完整的单帧人脸检测函数,可以直接套到视频循环里使用。

import cv2 # 加载 OpenCV 自带的正面人脸级联分类器 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) def detect_face(frame): # 转灰度图,级联分类器只接受单通道输入 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡化,把过亮或过暗区域的对比度拉回来 gray = cv2.equalizeHist(gray) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, # 每轮检测图像缩小到原来的 1/1.1 minNeighbors=5, # 候选框周边至少要有 5 个邻近框才保留 minSize=(64, 64), # 人脸最小尺寸,小于这个值的窗口直接丢弃 flags=cv2.CASCADE_SCALE_IMAGE ) return faces

这段代码里有几个关键点需要解释。转灰度是必须的,类 Haar 特征只分析亮度关系,不需要颜色信息;直方图均衡化是我额外加的,因为驾驶舱的光照变化非常大,逆光时脸部对比度会被压缩,均衡化能显著提高检测召回率。detectMultiScale 返回的是一个人脸矩形列表,每个矩形是 (x, y, w, h),后面关键点检测就是在这些矩形框内进行的。

参数的作用按优先级来说,minNeighbors 是影响精度的第一号参数。它要求每个候选窗口周围至少有多少个邻居窗口也通过检测,数值越小误检越多,数值越大越容易漏检。minSize 则是影响性能的第一号参数——检测窗口越小意味着要在图像上滑动更多次,计算量成倍上升。如果把 minSize 从 64 改成 32,单帧处理时间可能翻几倍。

2.3 尺度、邻域与人脸尺寸:三个参数的工程经验

这里说一下实际项目中我怎么设参数。驾驶座摄像头一般固定在中控台或后视镜附近,驾驶员人脸距离镜头约 40 到 60 厘米,普通 720p 画面里人脸宽度大约在 150 到 250 像素之间。这种情况下 minSize 设成 80 到 100 比较合理,既能过滤远处乘客的脸,又能保证不会因为驾驶员偶尔探头而丢框。如果摄像头是鱼眼或者广角,画面边缘人脸会变小,那就把 minSize 降到 64,同时接受一定的误检。

scaleFactor 这个参数经常被误认为是检测精度参数,其实它控制的是检测尺度金字塔的步长。设成 1.1 表示每轮检测把图像缩小 0.9 倍,相当于检测窗口逐渐变大,匹配尺度更精细,但帧数会下降。设成 1.3 时速度明显提升,但可能错过某些关键尺度上的正脸窗口。在驾驶场景中,我建议保留 1.1 到 1.2,因为这关系到眨眼检测的稳定性。人脸框如果在尺度上抖动,框的大小忽大忽小,后续关键点坐标也会跟着波动,EAR 曲线就会出现毛刺。

还有一个常被忽略的点:级联分类器对侧脸和人脸剧烈旋转基本无能为力。驾驶员转头看右侧后视镜时,检测框可能会短暂丢失,这是算法本身的边界。缓解办法是加一个跟踪器,比如 OpenCV 里的 CSRT 或 KCF,在分类器连续几帧检测到人脸后,用跟踪器接管中间帧,检测器只负责周期性校正。这样能把人脸框丢失的概率降下来,为后续关键点检测创造稳定输入。

3. 人脸关键点提取:从人脸框到眨眼和打哈欠的判定

3.1 关键点模型怎么选:68 点方案与轻量替代

拿到人脸框之后,下一个任务是定位眼睛和嘴巴的关键点。专利文档里的表述是“利用 landmark 进行眨眼和打哈欠的检测”,这里的 landmark 指的就是人脸关键点。最常见的是 dlib 的 68 点模型,它把脸分成眉毛、眼睛、鼻子、嘴巴、下颌五个区域,每只眼睛周围有 6 个点,嘴巴外沿有 12 个点。用这些点的坐标,就能算出眼睛的开合度和嘴巴的开合度。

选用 dlib 主要有三个理由。第一,模型文件是开放下载的,不需要自己标注训练;第二,它提供了清晰的 Python 接口,加载模型后直接传人脸框就能返回 68 个点;第三,模型推理速度在普通 CPU 上能做到每帧 10 到 20 毫秒,满足实时要求。缺点也很明显:模型文件接近 100MB,对嵌入式部署不太友好。如果是资源受限场景,可以考虑 MediaPipe 的 Face Mesh,它基于 TensorFlow Lite,模型更小,输出 468 个点,但接口和输出格式与 dlib 差异较大,代码不能直接复用。

从工程稳定性角度,我推荐先用 dlib 把流程跑通,再根据需要替换成轻量模型。因为 dlib 的输出格式是固定的,后续的 EAR、MAR 计算逻辑完全基于点坐标,理论上任何能输出对应 6 个眼睛点和嘴部点的模型都可以无缝替换。只要保证点的索引顺序与计算逻辑一致即可。

3.2 眨眼检测:EAR 的计算与连续帧判决

眨眼检测的核心指标叫 EAR,即 Eye Aspect Ratio。它利用眼睛周围 6 个特征点,计算垂直方向点到水平方向点的比例。眼睛睁开时,两组垂直距离相对较大,EAR 接近 0.3;眼睛闭合时,垂直距离趋近于零,EAR 可能掉到 0.1 以下。这个指标的优点是方向无关且尺度无关,人脸靠近或远离摄像头时,EAR 值基本稳定。

EAR 的公式可以写成:EAR = (dist(P2, P6) + dist(P3, P5)) / (2 * dist(P1, P4))。其中 P1、P4 是眼睛内眼角和外眼角,P2、P3 是上眼睑两个等分点,P5、P6 是下眼睑对应的两个点。实现代码非常简单:

import numpy as np def eye_aspect_ratio(eye_points): """ 计算眼睛纵横比 EAR eye_points: 按顺序传入 6 个关键点坐标,顺序为 [P1, P2, P3, P4, P5, P6],对应眼角/上下眼睑 """ p1, p4 = eye_points[0], eye_points[3] p2, p3 = eye_points[1], eye_points[2] p5, p6 = eye_points[4], eye_points[5] vertical_a = np.linalg.norm(p2 - p6) vertical_b = np.linalg.norm(p3 - p5) horizontal = np.linalg.norm(p1 - p4) return (vertical_a + vertical_b) / (2.0 * horizontal + 1e-6)

上面最后加了个 1e-6,防止水平距离为零时除零报错。实际项目中 alert 使用 1e-6 或更小值,常规点检测不会出现这类情况,但不妨加上。

单帧的 EAR 不足以判断眨眼,因为眼睛是一个动态过程,EAR 从正常值掉到低值再回到正常值,才算完成一次完整眨眼。实现时需要一个状态机:上一帧 EAR 大于阈值,当前帧 EAR 小于阈值,记录眼睛闭合;当后续帧 EAR 重新大于阈值时,判定完成一次眨眼。这个过程中至少要连续记录 2 到 3 帧闭合,才能排除单帧噪声或者眼睑抖动造成的误判。

3.3 打哈欠检测:MAR 与时间窗口

打哈欠用类似的思路,但指标换成 MAR,即 Mouth Aspect Ratio。嘴部也有 6 个可用关键点,分别是左右嘴角和上下嘴唇的等分点。MAR 的计算方式与 EAR 一致,只是点的索引不同。正常闭嘴时 MAR 在 0.2 到 0.3 之间,嘴巴张开到较大程度时,MAR 会超过 0.6 甚至更高。

单独看 MAR 瞬态值很容易误报,因为说话、喝水、咳嗽都会导致嘴巴张大。关键在于持续时间长度。普通说话时嘴巴张开时间一般不超过半秒,而哈欠的张口动作通常会持续 1 秒以上。因此我在判断哈欠时,要求连续多帧 MAR 超过阈值,并且累计张口时间超过 1000 毫秒,才输出一次哈欠事件。这个逻辑用帧计数实现即可,因为每帧之间的时间间隔是固定的,帧数乘以帧间隔就是持续时间。

这里要特别提一下数据来源的质量。如果用的是普通 RGB 摄像头,驾驶员嘴巴区域容易受光照阴影影响,上下嘴唇关键点在暗光下会抖动,MAR 曲线会出现突然的尖峰。一种缓解方式是对连续帧的 MAR 做中值滤波,取最近 5 帧的中位数作为当前值,能有效抹掉单帧噪声。滤波窗口不宜过大,否则会吞掉真实哈欠的起始时刻,对后续 PERCLOS 统计造成额外延迟。

4. PERCLOS 指标与预警状态机:从闭眼比例到疲劳判定

4.1 PERCLOS 是什么:闭眼时间占比与三个标准

PERCLOS 是 Percentage of Eyelid Closure 的缩写,字面意思就是单位时间内闭眼时间所占的比例。疲劳检测领域用得最多的是 P80 标准,即当眼睑遮住瞳孔面积超过 80% 时,认为该时刻眼睛处于闭合状态。统计一段固定时间内闭合帧数占总帧数的比例,就得到 PERCLOS 值。

为什么要用闭眼比例而不是瞬时眨眼次数来判定疲劳?因为疲劳的视觉特征是一段时间内闭眼行为累积,而不是某一次眨眼。正常人每分钟眨眼 15 到 20 次,每次眨眼持续约 100 到 150 毫秒,这个过程中眼睛闭合时间占比大约在 4% 到 6%。当驾驶员疲劳时,眨眼频率可能下降,但单次闭眼时间拉长到 500 毫秒甚至更长,有些接近微睡眠的状态下闭眼时间会达到 3 到 5 秒,PERCLOS 值可以轻松超过 20%。因此,PERCLOS 天然是一个有累积意义的统计量,比单帧状态可靠得多。

4.2 用滑动窗口统计 PERCLOS 的代码实现

PERCLOS 的实现需要一个统计缓存,我一般用 Python 的 deque 做固定长度滑动窗口。窗口大小取 600 帧,假设视频帧率 30fps,正好对应 20 秒统计时长。更新流程是每一帧计算 EAR 后判断当前眼睛是否处于闭合状态,把布尔值写入队列,每次取队列中闭眼布尔值占比即可。

from collections import deque class PerclosCounter: def __init__(self, window_size=600, ear_threshold=0.2): self.window = deque(maxlen=window_size) self.ear_threshold = ear_threshold def update(self, ear_value): # 判断当前帧是否闭眼,写入滑动窗口 is_closed = ear_value < self.ear_threshold self.window.append(is_closed) def get_perclos(self): # 窗口未满时返回 0,避免冷启动阶段误报警 if len(self.window) < self.window.maxlen: return 0.0 closed_count = sum(self.window) return closed_count / len(self.window) # 每帧调用示例 counter = PerclosCounter() counter.update(ear) ratio = counter.get_perclos()

这个实现的优势是计算复杂度为 O(1),不会随着窗口长度增加而变慢。sum 操作虽然要遍历整个 deque,但 600 帧的布尔求和耗时小于 0.1 毫秒,完全可以接受。需要注意冷启动问题:如果窗口未满就计算比例,实际有效样本不足,得出的 PERCLOS 值偏低,所以返回 0.0 作为占位。实际操作中,系统启动后的前 20 秒内不会有任何预警输出,这也是合理的安全策略。

PERCLOS 的阈值需要结合统计窗口时长来设定。窗口 20 秒时,常见的建议阈值在 0.35 到 0.4 之间,即 20 秒内有 7 到 8 秒闭眼才触发预警。窗口拉长到 60 秒,阈值可以相应下调到 0.3 左右。阈值设太高容易漏报真正疲劳的场景,设太低又会在正常眨眼时频繁报警。最稳妥的方法是采集 10 分钟正常驾驶和 10 分钟模拟疲劳状态的数据,画出两条 PERCLOS 曲线,取两条曲线之间的中间值作为初始阈值。

4.3 预警状态机:疲劳等级与输出策略

单靠一个 PERCLOS 阈值直接决定是否报警,在实际项目中不够稳健。我习惯把预警拆成两级:PERCLOS 连续 5 秒超过 0.25 时输出“轻度疲劳”提示;PERCLOS 连续 10 秒超过 0.4 时输出“深度疲劳”报警。这背后是一个简单的状态机,每个状态都有独立的持续时间计数,避免因为单次峰值触发报警。

下表是一组我自己调试后比较稳的参数,可以作为初始配置参考:

参数项建议值作用
EAR 闭眼阈值0.2低于此值判为眼睛闭合
眨眼最小闭合帧数3 帧低于此帧数忽略,视为噪声
MAR 哈欠阈值0.6超过此值判为嘴巴张开
哈欠最小持续时长1000 毫秒持续时间超过才计为哈欠
PERCLOS 正常阈值0.25轻度疲劳预警触发线
PERCLOS 严重阈值0.4深度疲劳报警触发线
统计窗口600 帧约 20 秒滑动窗口

这里还要考虑一个实际问题:帧率波动会影响“帧数”和“时间”之间的对应关系。如果检测算法在部分帧上耗时过大,实际帧率会从 30fps 掉到 15fps,600 帧窗口对应的真实时间就从 20 秒变成了 40 秒,PERCLOS 值的物理意义就会变模糊。所以我在实际工程里会给每个帧打上时间戳,统计时用时间戳计算闭眼时长总和,再除以总时长,而不用简单的帧数比。这样即使帧率有波动,PERCLOS 值也始终是时间维度的准确比例。

5. 避坑指南:疲劳检测系统跑不稳的几个真实问题

5.1 驾驶员侧脸看后视镜,人脸框直接消失

现象:驾驶员转头看左侧或右侧后视镜,人脸角度超过 45 度,级联分类器的人脸框开始一帧有一帧无,关键点检测跟着乱跳,PERCLOS 统计被大量无意义数据污染。

原因:类 Haar 特征分类器本质上是正面人脸检测器,对侧脸和大幅度旋转不敏感,这也是 AdaBoost 级联模型的已知边界。遗传用车场景中,驾驶员看后视镜是高频动作,几乎每 10 秒就会发生一次。

解决:在分类器后面加一个固定在框上的跟踪器,比如 OpenCV 的 CSRT 跟踪器。分类器检测到人脸后,用跟踪器锁定该区域;跟踪器连续丢失超过 30 帧才重新启动检测。这样即使侧脸瞬间检测不到,跟踪器也能维持框的位置,传给关键点检测的输入仍保持稳定。

5.2 驾驶员戴墨镜,EAR 判断全部失效

现象:墨镜把眼睛区域完全遮盖,关键点模型在墨镜上找不出瞳孔边缘的特征点,输出的 EAR 值要么异常偏大,要么不断抖动,眨眼检测完全失灵。

原因:dlib 的 68 点模型是在可见光人脸图像上训练的,它依赖眉毛、眼睑、虹膜之间的灰度差来定位眼睛点。墨镜会抹掉这些自然特征,模型只能凭训练数据里的“平均脸先验”硬猜位置,结果当然不靠谱。

解决:这是视觉方案的物理边界,软件层面几乎无法绕过。可行方案有两种,一是换用红外摄像头,红外光照下墨镜镜片通常不会完全遮挡眼睛轮廓;二是在检测策略里做降级处理,当双眼 EAR 连续出现异常值时,自动降低眨眼和 PERCLOS 的权重,转而依靠打哈欠和头部姿态判断疲劳。降级逻辑至少要保证系统不会因为一个墨镜就把整个预警功能关掉。

5.3 视频帧率波动,PERCLOS 统计结果失真

现象:代码在本地电脑上跑 CPU 占用率接近 90%,帧率从 30fps 掉到平均 17fps,日志里的 PERCLOS 值比实际驾驶员状态偏高一倍,有时候驾驶员闭眼一秒也会触发严重报警。

原因:基于帧数比例的 PERCLOS 假设每帧时间相同,但检测耗时波动导致帧与帧之间的时间间隔不一致。正常眨眼那几帧恰好落在计算压力大的时间段,闭眼帧被相对拉长,比例自然偏高。

解决:每帧计算后记录当前时间戳,以真实时间窗口做统计。按“闭眼时长总和 / 总时长”取代“闭眼帧数 / 总帧数”,相当于把 PERCLOS 变成一个时间维度指标。代码改动不大,但物理意义更准确,推荐所有项目都这么做。

5.4 夜间低照度环境,人脸检测率明显下降

现象:夜间行车时,车内光源不足,画面整体偏暗,检测器输出的人脸框变小变松,关键点模型输出的 EAR 噪声大,眨眼检测准确率从日间 95% 掉到 70% 以下。

原因:类 Haar 特征依赖图像的灰度差异,光线不足时脸部和背景的对比度被压缩,特征响应变弱。同时摄像头自动增益会把噪点放大,关键点在暗部区域的定位抖动加剧。

解决:在摄像头选型阶段就用红外摄像头,或者采用内置红外补光的方案,驾驶员脸部在近红外波段下会有稳定反射特征,这是车载 DMS 行业的通行做法。如果手里只有普通 RGB 摄像头,可以在预处理阶段加强直方图均衡化的强度,并且把关键点输出的 EAR 序列做更激进的滤波,比如滑动中位数窗口从 5 帧增大到 9 帧。

5.5 眨眼和闭眼混在一起,报警时机变得“玄学”

现象:测试者只是眨了一下眼,系统报警了;测试者确实闭眼两秒,系统反而没报警,报警触发完全没有可预测的边界。

原因:EAR 低于阈值时,系统只记录“闭眼”,但没有区分这是一次时长 150 毫秒的正常眨眼,还是一次持续 2 秒的闭眼。如果判定逻辑只看单帧,没有要求最低连续帧数,眨眼瞬间也会被当成闭眼累积进 PERCLOS,报警时间点自然飘忽。

解决:在闭眼判定中加入“最少连续帧数”约束,比如连续 3 帧 EAR 低于阈值才把当前时刻计入闭眼状态。这样正常眨眼的 150 毫秒宽度会被过滤掉,而真正的闭眼因为持续超过 3 帧会被完整捕获。类似地,从闭眼恢复到睁眼的边界也要等连续 2 帧 EAR 高于阈值后才切换状态,防止在阈值边界来回抖动。

6. 进阶验证:用离线视频与帧级回放,把一套疲劳检测系统调可信

系统能跑起来只是第一步,难的是证明它在不同光照、不同人脸、不同疲劳程度下都稳定。我的习惯是先做一轮离线视频回放验证,再谈参数优化。具体做法是录制三段驾驶模拟视频:一段正常驾驶、一段轻度疲劳、一段明显疲劳,时长各 10 分钟。然后把检测代码跑在每段视频上,输出每一帧的 EAR、MAR、PERCLOS 值到 CSV 文件。下一步是手工标注每一帧的闭眼状态,正样本是“帧内眼睛闭合超过 80%”,负样本是“眼睛张开”。最后把标注结果和检测结果逐帧比对,能直观看到误检集中在哪个阶段。

评估指标不用太复杂。先看每帧闭眼判定的准确率,再看每分钟 PERCLOS 曲线的趋势是否和实际状态一致。一个实用的做法是把三张 PERCLOS 曲线画在同一张图里,正常驾驶的曲线应该稳定在 0.1 以下,轻度疲劳可能偶尔冲到 0.3,明显疲劳应该持续停留在 0.3 到 0.5 区间。如果曲线区分度不够,优先调整 EAR 闭眼阈值,而不是调 PERCLOS 报警阈值。

对于判别阈值的选择,我会在标注结果上画一条简单 ROC 曲线,遍历从低到高的所有候选阈值,找到误报率和漏报率都相对较低的点。通常在PERCLOS 上,精度优先项目选 0.35,召回优先项目选 0.25。这套系统里另外还有哈欠检测做辅助,所以我可以把 PERCLOS 阈值适当调高一点,用哈欠事件来兜住漏报,整体预警效果反而更好。

这个项目如果要扩到真实产品,还可以把心率检测、脑电图等信号做融合输入,但那段路涉及更多硬件,不是单纯软件工程能覆盖的。先把视觉这条链路做扎实,已经能覆盖大部分疲劳场景。从那以后我每次接疲劳检测项目,都强制走一遍离线视频的帧级回放,全程不跳过中间任何一帧,这个方法虽然土,但比任何可视化监控都更能暴露问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询