ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于dlib人脸关键点的疲劳驾驶检测与预警系统设计

基于dlib人脸关键点的疲劳驾驶检测与预警系统设计 简介这是一套面向计算机相关专业毕业设计的学习资源以Python和卷积神经网络实现驾驶员疲劳检测与预警系统能够对驾驶过程中的疲劳状态进行识别与提示适合正在做课程项目、毕业设计或希望进行目标检测实战训练的学生。压缩包共37个文件总大小约500MB各类型文件的分工较为清晰py源码覆盖训练、测试、摄像头检测、视频检测与模型评估等模块pyc为编译缓存pth为预训练模型权重jpg为检测效果样例txt为说明文档另有数据集zip和训练日志可直接用于复现实验。项目经导师指导并获评98分已有54人学习下载代码均经过本地编译和严格调试可运行无误。内容还包含SSD网络、VGG16特征提取、损失函数与数据增强等实现便于对照学习目标检测与疲劳判断的完整流程适合作为高分毕设参考。1. 疲劳驾驶检测不是识别人脸而是识别“睁不开眼”凌晨两点的国道方向盘一抖车头偏了半米眼睛再睁开时背脊全是冷汗——这种场景下真正救人的不是一个能认出你是谁的人脸识别系统而是一个能在你眼皮快合上的时候喊醒你的预警装置。基于 Python 的人脸识别驾驶员疲劳检测与预警系统核心任务不是判断“这是谁”而是判断“这个人还醒不醒”。它用摄像头采集驾驶舱画面靠人脸关键点计算眼睛开合程度、打哈欠频率、头部姿态变化在阈值连续超限时触发声光报警。这套方案适合做毕业设计、车载产品原型以及安防场景的疲劳监控最吸引人的地方在于不依赖 GPU不训练深度模型一台普通笔记本加一个 USB 摄像头就能跑起来。2. 用 dlib 提取人脸关键点选型理由与最小可运行代码2.1 为什么选 dlib 而不是 OpenCV DNN做疲劳检测的人脸环节市面上常见方案有三类OpenCV 自带的人脸检测器、基于深度学习的检测模型如 OpenCV DNN 加载 Caffe/TensorFlow 模型、dlib 的 HOG 检测器加 68 点关键点模型。很多“人脸识别门禁系统设计”会首选 OpenCV DNN因为检测精度高在公开评测集上表现亮眼。但我做驾驶员疲劳检测时还是选 dlib理由很现实车载或毕设环境基本都是 CPU 计算OpenCV DNN 检测一张 640x480 的画面在普通笔记本上可能要 80 到 150 毫秒而 dlib 的 HOG 检测器加 68 点 landmark 在同尺寸下能跑到 20 到 40 毫秒实时性差距明显。更重要的是dlib 把“检测人脸”和“定位关键点”打包成两个简单接口预训练模型文件开箱即用省掉自己训练关键点模型的麻烦。还有一个细节是版本兼容性。dlib 的 Python 包在 Windows 上安装很折腾很多 python 安装教程里卡人最多的就是这一步——编译 dlib 需要 C 工具链和 CMake装不好就报cl.exe相关的红字错误。我的习惯是先用 conda 建一个 Python 3.9 的虚拟环境再直接conda install dlib一分钟装完省得折腾编译问题。如果你用 vscode python 环境配置就先把 conda 环境选好再装 opencv-python 和 dlib后面所有代码都在这个环境里跑。2.2 摄像头实时关键点检测下面这段代码是这个系统的地基打开摄像头逐帧检测人脸标出 68 个关键点。代码很短但每个参数都值得抠清楚。import cv2 import dlib # 初始化检测器和关键点预测器 # shape_predictor_68_face_landmarks.dat 是 dlib 官方预训练模型 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) # 0 表示第一个摄像头 if not cap.isOpened(): raise IOError(摄像头打开失败检查驱动或设备编号) while True: ret, frame cap.read() if not ret: break # 统一缩放到 640 宽保证检测速度 frame cv2.resize(frame, (640, 480)) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 第二个参数 1 表示图像金字塔上采样一次小脸更容易被检出 faces detector(gray, 1) for face in faces: landmarks predictor(gray, face) for i in range(68): x landmarks.part(i).x y landmarks.part(i).y cv2.circle(frame, (x, y), 1, (0, 255, 0), -1) cv2.imshow(Face Landmarks, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里的detector(gray, 1)是很多人会忽略的关键参数。1表示对图像做一次金字塔上采样相当于把图像放大两倍再检测能让远处的小脸被找到代价是耗时增加设为0则只检测原始尺寸速度快但漏检率高。我一般用1因为在驾驶舱这种固定场景里人脸通常占画面比例较大一次上采样足够没必要用2。另一个关键点是cv2.resize到 640x480这比直接处理 1080p 快了近三倍而且 68 点模型对 640 宽度下的人脸已经能给出稳定的关键点位置。2.3 参数和坐标还原如果摄像头分辨率是 1280x720而你在检测前缩小到了 640x480那么关键点坐标是基于缩小后图像的直接叠加到原图上会对不齐。需要记录缩放比例并在使用坐标时还原。后面计算 EAR、画框、截图都会踩这个坑。scale_x original_width / 640 scale_y original_height / 480 # 以左眼外眼角为例坐标还原到原图 left_eye_outer_x int(landmarks.part(36).x * scale_x) left_eye_outer_y int(landmarks.part(36).y * scale_y)提示dlib 的 68 点模型文件shape_predictor_68_face_landmarks.dat约 60MB 到 100MB不同版本大小略有差异但必须是 68 点版本。网上有些精简版只有 5 点只能定位眼睛和嘴巴的大概位置算不了 EAR别下错了。坐标还原还有个隐藏问题如果你先缩小再检测然后把缩小后的关键点坐标乘回原图尺寸当人脸边框很近时精度还凑合但一旦人脸离摄像头远landmark 在小图上产生的像素级误差会被放大。稳妥做法是先用小图做人脸检测拿到人脸框再把框映射回原图在原图上调用predictor取关键点。这样检测快关键点精度也不损失。3. EAR、MAR 与头部姿态把疲劳变成可计算的数值3.1 眼睛的 EAR闭眼程度怎么量化人脸关键点拿到后疲劳检测的下一步是把“眼睛睁开多少”变成一个稳定数值。学界最常用的指标是 EAREye Aspect Ratio眼睛纵横比它用 6 个关键点计算一个比例垂直方向的眼睑距离除以水平方向的眼睛宽度。dlib 的 68 点模型里左眼索引是 42 到 47右眼索引是 36 到 41每个眼睛恰好 6 个点。from scipy.spatial import distance as dist def eye_aspect_ratio(landmarks, eye_indices): 计算单个眼睛的纵横比 EAR eye_indices: 6 个关键点索引按顺序为 外眼角、上眼睑内侧、上眼睑外侧、内眼角、下眼睑外侧、下眼睑内侧 p1 landmarks.part(eye_indices[0]) p2 landmarks.part(eye_indices[1]) p3 landmarks.part(eye_indices[2]) p4 landmarks.part(eye_indices[3]) p5 landmarks.part(eye_indices[4]) p6 landmarks.part(eye_indices[5]) # 垂直方向两段距离 A dist.euclidean((p2.x, p2.y), (p6.x, p6.y)) B dist.euclidean((p3.x, p3.y), (p5.x, p5.y)) # 水平方向宽度 C dist.euclidean((p1.x, p1.y), (p4.x, p4.y)) return (A B) / (2.0 * C) # 左右眼分别计算后取平均减小单眼遮挡带来的误差 left_ear eye_aspect_ratio(landmarks, [42, 43, 44, 45, 46, 47]) right_ear eye_aspect_ratio(landmarks, [36, 37, 38, 39, 40, 41]) ear (left_ear right_ear) / 2.0EAR 的物理含义很直观睁眼时上下眼睑距离大EAR 大约在 0.25 到 0.35 之间闭眼时上下眼睑几乎重合EAR 会掉到 0.1 以下。因为是比例值它对不同人脸尺寸有一定鲁棒性——摄像头拉近拉远时分子分母同尺度变化EAR 不会剧烈漂移。但要注意EAR 阈值不是通用的单眼皮、眯眼习惯、戴眼镜都会影响基线值。我一般先用正常驾驶视频记录 2 分钟的 EAR 均值低于均值的 70% 才判定为“闭眼”而不是拿到代码直接填 0.2。3.2 打哈欠的 MAR 与嘴部判定疲劳的第二个信号是连续打哈欠。和 EAR 同理嘴部也可以算一个嘴巴纵横比 MARMouth Aspect Ratio。dlib 的 68 点里嘴部关键点分布在 48 到 67外唇线是 48 到 59。这里用外唇线的 6 个主特征点计算。def mouth_aspect_ratio(landmarks): 计算嘴巴纵横比 MAR用于检测打哈欠 选取右嘴角(48)、左上唇(49)、左下唇(51)等关键点 p1 landmarks.part(48) # 右嘴角 p2 landmarks.part(49) # 上唇右 p3 landmarks.part(50) # 上唇中 p4 landmarks.part(51) # 上唇左 p5 landmarks.part(55) # 下唇右 p6 landmarks.part(56) # 下唇中 p7 landmarks.part(57) # 下唇左 p8 landmarks.part(58) # 下唇右下方 p9 landmarks.part(59) # 下唇左下方 A dist.euclidean((p2.x, p2.y), (p9.x, p9.y)) B dist.euclidean((p3.x, p3.y), (p8.x, p8.y)) C dist.euclidean((p4.x, p4.y), (p7.x, p7.y)) D dist.euclidean((p1.x, p1.y), (p5.x, p5.y)) return (A B C) / (3.0 * D)正常说话时 MAR 在 0.3 到 0.5 之间波动打哈欠时嘴巴张大MAR 会超过 0.65 甚至更高。但单独看一帧的 MAR 没有意义因为说话、大笑也会让嘴巴张开。真正有判别力的是“MAR 持续超过阈值的时间长度”。一个哈欠通常持续 1.5 到 4 秒而普通说话每个音节只有 200 到 400 毫秒。所以系统里我一般记录 MAR 超阈值帧的连续长度超过 15 帧30fps 下约 0.5 秒才开始计入哈欠候选超过 45 帧1.5 秒才判定为一次有效哈欠。3.3 点头疲劳solvePnP 估计头部俯仰角疲劳驾驶还有个典型动作是“点头”——意识模糊时颈部肌肉放松头部会缓慢下垂然后猛地抬起。判断这个动作需要知道头部的俯仰角做法是用 2D 关键点和 3D 头部模型点做透视解算。这就是 cv2.solvePnP 的用途给定一张脸上的 2D 关键点坐标以及同一个点在 3D 空间中的标准坐标反推出相机的旋转和平移。通常取鼻尖、下巴、左右眼外角这 4 组点3D 坐标可以近似用通用的人脸模型值。import numpy as np model_points_3d np.array([ (0.0, 0.0, 0.0), # 鼻尖 (0.0, -63.6, -12.5), # 下巴 (-45.0, 32.5, -32.5), # 左眼外角 (45.0, 32.5, -32.5), # 右眼外角 ], dtypenp.float64) def estimate_head_pitch(landmarks, frame_width, frame_height): 估计头部俯仰角pitch返回角度负值表示低头 image_points np.array([ (landmarks.part(30).x, landmarks.part(30).y), # 鼻尖 (landmarks.part(8).x, landmarks.part(8).y), # 下巴 (landmarks.part(36).x, landmarks.part(36).y), # 左眼外角 (landmarks.part(45).x, landmarks.part(45).y), # 右眼外角 ], dtypenp.float64) # 近似相机内参焦距取图像宽度主点取画面中心 focal_length frame_width center (frame_width / 2.0, frame_height / 2.0) camera_matrix np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtypenp.float64) dist_coeffs np.zeros((4, 1)) _, rvec, _ cv2.solvePnP(model_points_3d, image_points, camera_matrix, dist_coeffs) rotation_matrix, _ cv2.Rodrigues(rvec) # 从旋转矩阵提取欧拉角取 pitch俯仰 pitch np.degrees(np.arctan2( rotation_matrix[1, 2], rotation_matrix[2, 2] )) return pitch这套单目姿态估计的精度能达到 ±10 度以内对判断“低头”这种大幅度动作足够。因为相机内参是近似的用画面宽度做焦距、画面中心做主点pitch 的绝对值不完全准确但相对变化量很可靠。我实际使用时只看“pitch 在 10 秒内持续向负方向偏移并达到 25 度以上”不纠结绝对角度。需要注意的是如果人脸不在画面中心主点假设会失效所以部署时摄像头最好正对驾驶员面部。3.4 综合判定流程与初版阈值这三个特征单独用都很容易误报必须组合成一个状态机每帧计算 EAR、MAR、pitch然后在时间维度上做统计。初版阈值可以这样设EAR 低于 0.22 记为闭眼帧MAR 高于 0.65 记为张嘴帧低头角度超过 20 度记为低头帧。这些值不是定死的在第 6 章会讲怎么用离线视频校准。def compute_features(landmarks, frame): ear (eye_aspect_ratio(landmarks, [42,43,44,45,46,47]) eye_aspect_ratio(landmarks, [36,37,38,39,40,41])) / 2.0 mar mouth_aspect_ratio(landmarks) pitch estimate_head_pitch(landmarks, frame.shape[1], frame.shape[0]) return { ear: round(ear, 3), mar: round(mar, 3), pitch: round(pitch, 1), eye_closed: ear 0.22, mouth_open: mar 0.65, head_down: pitch 0 and abs(pitch) 20 }这段代码把每个维度的判定结果汇总成一份特征字典后续的滑动窗口判定只需要读这个字典里的布尔值。这样设计的好处是模块解耦摄像头、关键点提取、疲劳特征计算、预警决策分属不同文件改阈值时不用动特征提取逻辑。如果你拿到的“源码”是单文件堆下来的我建议先拆成这个结构后面所有调试都会顺手很多。4. 预警系统从单帧到连续决策滑动窗口与分级声光报警4.1 为什么单帧 EAR 低不能报警最常见的错误是把“某一帧 EAR 低于阈值”当成疲劳信号。正常人每 3 到 5 秒都会眨眼一次每次眨眼持续 200 到 400 毫秒在 30fps 下就是 6 到 12 帧的 EAR 低谷。如果按单帧判断系统会在你正常眨眼时疯狂报警不出 5 分钟驾驶员就会把设备关掉。反过来如果只要求一帧低就报警显然太敏感只要求连续多帧低又可能漏掉快速连续眨眼的前兆。所以预警决策必须在时间窗口上做统计最经典的指标是 PERCLOS眼睛闭合时间占比。4.2 滑动窗口 PERCLOS 的实现PERCLOS 的定义很简单在固定时间窗口内闭眼帧数占总帧数的比例。研究普遍认为 PERCLOS 超过 0.4 表示明显疲劳。实现上不需要存几十秒的视频用一个固定长度的 deque 滚动记录每帧的“闭眼状态”就行。from collections import deque class SlidingWindowDecision: def __init__(self, window_size30, ear_threshold0.22, perclos_threshold0.4): self.window_size window_size # 窗口长度30 帧在 30fps 下等于 1 秒 self.ear_threshold ear_threshold # 闭眼判断阈值 self.perclos_threshold perclos_threshold # PERCLOS 报警阈值 self.buffer deque(maxlenwindow_size) def push(self, ear_value): 每帧推入一个 EAR 值返回是否达到疲劳报警条件 is_closed 1 if ear_value self.ear_threshold else 0 self.buffer.append(is_closed) if len(self.buffer) self.window_size: return False perclos sum(self.buffer) / self.window_size return perclos self.perclos_threshold def reset(self): self.buffer.clear()参数window_size30是关键30fps 下窗口正好覆盖 1 秒。这个长度既能容纳一次正常眨眼200 到 400 毫秒而不触发报警又能在大约 16 帧闭眼0.5 秒时让 PERCLOS 快速攀升。不要把这个值设得太大比如 90 帧因为那会让报警延迟 3 秒——驾驶员已经撞上护栏报警才响就毫无意义了。perclos_threshold0.4是经验值实际测试时可以从 0.3 到 0.5 逐步调整看驾驶员真实困倦时的表现再定。4.3 分级预警与防重报机制滑动窗口判定通过后不能直接进入“无限报警”状态必须加防重报逻辑。常见做法是分级预警一级提醒、二级警告、三级强制声光。一级发生时系统只播放轻提示音二级会语音播报“请靠边停车休息”三级则触发持续蜂鸣并在中控屏显示红色警示。每个级别之间要有锁定时长比如二级触发后 20 秒内不重复告警防止一次疲劳状态被反复上报。import time class FatigueWarning: def __init__(self): self.last_warning_time 0 self.cooldown 20 # 预警冷却时间单位秒 def try_warning(self, level): now time.time() if now - self.last_warning_time self.cooldown: return False if level 1: self._beep(800, 200) elif level 2: self._beep(1200, 400) self._beep(1200, 400) elif level 3: self._beep(1500, 800) for _ in range(3): self._beep(1800, 200) self.last_warning_time now return True def _beep(self, freq, duration_ms): try: import winsound winsound.Beep(freq, duration_ms) except ImportError: pass # 非 Windows 环境可换 pygame.mixer 实现冷却时间cooldown20是个权衡值。太短会持续响铃让驾驶员烦躁太长会漏掉“二次入睡”的场景。我实际测试下来20 到 30 秒比较合理既给了驾驶员反应时间又能在 30 秒后再次提醒。语音播报如果用 Windows 自带工具可以叠加 winsound 的MessageBeep或者调用 PowerShell 的System.Media.SpeechSynthesizer说一句固定文本跨平台则推荐 pygame.mixer 播放预录制的 mp3。4.4 源码目录结构与模块划分在动手写完整程序之前先把目录结构定清楚。一份高分的毕设源码不是把所有函数堆进一个main.py而是让新手扫一眼就知道每个文件在干什么。文件/目录职责关键说明main.py主程序入口初始化各模块、启动主循环camera.py视频流采集封装摄像头初始化、帧率控制、缩放detector.py人脸检测与关键点提取加载 dlib 模型返回 68 点 landmarksfeatures.py疲劳特征计算输出 ear、mar、pitch 和逐项布尔标志decision.py滑动窗口判定负责 PERCLOS 与哈欠、低头状态统计warning.py声光报警分级警报警示与冷却锁存data/录制的测试视频与标注记录用于离线验证阈值models/dlib 预训练模型文件shape_predictor_68_face_landmarks.dat这个目录结构本身就是为“后续维护”设计的。你拿到任何一份源码第一步不是读代码而是看目录下有没有data和models这两个目录——没有models的源码跑不起来没有data的源码无法验证。这两类资源是这套系统能不能落地复现的硬门槛。5. 驾驶员疲劳检测 5 个翻车现场与排查清单5.1 dlib 装不上连 python 环境都起不来现象pip install dlib在 Windows 上疯狂报错最后的红字是error: command cl.exe failed。很多 python 安装教程走到这一步就直接劝退用户了。原因新版 dlib 从源码编译时需要 MSVC 编译器和 CMakeWindows 默认环境经常缺这两个组件。如果用的是 Python 3.10 以上部分 dlib 版本还没有预编译轮子问题更严重。解决优先用 conda 建虚拟环境再装预编译版本。conda create -n fatigue python3.9然后conda install dlibconda 会直接抓编译好的二进制包全程不用碰编译器。另外一个变通方案是用pip install dlib-bin这类第三方预编译包但版本匹配不如 conda 稳定。装完后立刻打开 python 执行import dlib能过才算环境真配好了。5.2 摄像头画面卡成 PPTCPU 占用接近 100%现象一打开摄像头画面肉眼可见地掉帧程序界面像幻灯片有时还会出现画面延迟越来越严重的情况。原因视频流的原始分辨率通常是 1280x720 或 1920x1080直接在这种尺寸上跑 dlib 的 HOG 检测器加 68 点关键点每一帧要处理几十毫秒。加上滑动窗口和报警逻辑后主循环的耗时超过了帧间隔画面自然卡顿。解决检测前先缩小画面到 640 宽关键点坐标按缩放比还原。同时把detector(gray, 1)的上采样次数降到1如果还是慢改成0。还可以直接开两个线程一个线程只负责读帧另一个线程做检测和逻辑判断主线程只管绘制和报警。架构上叫生产者消费者模型疲劳检测帧率在 20fps 以上就不会影响用户体验。5.3 EAR 抖动导致误报驾驶员眨个眼就报警现象明明睁着眼EAR 值却在 0.3 和 0.15 之间剧烈跳动系统把正常眨眼当成了疲劳闭眼PERCLOS 频频越过阈值。原因光照变化、摄像头自动曝光、人脸在画面中轻微晃动都会让 68 点关键点在垂直方向抖动几个像素。EAR 的分母是眼睛宽度一般有 20 到 40 个像素分子是垂直距离只有 5 到 15 个像素关键点上下跳动 2 个像素就可能让 EAR 波动 30%。解决对连续帧的 EAR 做移动平均窗口取 5 帧等效于低通滤波。再有就是刚才说的确保检测图像稳定不要用抖动剧烈的手持摄像头。如果特定光线条件下 EAR 还是跳得厉害把检测区域固定在上一次人脸框附近减少全图搜索带来的坐标漂移。ear_history deque(maxlen5) def smooth_ear(ear_value): ear_history.append(ear_value) return sum(ear_history) / len(ear_history)5.4 白天正常、夜间检测失效摄像头像瞎子一样现象傍晚六点一过系统开始频繁漏人脸有时干脆一张脸都检不到报警形同虚设。原因绝大多数 USB 摄像头的自动增益和自动曝光在弱光环境下会把画面提亮结果噪点剧增dlib 的 HOG 检测器在噪点图像上检索人脸的能力远低于白天。夜间只有仪表盘背光时脸部对比度太低检测器直接“看不见”。解决优先换用带红外补光的广角摄像头这也是“人脸识别门禁系统设计”在夜间场景的通行做法。如果只能用普通摄像头手动把曝光锁定在一个合理范围用 OpenCV 设置摄像头属性。要说明的是不同型号摄像头对 OpenCV 属性的支持度不一致代码里设完必须顺手确认设置是否生效。# 尝试锁定曝光和增益数值因摄像头驱动不同需要现场调 cap.set(cv2.CAP_PROP_EXPOSURE, -4) cap.set(cv2.CAP_PROP_GAIN, 0) cap.set(cv2.CAP_PROP_BRIGHTNESS, 70) # 验证设置是否生效 actual_exposure cap.get(cv2.CAP_PROP_EXPOSURE) print(exposure:, actual_exposure)如果actual_exposure还是原来的值说明摄像头驱动不支持该属性只能用红外补光方案不要在代码属性上继续纠缠。5.5 戴墨镜、戴口罩时关键点乱飞EAR 和 MAR 全部失效现象驾驶员一戴墨镜EAR 值时高时低乱跳戴口罩时MAR 持续显示“张嘴”哈欠误报不断。测评时一戴道具系统立刻失灵观感极差。原因dlib 的 68 点模型是在正脸无遮挡图片上训练的墨镜区域没有关键点语义信息模型会强行在镜框上“幻想”出眼睑轮廓口罩同理嘴部点会被推到口罩边缘导致 MAR 曲线完全失真。解决对关键点区域做可信度预检。一个简单可行的启发式是计算眼睛区域的灰度方差如果眼部区域被墨镜遮住灰度方差会显著低于正常皮肤区域这时丢弃该帧的 EAR转而用头部姿态和方向盘动作作为备选判定信号。# 用拉普拉斯方差判断眼部区域纹理是否可信 def eye_region_reliable(gray_frame, landmarks, eye_indices): points [(landmarks.part(i).x, landmarks.part(i).y) for i in eye_indices] xs [p[0] for p in points] ys [p[1] for p in points] x, y, w, h cv2.boundingRect(np.array(points)) margin max(w, h) // 4 roi gray_frame[max(0, y-margin):yhmargin, max(0, x-margin):xwmargin] if roi.size 0: return False variance cv2.Laplacian(roi, cv2.CV_64F).var() return variance 50 # 低于阈值说明区域过平疑似遮挡或失焦这个 50 的阈值是经验值需要根据摄像头噪点水平调整。墨镜镜片反射强光时拉普拉斯方差反而可能很高还得结合“左右眼 EAR 差是否过大”来辅助判断。更周全的方案是引入第二路相机专门采集方向盘或者驾驶员手部动作但这已经超出了本标题的范围先不展开。6. 用离线视频校准阈值让系统不在你眼皮底下打盹6.1 EAR 曲线与阈值校准装上跑通之后最不该做的事情就是拿默认阈值直接上路实测。每个人脸的 EAR 基线不一样摄像头安装角度不一样光照条件不一样阈值必须基于你自己的数据来定。我的做法是录两段 5 分钟视频一段正常驾驶状态保持清醒、正常眨眼一段模拟疲劳状态故意半闭眼、点头、打哈欠。离线回放视频把每一帧的 EAR 导出成 CSV 文件然后用 matplotlib 画出曲线。import csv import matplotlib.pyplot as plt # 回放视频并记录特征 with open(ear_log.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame, ear, mar, pitch]) # 循环里逐帧调用 compute_features把结果写进去 # 绘制 EAR 曲线 data list(csv.reader(open(ear_log.csv))) ears [float(row[1]) for row in data[1:]] plt.plot(ears) plt.axhline(y0.22, colorr, linestyle--) plt.show()看曲线的时候重点关注两点一是正常驾驶段的最低 EAR 谷值出现在哪里二是模拟疲劳段的 EAR 持续低于哪个值。把阈值设在这两组值之间比如正常段最低是 0.25疲劳段稳定在 0.12那阈值取 0.18 到 0.2 会留有足够安全边际。这里也能验证 PERCLOS 窗口长度是否合理——如果正常眨眼的 EAR 低谷都持续超过 0.5 秒说明摄像头帧率太低或者关键点抖动太大窗口和阈值都要重新调整。6.2 用混淆矩阵评估系统阈值定完后还需要量化系统表现这是从“能跑”到“可以作为毕设成果”的关键一步。准备一段 3 分钟左右的标注视频人工记录哪些时间段属于疲劳状态哪些属于清醒状态然后让系统跑一遍输出判定结果统计真正疲劳被报警的次数真阳性、清醒状态被误报的次数假阳性、疲劳状态没报警的次数假阴性。from sklearn.metrics import confusion_matrix # 假设 y_true 是人工标注的疲劳帧y_pred 是系统输出的报警帧 # 1 表示疲劳0 表示正常 y_true [1, 1, 0, 1, 0, 0, 1] y_pred [1, 0, 0, 1, 1, 0, 1] tn, fp, fn, tp confusion_matrix(y_true, y_pred).ravel() print(f准确率: {(tp tn) / (tp tn fp fn):.2f}) print(f误报率: {fp / (fp tn):.2f}) print(f漏报率: {fn / (fn tp):.2f})对疲劳检测这个场景漏报率比误报率严重得多。误报顶多是吵漏报意味着系统在驾驶员真正睡着时没响。如果你发现漏报率高优先调低闭眼阈值和 PERCLOS 阈值如果误报率高则抬高阈值同时适当增大窗口长度。这里没有“一套万能参数”参数调整记录本身就是高分毕设的加分项。6.3 部署与演示建议最后说说演示环节。现场答辩或者给导师演示的时候最怕的是“现场没网、摄像头驱动装不上、dlib 模型文件没带”。我把这几条当成血泪教训把所有依赖提前装进独立 conda 环境模型文件放在程序相对路径下而不是绝对路径摄像头驱动提前用另一台机器试过。演示用的视频建议提前录好并存成 MP4 文件——程序里做一个--video参数切换摄像头输入和视频文件输入这样即使现场摄像头被占用也能顺利跑通。我做这个项目时翻车最多的地方其实是摄像头安装角度装太正了驾驶员脖子一仰就出框装太高了又只能拍到头顶最后在座椅侧后方装了一个 30 度角的支架画面稳定性和人脸检测率才同时达标。如果你只是做毕设没有真实驾驶舱用普通办公室椅子加一个显示器支架也能模拟大部分视角问题。疲劳检测这个方向难点从来不是某一个算法有多高深而是把“睁不开眼”这个模糊的人类状态翻译成一个在特定场景下稳定可靠的数值决策。希望帮到你。本文还有配套的精品资源点击获取
返回列表