疲劳检测这个需求,这几年在智慧交通、在线教育、工业安防里出现的频率越来越高。拿驾驶场景来说,司机疲劳往往表现为持续眨眼、闭眼时间变长、头部下垂,这些信息从摄像头里就能读出来。我最近用 OpenCV 配合 dlib,做了一套基于人眼纵横比(EAR)的疲劳检测原型,整个流程跑通之后发现,难点并不在算法本身,而在参数调优和工程鲁棒性。这篇文章就围绕 dlib 关键点提取和 EAR 计算,把完整方案、代码、踩坑记录都整理出来,希望能给打算做类似项目的人省点时间。适合正在接触人脸关键点、想做疲劳状态判断,或者已经有检测框但不知道怎么量化闭眼程度的人参考。
1. 整体设计:为什么选 dlib + OpenCV 做疲劳检测
1.1 疲劳检测任务的核心诉求
疲劳检测听起来是个简单问题,真做起来会发现需求很拧巴:既要实时,又要在普通电脑上跑得动;既要准确,又不能总被光照变化干扰。我最初给自己定的目标是做一个能在驾驶模拟环境下稳定运行的监视程序,摄像头放在方向盘前方约 60 厘米处,只靠一盏台灯照明,CPU 占用尽量别太高。这个环境下,人脸基本是正面的,姿态变化不大,但会频繁出现低头、转头和眨眼。
很多刚接触这个方向的人第一反应是上深度学习,比如训练一个分类网络判断眼睛是睁是闭。这个思路当然可行,但对一个快速验证原型的项目来说有点重:你需要采集大量闭眼和睁眼样本,标记边界框或者关键点,面对不同肤色、眼镜、光照做泛化,训练和推理还依赖 GPU。用几何特征的方法就不一样,只要人脸关键点检测准确,眼睛的闭合程度可以通过几个点之间的距离比例直接算出来,不需要任何训练,解释性强,调参也直观。这套方案特别适合嵌入式设备和普通办公电脑,也是我最终选型的主要原因。
另一个容易被忽视的诉求是时间维度的连续性。单帧 EAR 低并不一定说明疲劳,可能只是正在眨眼。真正可靠的判断要看连续若干帧内闭合状态是否持续,或者一段时间内闭合比例是否过高。所以我从一开始就明确:核心指标是 EAR,但决策逻辑必须结合帧序列,这样才能把眨眼和疲劳闭眼区分开。
1.2 主流技术路线对比
为了让自己选型时心里有底,我对比过几种常见的眼部状态检测方案。简单整理了一个对比表,方便后面再做同类项目的人参考:
| 技术路线 | 精度 | 计算成本 | 落地难度 | 典型问题 |
|---|---|---|---|---|
| 肤色分割 + 瞳孔检测 | 低 | 极低 | 易 | 光照和镜头角度影响大,容易把阴影当瞳孔 |
| dlib 关键点 + EAR | 中高 | 低 | 低 | 对侧脸和遮挡敏感,需要规则约束 |
| 传统分类器 + 睁闭眼特征 | 中 | 中 | 中 | 需要手动提取特征,场景迁移差 |
| 深度学习分类网络 | 高 | 高 | 高 | 依赖标注数据,实时推理要考虑硬件成本 |
| 脑电/眼电等生理信号设备 | 高 | 低 | 极高 | 设备贵,不适合通用落地 |
对比下来,dlib 加 EAR 方案最大的优势是“够用且不折腾”。dlib 的人脸检测器是经典的 HOG + 线性分类器,速度很快,在正面人脸条件下的表现扎实;68 点关键点预测器能稳定输出眼睛区域六个特征点,正好满足 EAR 的计算要求。OpenCV 则负责视频帧读取、图像预处理和结果可视化,生态成熟,和 dlib 配合基本没有摩擦。这套组合也能平滑切换到视频文件,方便离线测试。
1.3 EAR 原理:为什么它能反映疲劳
EAR 的全称是 Eye Aspect Ratio,中文一般译作眼睛纵横比。它不是某个库里的现成函数,而是根据眼睛周边六个关键点坐标算出的一个标量值。这六个点在 dlib 的 68 点模型中分别对应眼睛的轮廓:左右眼角各一个点,上下眼睑各有两点。计算时取一侧眼睛的全部六个关键点,先算两条“竖直距离”的平均值,再除以“水平距离”,就得到该侧眼睛的 EAR。
公式可以写成:
EAR = ( ||P2 - P6|| + ||P3 - P5|| ) / ( 2 * ||P1 - P4|| )其中 P1、P4 是左右眼角,P2、P3 是上眼睑的点,P5、P6 是下眼睑的点。正常情况下眼睛睁开时,上下眼睑距离明显,EAR 通常在 0.25 到 0.35 之间。闭眼时上下眼睑几乎重合,竖直距离趋近于零,但水平距离也就是内外眼角之间的距离保持不变,所以 EAR 会一下子掉到 0.1 以下。用人话说,这就像用尺子量门框:门的宽度基本固定,但门缝的高度是变化的,EAR 就是“高度除以宽度”这个比值。
我刚开始看论文的时候,总觉得这个公式太简单,不像是能解决疲劳检测的样子。后来实测才发现,简单的比例反而对个体差异更友好,因为每个人眼睛大小不同,眼裂宽度和长度有差异,但纵横比是相对值,在不同人之间仍有较好的稳定性。真正需要关注的是阈值和连续帧判断,而不是公式本身。
2. 环境准备与模型使用
2.1 运行环境与依赖安装
我这个项目用的是 Python,版本 3.8。依赖其实只有四个主库:opencv-python 负责图像和视频处理,dlib 负责检测人脸和输出关键点,numpy 用来做坐标计算,imutils 可以顺手用来做图像缩放和转灰度,不是必装,但能省点样板代码。
安装命令很简单:
pip install opencv-python dlib numpy imutils如果这条命令执行顺利,那说明你的 Python 环境很干净。实际情况中,最容易出问题的就是 dlib。它依赖 C++ 编译,Windows 上如果用的是官方默认 Python 环境,直接 pip install dlib 经常会在编译阶段报错。我的建议是优先查找针对当前 Python 版本预编译好的 dlib wheel 文件,下载后使用 pip 安装本地文件;macOS 上也很常见,先确保安装了系统要求的 C++ 工具链,再执行安装。Linux 环境相对顺利,但有时需要先安装 cmake 和 basisr 基础依赖。
装好之后可以做一次快速验证,确认各模块好用:
import cv2 import dlib import numpy as np print(cv2.__version__) print(dlib.__version__)2.2 人脸关键点模型怎么选
dlib 本身只能检测人脸框,不能直接给出眼睛和嘴巴的关键点。关键点预测需要单独的模型文件,最常用的是 68 点模型,文件几百兆,不过实际上模型体积大主要因为shape predictor内部用了多棵回归树,文件大小并不会影响推理速度太多。你可能会看到 5 点模型,体积小,但 5 点模型只有眉毛、鼻子、嘴角等稀疏信息,撑不起 EAR 计算,所以做眼部检测必须用 68 点模型。
模型文件名为 shape_predictor_68_face_landmarks.dat,一般可以从 dlib 官方发布渠道或第三方模型分享社区下载。下载后建议固定放在项目目录下的 models 子目录里,并写一个判断文件是否存在的逻辑,防止程序启动时直接报错。模型文件和代码版本最好保持一致,不同版本的关键点顺序一般不会变,但你很难排查排列混乱的坑,所以固定一个版本是省心的做法。
2.3 摄像头与视频源准备
开发初期我建议不要直接对着摄像头调试,先用一段录好的视频作为输入,这样程序出错时可以直接重放同一段内容,方便对比。我的调试流程是:先用手机或者摄像头录一段 15 秒视频,包含睁眼、眨眼、闭眼、低头几个动作,保存成 test.mp4,再让代码逐帧读取。这样可以反复测试,不受环境人员走动的影响。
等代码稳定后再切换到摄像头。初始化摄像头和视频文件的方式几乎一样,只是参数不同:
cap = cv2.VideoCapture(0) # 0 表示系统相机,也可以指定视频文件路径 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30)分辨率不要盲目调高。dlib 的 HOG 检测器在 640x480 分辨率下表现足够,1080p 反而会让 CPU 吃不消。实测上来讲,1080p 下每帧人脸检测可能耗时 80 毫秒以上,如果还跑关键点预测,帧率会掉到个位数,体验非常差。可以先降分辨率,后面优化阶段再考虑局部放大。
3. 核心代码实现:从人脸检测到 EAR 计算
3.1 初始化检测器与关键点预测器
这部分是所有工作的地基。创建一个人脸检测器,再加载关键点预测器。由于预测器加载模型文件比较耗时,最好放在程序初始化阶段,不要在视频循环里重复加载。
import cv2 import dlib import numpy as np detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat")dlib 的 frontal_face_detector 就是前面提到的 HOG 检测器,输入灰度图,输出人脸矩形框。predictor 输入灰度图和 face rect,输出 68 个关键点。这里必须注意,dlib 的检测器要求输入 uint8 类型灰度图,如果直接把彩色图传进去,大概率会报类型错误,或者输出诡异结果。
3.2 定义 EAR 计算函数
我先按 dlib 的关键点索引直接实现一版,思路最清晰。在 68 点模型中,右眼的关键点编号从 36 到 41,左眼从 42 到 47。需要取点顺序和公式对应,否则会算出完全错误的值。
def eye_aspect_ratio(landmarks, eye_indices): coords = [(landmarks.part(i).x, landmarks.part(i).y) for i in eye_indices] coords = np.array(coords, dtype=np.float32) p1 = coords[0] p2 = coords[1] p3 = coords[2] p4 = coords[3] p5 = coords[4] p6 = coords[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,目的是防止水平距离恰好为 0 时除零报错。虽然 dlib 正常输出的眼角点不可能完全重合,但加上一个小常数没有任何副作用,属于防御性写法。
左右眼各算一个 EAR,之后可以取平均值。这样做的原因是有人两眼的闭合速度和程度并不完全同步,单只眼睛受光照角度、眼镜边框遮挡的影响更大,取均值能减少单点噪声。当然,如果项目要求更精细,比如检测单眼故意闭眼,就不能简单平均。
3.3 实时视频循环与疲劳判定逻辑
视频循环是整个程序的主干,流程是:读灰度图 -> 人脸检测 -> 关键点预测 -> 计算 EAR -> 根据阈值和连续帧数更新疲劳状态 -> 可视化。
我直接把整理好的完整代码放在这里,代码里加入足够的注释,方便直接跑:
import cv2 import dlib import numpy as np LEFT_EYE = list(range(42, 48)) RIGHT_EYE = list(range(36, 42)) EAR_THRESH = 0.25 CONSEC_FRAMES = 20 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat") def eye_aspect_ratio(landmarks, eye_indices): coords = [(landmarks.part(i).x, landmarks.part(i).y) for i in eye_indices] coords = np.array(coords, dtype=np.float32) p1, p2, p3, p4, p5, p6 = coords 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) cap = cv2.VideoCapture("test.mp4") frame_counter = 0 alarm_on = False while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: landmarks = predictor(gray, face) left_ear = eye_aspect_ratio(landmarks, LEFT_EYE) right_ear = eye_aspect_ratio(landmarks, RIGHT_EYE) ear = (left_ear + right_ear) / 2.0 # 画出眼睛区域的关键点和连线 left_pts = np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in LEFT_EYE], dtype=np.int32) right_pts = np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in RIGHT_EYE], dtype=np.int32) cv2.polylines(frame, [left_pts], True, (0, 255, 0), 1) cv2.polylines(frame, [right_pts], True, (0, 255, 0), 1) if ear < EAR_THRESH: frame_counter += 1 if frame_counter >= CONSEC_FRAMES: cv2.putText(frame, "FATIGUE!", (20, 60), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 0, 255), 3) else: frame_counter = 0 cv2.putText(frame, f"EAR: {ear:.3f}", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2) cv2.imshow("Fatigue Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码里有一个容易忽略的逻辑:frame_counter 只在 EAR 低于阈值时增加,一旦超过连续帧数就触发疲劳画面提示。如果中间任何一帧 EAR 恢复正常,计数就归零。这个设计比单纯“当前帧 EAR 低就报警”可靠得多,因为眨眼动作的持续时间通常小于 200 毫秒,在 30fps 视频下不足 6 帧,设置 20 帧作为阈值可以有效过滤掉眨眼。
3.4 阈值和连续帧数怎么调
EAR_THRESH 取 0.25、CONSEC_FRAMES 取 20 是一组常见起点,但不保证对你的人脸也适用。每个人的眼睛特征不同,摄像头距离不同,阈值可能需要上下浮动。我在实际测试中见过有的朋友正常睁眼 EAR 只有 0.20,因为眼睛比较细长,如果用 0.25 作为阈值,他会一直被误判为闭眼。
所以调参不能拍脑袋,要先用一段“正常睁眼 + 正常眨眼”的视频,在代码里打印每一帧的 EAR 值,连续跑几十秒,然后统计睁眼状态下 EAR 的分布和闭眼状态下的取值。一般来说,取二者的中间值作为初始阈值比较合适,比如睁眼平均 0.30,闭眼最低 0.08,那阈值设 0.2 左右就比 0.25 更稳。
连续帧数的选择和时间有关。假设视频帧率是 30fps,每帧间隔约 33 毫秒,那么 20 帧对应约 660 毫秒的持续闭眼。一般疲劳闭眼会超过 500 毫秒,而单次眨眼在 100 到 200 毫秒,两边差距明显。如果摄像头帧率是 15fps,连续帧数可以酌情减少到 10 到 12 帧,但要记住,帧率过低本身就会丢失闭眼细节,很难做精细检测。
3.5 报警与记录扩展
把 EAR 和疲劳状态可视化之后,还可以做几件提升工程价值的事。比如触发疲劳报警时保存当前帧截图,或者记录疲劳事件的时间戳。代码里加上一个简单计数器,每次触发报警时把当前时间写入 CSV 文件,后面回看时就能知道驾驶员在什么时间段出现过持续的闭眼状态。这个扩展对测试模型效果很有用,也能让项目更接近真实产品。
报警提示不一定非要用计算机视觉的方式。实际项目中我更喜欢在界面上显示醒目文字,同时调用系统蜂鸣声,提醒测试人员注意。如果接上位机,还可以把疲劳状态通过串口或网络协议发送出去,让外部设备执行减速、振动座椅等动作。但这一步要和具体硬件配合,不是核心 EAR 算法的范畴,这里不展开。
4. 常见问题与排查技巧实录
4.1 dlib 安装和模型文件加载的坑
最典型的问题是加载模型时提示文件找不到,或者程序一运行就报“Unable to load shape_predictor”。这类问题九成是路径写错了。模型文件和其他代码不在同一目录,或者没有加前缀目录“models”,都会导致找不到文件。建议先使用绝对路径打印文件是否存在:
import os model_path = "models/shape_predictor_68_face_landmarks.dat" print("model exists:", os.path.exists(model_path))如果存在但加载仍然失败,再看模型文件是否损坏。下载过程中中断容易得到残缺文件,直接报错可能不是路径而是文件尺寸不对。一个比较笨但也有效的检查方式是看文件体积,正常 68 点模型体积约 99MB 上下,如果只有几十KB,基本可以判断是坏文件。
另一个常见坑是环境里存在多个 dlib 版本。之前我用 conda 安装过一次旧版 dlib,后面又用 pip 装了新版,导致 predictor 版本和模型版本不匹配。排查方法也很简单,在启动脚本里打印 dlib.version,如果版本太老旧,尽量升级到新版。实在不想折腾,就新建一个干净的虚拟环境,只装必要依赖,能省很多莫名其妙的问题。
4.2 摄像头打开失败和画面卡顿
摄像头打开失败的原因通常集中在两点:摄像头编号错误,或者权限没给足。桌面端命令cap = cv2.VideoCapture(0)弹不出画面,可以先试试cap = cv2.VideoCapture(1),因为有些电脑自带摄像头和 USB 外接摄像头共存在一起,编号从 0 开始,但默认相机可能被其他软件占用。如果总是失败,用系统自带的相机应用确认摄像头本身能用,再回到 OpenCV。
画面卡顿则要从两方面排查。一方面是分辨率太高,比如 1920x1080,即使摄像头支持,dlib 的检测时间也会大幅上升。先把分辨率固定到 640x480,看看帧率是否恢复。另一方面是算法处理时间太长,检测器每次全图扫描,如果背景复杂,检测耗时会更长。可以引入“跳帧处理”的思路,比如每两帧才运行一次完整的人脸检测和关键点计算,中间帧直接复用上一帧的人脸位置,显示时仍然保持较高视觉帧率。这个方法能有效降低 CPU 占用,是嵌入式场景的标准操作。
4.3 人脸检测漏检和误检
HOG 人脸检测器对正面人脸的检测效果不错,但一旦人脸偏转超过 45 度,就会出现漏检。解决方式主要有两个:一是确保摄像头位置尽量正对受测者,二是如果应用场景允许,直接使用 dlib 自带的 CNN 人脸检测模型。CNN 模型在侧脸、模糊、遮挡条件下的鲁棒性更好,但模型更大,推理速度也更慢,对普通 CPU 来说可能压力大。我在驾驶模拟场景里最终还是选择了 HOG,因为方向盘前的坐姿相对固定,不需要支持大幅度转头。
误检的情况也不少见。很多人以为误检只会发生在背景复杂的时候,实际上过度检测在简单环境下也可能发生,比如把柜子上的圆形图案误认为人脸。HOG 检测器返回多个框,只需要在循环里对每个框做关键点计算,但结果可能非常不稳定。处理办法是按置信度排序,只保留最大的检测框,因为疲劳检测场景一般只需要主驾驶位的人脸,不需要检测后排乘客。加一个简单的面积判断就能让结果稳定很多。
4.4 EAR 数值异常怎么定位
EAR 数值异常通常有两种:要么始终很高,要么始终很低。始终很高,并且闭眼时数值也降不下来,最常见的原因是眼睛区域的关键点索引用错了。有的代码习惯从 36 到 47 连续取值,但这里 6 个点并不是按公式顺序排列,需要严格按 P1 到 P6 的语义取点。我在调试时就犯过这个错,只取了连续六个点,导致先算竖直距离时而算到了眼角点上。
EAR 始终很低,则是检测区域漂移。dlib 关键点预测器是基于人脸框内相对位置回归的,如果检测框不准,关键点可能会落在眼睛下方或眉毛上。这时可以先显示关键点坐标,观察关键点是否跟着眼睑轮廓走。另一个解决办法是对关键点时间序列做一次移动平均,比如保存最近 5 帧的 EAR,取平均值作为当前值。平滑处理能滤掉关键点抖动带来的数值跳动,代价是响应变慢,所以窗口不能开太大。
提示:如果屏幕上的点线看起来歪了,不要急着改参数,先把关键点可视化打开,确认每个点到底落在哪里,绝大部分问题一眼就能发现。
4.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| dlib 安装失败 | 缺少 C++ 编译环境 | 找预编译 wheel 或使用 conda 安装 |
| 模型加载报错 | 路径错误 / 文件损坏 | 检查路径,确认文件体积约 99MB |
| 摄像头打不开 | 编号错误 / 权限不足 | 尝试不同编号,检测系统权限 |
| 画面严重卡顿 | 分辨率过高 / 每帧全图检测 | 降到 640x480,使用跳帧和 ROI |
| 侧脸漏检 | HOG 检测器的固有局限 | 调整摄像头角度,或换 CNN 模型 |
| EAR 闭眼时不下降 | 关键点索引顺序不对 | 严格按公式语义取六个点 |
| EAR 抖动严重 | 检测框不稳 / 关键点抖动 | 对 EAR 做移动平均,限制人脸框变化 |
5. 实测效果与进一步优化
5.1 一组有代表性的实测数据
我在一段测试视频上反复跑过这版程序,视频里包含睁眼、正常眨眼、刻意闭眼三种状态。为了减少个体误差,分别统计了睁眼静止期和完全闭眼期的 EAR 值。正常睁眼时,左眼和右眼的 EAR 平均值在 0.30 左右,最低也稳定在 0.26 以上;完全闭眼时,EAR 的峰值不超过 0.12。把阈值设为 0.20,要比论文常用的 0.25 更适合我的脸型,说明阈值确实需要按人微调。
在 30fps 的输入视频中,使用单线程循环,不做跳帧和优化,dlib 检测器加上关键点预测的总耗时约为 25 到 35 毫秒每帧,勉强达到实时。如果直接把摄像头分辨率调整到 1280x720,耗时增加到 50 毫秒以上,画面已经能感觉到明显延迟。所以实际使用时我固定 640x480,并且开启了跳帧:每两帧做一次完整检测,中间帧使用最近一次检测结果计算 EAR,画面上仍然以每秒 30 帧显示,CPU 占用下降接近三分之一,延迟体感明显改善。
5.2 性能优化:让算法跑得更稳
除了跳帧,还可以做局部 ROI 裁剪。上一帧检测到人脸后,把下一帧的人脸扩大 30% 作为候选区域,只在这个区域里运行 dlib 检测器,能显著减少全图扫描成本。需要注意的是,如果人脸移动很快,ROI 可能跟踪丢目标,所以当检测不到人脸时,需要退回全图检测,重新初始化人脸位置。这种先粗后细的策略在实践里很有用。
另一个被低估的优化点是灰度图预处理。可以尝试对灰度图做直方图均衡化,增强明暗对比,尤其是逆光环境下,人脸检测成功率会提高不少。均衡化操作本身耗时不长,但代价也很低,建议直接加入预处理流程。我自己试验下来,在室内灯光较弱时,均衡化能减少大约 20% 的漏检。
最后,如果实在想把帧率再往上提,可以把 EAR 计算和界面显示拆成两个线程:一个线程负责读取摄像头、人脸检测、关键点预测,另一个线程负责绘制和显示。这样即使算法线程偶尔卡顿,界面线程也不会阻塞,互动体验会好很多。引入多线程的核心目的是解耦 IO 和计算,不是解决所有性能问题的银弹。
5.3 从 EAR 到更完整的疲劳判断体系
单一 EAR 指标虽然能抓住闭眼状态,但要判断“疲劳”往往还不够。我在后续版本里加入了两个扩展方向:一是计算嘴部纵横比(MAR),用于检测打哈欠;二是引入 PERCLOS 指标,也就是在一段时间内眼睛闭合时间所占的百分比。PERCLOS 是驾驶疲劳研究领域用得比较多的一个指标,比单次连续帧数统计更能反映持续性疲劳状态。
PERCLOS 的实现思路很简单:维护一个滑动窗口,比如最近 60 秒内统计 EAR 低于阈值的帧数占比。如果占比超过 0.2,就认为存在明显疲劳倾向。这个指标和连续帧计数并不冲突,两者可以同时使用:连续帧计数用于快速提醒,PERCLOS 用于阶段性的状态评估。
嘴部MAR的计算和EAR非常相似,只要把上下嘴唇之间三个关键点的竖直距离和水平宽度做一个比例。实际操作时,先判断是否张嘴,再判断张嘴持续时间,如果连续长时间张嘴且结合眼部 EAR 偏低,疲劳置信度会更高。多指标融合比单纯依赖 EAR 显得更可靠,代码扩展起来也不复杂,关键是状态机的划分要清晰。
5.4 工程化落地时的一点建议
走到这一步,项目已经能从“能跑的 demo”变成“能演示的原型”。如果还要继续往产品方向走,需要重点考虑两点:第一,把参数配置外部化,EAR 阈值、连续帧数、ROI 系数、摄像头编号这些全部放到配置文件里,避免每次调整都改代码重启。第二,设计好告警状态机,比如区分“注意力分散”“短暂闭眼”“严重疲劳”三档,分别映射到不同颜色和声音。这些设计在原型阶段看不重要,在演示和交付时却非常加分。
我在实际调试中还发现一个规律:用视频文件测试时,程序往往看着很稳定,切到摄像头实时流后就会出现各种偶发问题。这主要是因为摄像头的帧间隔不均匀,光照也在动态变化。所以从第一天起就建议所有结果都同时记录到日志,包括时间戳、EAR 值、阈值判定结果。后面做任何改动,都能用同一份日志做对比,而不是靠肉眼感觉帧率变没变。
写在最后:几个折腾出来的经验
这套项目做完,我最深的体会是:EAR 算法本身不难,难的是让它适配具体的脸、具体的光线、具体的摄像头角度。不要迷信论文里的默认阈值,一定要花点时间打印、记录和分析数据。我调参最顺利的一次,就是先把十分钟视频的 EAR 曲线画出来,再对照视频画面一点点找阈值,整个过程比盲目试参数快了太多。
另外,dlib 的环境安装很容易劝退新手,但挺过这一步后面的收益很大。如果你在安装阶段被卡住,不妨换一个干净的环境,或者找预编译包,千万别在配置环境上耗太久。OpenCV 和 dlib 都是很成熟的工具,只要代码逻辑理顺,剩下的问题基本都是参数和场景适配问题,耐心记录、逐步对比,一定能把效果稳定下来。
如果后续想把项目做得更完整,我建议先加 PERCLOS 指标,再做嘴部哈欠识别,最后接一个简洁的可视化看板。整个过程不需要推翻现有代码,只需要在检测结果体系上做叠加。疲劳检测不是单点算法问题,而是一个持续迭代的系统工程。