☰
基于Python与OpenCV的实时疲劳驾驶检测系统实战解析
2026/9/28 2:03:42 网站建设 项目流程

简介:这套Python源码面向驾驶员疲劳监测场景,基于面部特征分析实现,适合计算机视觉开发者、车载安全系统研究人员及毕业设计学生参考。压缩包共22个文件,含5个Python程序、10个XML配置文件、1个MP3音频文件、1个DAT数据文件及说明文档,整体大小68.32MB,目录结构清晰,便于快速定位相关模块。目前已有93人学习,系统通过摄像头实时捕捉面部图像,借助OpenCV完成面部及眼睛区域定位,并将眼睛闭合频率、眨眼模式作为疲劳判定指标,判别过程直观高效。源码内含面部检测、眼睛区域定位、疲劳特征分析、预警机制和用户界面等核心模块,逻辑完整,对实时视频流逐帧处理,能有效识别眼睛闭合时间变长、眨眼频率增加等疲劳信号。同时提供音频预警提醒,当疲劳特征达到设定阈值时自动触发,适用于长途驾驶、公共交通及货运车辆监控等场景,可作为课程设计或实际项目基础,便于二次开发与集成。

1. 疲劳检测系统不是玄学:一套 Python 方案能做什么

顶着「Python基于驾驶员面部特征的疲劳检测系统源码.rar」这个标题来找方案的人,十有八九不是在找论文,而是在找一套能跑起来、能拿去交差或做产品的代码。这类系统的核心诉求是:用普通摄像头实时判断一个人是否疲劳,疲劳了就报警。它解决的问题很具体——长途货运司机、网约车平台的安全监管、驾校模拟器学员状态监测,甚至矿山和工厂的固定岗位值守。所谓「面部特征」,本质上就是靠人脸关键点算出眼睛开合度、嘴巴张合度、头部姿态这几个量,再用阈值和持续时长去判定疲劳等级。它不是一个黑匣子,而是一套由 OpenCV、dlib(或 MediaPipe)和一个置信度判定逻辑组成的管线,指标够明确,技术栈够常见,是 Python 开发者最容易落地的计算机视觉方向之一。下面我会把这条管线拆开,从选型到参数设置再到常见的翻车现场,按我自己做过项目的顺序讲清楚。

2. 先把管线立起来:人脸检测与关键点检测的选型逻辑

2.1 为什么主流实现都绕不开 dlib 的 68 点模型

疲劳检测的第一步不是算疲劳,而是先稳定地拿到眼睛和嘴巴的位置。常见的做法是先用一个人脸检测器找出人脸框,再在框内定位 68 个关键点,其中眼睛区域占 6 个点、眉毛 5 个点、嘴巴 20 个点。为什么大家都愿意用 dlib 而不是自己训练一个关键点模型?因为 dlib 自带两个经典模型:mmod_human_face_detector.dat(基于 HOG 或 MMOD 的人脸检测)和shape_predictor_68_face_landmarks.dat(68 点关键点回归)。后者是一个在 iBUG 300-W 数据集上训练的回归树集成模型,文件体积约 100MB,检测速度在中低端 CPU 上能做到 30ms 到 50ms 一帧,配合 OpenCV 的 VideoCapture 读摄像头,正好卡在实时性的边缘线上。

另一个常见替代是 MediaPipe 的 FaceMesh,它输出 468 个关键点,对眼睛和嘴唇的轮廓描述更细,而且自带 iris 追踪。但 FaceMesh 的一个麻烦在于它是 TensorFlow Lite 模型,在纯 CPU 环境下首次加载的初始化时间较长,推理速度通常比 dlib 的 68 点略慢,再加上 468 点里真正用到的其实还是眼睛和嘴巴周边的几十个点,性价比并不高。所以行业内的保守做法是:dlib 负责检测和关键点,OpenCV 负责图像预处理和画框,numpy 负责数值计算。这套组合稳定、可控、好调试,源码里绝大多数可运行的疲劳检测项目都是这样组织的。

2.2 从摄像头帧到关键点坐标:最小可运行链路

无论源码包怎么组织,主循环都是这样一个骨架:读帧、转灰度、检测人脸、取关键点、算指标、判定状态。这里我给出最简可运行的代码,它对应了源码里detect_face()到get_landmarks()这一段逻辑。使用时注意把两个.dat模型文件的路径改成你本机的实际路径。

import cv2 import dlib # 初始化检测器和关键点模型 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 打开摄像头,0 代表默认摄像头,换成文件路径可读视频 cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break # 统一转灰度,dlib 的检测器内部基于灰度图做 HOG 特征 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 第二参数 1 表示对图像做一次上采样,能提高小脸检出率,代价是耗时翻倍 faces = detector(gray, 1) for face in faces: # 返回 68 个关键点,每个点是 dlib.point 对象,含 x, y 坐标 landmarks = predictor(gray, face) # 这里只演示取左眼外眼角和右眼外眼角,后面的 EAR 计算要取完整 6 点 left_eye_x = landmarks.part(36).x right_eye_x = landmarks.part(45).x cv2.circle(frame, (left_eye_x, landmarks.part(36).y), 2, (0, 255, 0), -1) cv2.circle(frame, (right_eye_x, landmarks.part(45).y), 2, (0, 255, 0), -1) cv2.imshow("fatigue_detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这段代码里有两个直接影响后续指标质量的参数。第一个是detector(gray, 1)的上采样参数,默认0表示不放大图像,1表示把图像放大一倍再检测,小脸(比如摄像头离人较远)检出率明显提升,但每帧耗时也会涨。对车载场景,我一般用0或1交替测试,以稳定 25FPS 为准。第二个是cv2.VideoCapture(0)的读帧方式,在 Windows 上 OpenCV 默认用 DSHOW 后端,在 Linux 上默认走 V4L2,如果你的摄像头在 Linux 上打不开,可以显式指定cv2.VideoCapture(0, cv2.CAP_V4L2)。很多新手在这里翻车,以为代码错了,其实只是后端不对。

2.3 坐标系与关键点索引:68 点布局必须刻在脑子里

dlib 的 68 点索引是这套方案的“接口契约”,记不住索引,后面所有特征计算都会乱套。这里把我常用的索引段整理出来,几乎每个源码包的注释里都会用到:

区域索引范围典型用途
下颌轮廓0-16头部姿态估计中的脸型约束
左眉17-21疲劳表情辅助判断
右眉22-26疲劳表情辅助判断
鼻梁与鼻尖27-35脸部中心参考点
左眼36-41计算左眼 EAR
右眼42-47计算右眼 EAR
外嘴唇48-59计算嘴巴开合度 MAR
内嘴唇60-67更精细的哈欠检测

眼睛的 6 个点是有顺序的:36 是左眼角外侧,37 是上眼睑左侧,38 是上眼睑右侧,39 是右眼角内侧,40 是下眼睑右侧,41 是下眼睑左侧。写代码时最容易犯的错是把 37 和 38 当成一组“垂直点”去算距离,实际上计算眼睛开合度用的是上眼睑点和下眼睑点的纵向距离,取的是 37 与 41、38 与 40 这两组。这个细节决定了 EAR 公式里的分子写的是p2 - p6和p3 - p5还是其他组合,写错一个索引,闭眼阈值就完全失效。

3. 从关键点到疲劳特征:EAR 与 MAR 的数学直觉和代码实现

3.1 眼睛开合度 EAR:为什么它能抵抗人头晃动

疲劳检测里最核心的特征是眼睛的开合程度,学术上叫 Eye Aspect Ratio,简称 EAR。它的计算方式是取眼睛 6 个关键点,用两个垂直距离的平均值除以水平距离。因为分子分母都是同一套坐标系下的长度,所以人头靠近摄像头或稍微左右转动时,这个比值不会剧烈变化——这是它比单纯计算“眼睛高度像素值”高明的地方。下面是标准实现:

import numpy as np def eye_aspect_ratio(eye_points): """ 计算眼睛开合度 EAR eye_points: dlib 返回的 landmarks 对象,已按索引截取 """ # 两个垂直距离 vertical_1 = np.linalg.norm(eye_points[1] - eye_points[5]) vertical_2 = np.linalg.norm(eye_points[2] - eye_points[4]) # 水平距离 horizontal = np.linalg.norm(eye_points[0] - eye_points[3]) # 加 1e-6 防止人脸框抖动时水平距离出现 0 ear = (vertical_1 + vertical_2) / (2.0 * horizontal + 1e-6) return ear

参数上有两点值得说明。第一,1e-6是为了防止水平距离为 0 导致除零错误,这种情况在关键点抖动时会偶发,尤其是侧脸角度过大时两个眼角可能重合。第二,np.linalg.norm算的是欧氏距离,在逐帧循环里频繁调用会有性能开销,如果追求极致速度可以手动拆成((x1-x2)**2 + (y1-y2)**2) ** 0.5,但对 30FPS 的场景影响不大,不必过早优化。

调用时 LEFT_EYE_START 到 LEFT_EYE_END 的索引区间是 36 到 42(左开右闭),实际取点就是landmarks[36:42]。右眼同理取landmarks[42:48]。很多源码里会把两只眼睛的 EAR 取平均作为当前帧的 EAR,这是合理的,因为人在自然眨眼时两只眼睛不完全同步,取平均能减少单眼误判。

3.2 嘴巴开合度 MAR:哈欠识别的判定特征

哈欠检测用的是嘴部开合度 MAR,公式和 EAR 类似,但取点逻辑不同。外嘴唇 48 到 59 一共 12 个点,但计算开合度只需要 6 个代表性点:48 和 54 是左右嘴角,51 和 57 是上下嘴唇中点,49 和 53 是上嘴唇两侧,55 和 59 是下嘴唇两侧。常用做法是取(51, 57)和(49, 53)、(55, 59)这几组垂直距离的平均,再除以嘴角距离。实现如下:

def mouth_aspect_ratio(mouth_points): """ 计算嘴巴开合度 MAR,用于哈欠检测 mouth_points: 按索引截取的外嘴唇 12 点 """ # 上嘴唇三个点到下嘴唇三个点的垂直距离 vertical_1 = np.linalg.norm(mouth_points[2] - mouth_points[9]) # 51 与 57 vertical_2 = np.linalg.norm(mouth_points[3] - mouth_points[8]) # 52 与 58 vertical_3 = np.linalg.norm(mouth_points[4] - mouth_points[7]) # 53 与 59 # 嘴角水平距离,48 与 54 horizontal = np.linalg.norm(mouth_points[0] - mouth_points[5]) mar = (vertical_1 + vertical_2 + vertical_3) / (3.0 * horizontal + 1e-6) return mar

注意这里mouth_points的传参顺序要跟外嘴唇索引保持一致:48 在数组第 0 位,54 在第 5 位,51 在第 2 位,57 在第 9 位。如果你从 landmarks 里切片时用的是landmarks[48:60],那么顺序就和 dlib 的原始索引一一对应,不会错位。MAR 的正常基线大约在 0.2 到 0.4 之间,打哈欠时会冲到 0.8 以上,而且打哈欠的过程持续 3 到 5 秒,所以它有天然的时间滤波优势,比眨眼判定更稳定。

3.3 头部姿态估计:只用 6 个点也能算俯仰角

疲劳驾驶的另一个信号是头部逐渐下垂,这对应了头部的 pitch 角(俯仰角)。标准做法是拿 68 个关键点里的脸部特征点(鼻尖 30、下巴 8、左眼左角 36、右眼右角 45、嘴角左角 48、嘴角右角 54)作为 2D 特征,再和一个通用 3D 人脸模型上的对应 3D 点做 PnP 求解。OpenCV 的cv2.solvePnP能直接干这件事:

# 3D 模型点,来自通用人脸模型,单位是毫米 model_points = np.array([ (0.0, 0.0, 0.0), # 鼻尖 Nose tip (0.0, -63.6, -12.5), # 下巴 Chin (-43.3, 32.7, -26.0), # 左眼左角 Left eye left corner (43.3, 32.7, -26.0), # 右眼右角 Right eye right corner (-28.9, -28.9, -24.1), # 左嘴角 Left mouth corner (28.9, -28.9, -24.1) # 右嘴角 Right mouth corner ], dtype=np.float32) # 2D 点从 landmarks 中取出,注意顺序与 3D 模型点一一对应 image_points = np.array([ (landmarks[30].x, landmarks[30].y), (landmarks[8].x, landmarks[8].y), (landmarks[36].x, landmarks[36].y), (landmarks[45].x, landmarks[45].y), (landmarks[48].x, landmarks[48].y), (landmarks[54].x, landmarks[54].y) ], dtype=np.float32) # 相机内参用近似值,focal length 取图像宽度 focal_length = frame.shape[1] center = (frame.shape[1] / 2, frame.shape[0] / 2) camera_matrix = np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtype=np.float32) # 假设无镜头畸变,对普通摄像头够用 dist_coeffs = np.zeros((4, 1)) success, rotation_vector, translation_vector = cv2.solvePnP( model_points, image_points, camera_matrix, dist_coeffs ) # 旋转向量转欧拉角,顺序对应俯仰、偏航、翻滚 rvec_matrix = cv2.Rodrigues(rotation_vector)[0] proj_matrix = np.hstack((rvec_matrix, translation_vector)) euler_angles = cv2.decomposeProjectionMatrix(proj_matrix)[6] pitch, yaw, roll = euler_angles.flatten()[:3]

这段代码有三个坑要提前说。第一,dist_coeffs用全零是常见做法,但如果用的是广角摄像头或行车记录仪镜头,畸变会明显影响 pitch 的准确度,有标定参数一定要填进去。第二,3D 模型点里的-63.6等数值来自通用的 Face Pose 模型,它假设的是一个“平均脸”,对特定人脸会有固定偏差,但这个偏差在疲劳判定中通常不影响“低头”和“抬头”的相对比较。第三,decomposeProjectionMatrix返回的欧拉角里 pitch 的正负号含义需要实测确认,不同 OpenCV 版本行为一致但角度定义容易混淆,建议在测试时打印数值做一次人工校准。

4. 疲劳怎么判:PERCLOS、闭眼时长与多指标投票机制

4.1 闭眼帧数统计:PERCLOS 是行业认可的疲劳金标准

拿到逐帧的 EAR 之后,真正判定疲劳的指标不是单帧 EAR 值,而是 PERCLOS——单位时间内眼睛闭合帧数占总帧数的百分比。它来自驾驶疲劳研究领域,公式是:PERCLOS = (闭眼帧数 / 统计窗口总帧数) × 100%。判“闭眼”需要一个 EAR 阈值,通常取 0.2 到 0.25,低于阈值即视为闭眼。这里有个经验:人在正常放松状态下 EAR 大约在 0.3 左右,闭眼时降到 0.15 以下,阈值取 0.2 能在绝大多数人脸上工作。下面是核心统计逻辑:

class FatigueCounter: def __init__(self, ear_threshold=0.2, perclos_window=60, min_eye_close_time=2.0, fps=25): self.ear_threshold = ear_threshold self.perclos_window = perclos_window # 统计窗口帧数 self.close_frames = 0 # 窗口内闭眼帧数 self.total_frames = 0 self.eye_close_start = None # 连续闭眼起始时间 self.min_eye_close_time = min_eye_close_time # 判定疲劳闭眼的持续秒数 self.fps = fps def update(self, ear_value, current_time): is_close = ear_value < self.ear_threshold # 更新窗口内闭眼帧统计 if self.total_frames < self.perclos_window: self.total_frames += 1 if is_close: self.close_frames += 1 return None else: perclos = (self.close_frames / self.total_frames) * 100 # 窗口滑动:丢弃最早的一帧统计,近似 FIFO self.total_frames -= 1 self.close_frames -= 1 if is_close: self.close_frames += 1 self.total_frames += 1 # 连续闭眼时长判定 if is_close and self.eye_close_start is None: self.eye_close_start = current_time elif not is_close and self.eye_close_start is not None: close_duration = current_time - self.eye_close_start self.eye_close_start = None if close_duration > self.min_eye_close_time: return "fatigue" return None return None

这段逻辑里有几个参数值得反复调。perclos_window=60表示统计 60 帧的窗口,在 25FPS 下就是 2.4 秒。窗口太短会让 PERCLOS 波动剧烈,窗口太长则反应迟钝,建议在 1.5 秒到 3 秒之间调。min_eye_close_time=2.0是判定“疲劳性闭眼”的最短持续时间,正常人眨眼只持续 100 到 200 毫秒,不会超过这个值,所以 2 秒能有效过滤正常眨眼。注意这个阈值和 PERCLOS 阈值是两套独立的逻辑,前者强调“连续闭眼”,后者强调“频繁闭眼”,两者同时超过才触发报警比较稳妥。

4.2 眨眼频率统计:单次闭眼不够,频率异常才有意义

疲劳的另一个早期信号是眨眼频率降低或异常升高。正常驾驶时人每分钟眨眼 10 到 20 次,疲劳时会先减少到 5 次以下,或者出现“长时间不眨眼再连续快速眨眼”的模式。实现眨眼次数检测需要在状态机里识别“闭眼-睁眼”的这个变化沿,而不是简单地统计闭眼帧数:

class BlinkCounter: def __init__(self, ear_threshold=0.2, min_blink_interval=0.1): self.ear_threshold = ear_threshold self.prev_eye_state = False # 上一帧是否为闭眼状态 self.blink_count = 0 self.start_time = None self.blink_freq = 0.0 self.min_blink_interval = min_blink_interval # 两次眨眼最小间隔,过滤异常抖动 def update(self, ear_value, current_time): is_close = ear_value < self.ear_threshold # 检测到从睁眼变为闭眼,记一次眨眼的开始 if is_close and not self.prev_eye_state: self.start_time = current_time # 检测到从闭眼变为睁眼,完成一次眨眼 if not is_close and self.prev_eye_state and self.start_time is not None: blink_duration = current_time - self.start_time # 眨眼时长在 50ms 到 500ms 之间才算一次正常眨眼 if 0.05 < blink_duration < 0.5: self.blink_count += 1 self.start_time = None self.prev_eye_state = is_close return self.blink_count

眨眼频率的统计要有时间基准。推荐每 60 秒输出一次平均频率:freq = blink_count / elapsed_minutes。在界面或日志里直接显示这个值,你会发现在第 20 分钟左右就能看到明显下降趋势,比 PERCLOS 报警出现得更早。不过单独用低眨眼频率报警会有误报——很多人在专注看路时本就会减少眨眼,所以它应该作为加权因子而不是独立报警条件。

4.3 多指标投票:PERCLOS、连续闭眼、MAR、pitch 怎么组合

单一指标都有盲区:PERCLOS 在光照差时容易误判,连续闭眼时长对“眯眼”不敏感,MAR 会被说话或大笑干扰,pitch 会被人调整座椅高度影响。所以我实际部署时用的是加权投票逻辑:每个指标独立输出一个 0 到 1 的疲劳评分,总分超过阈值才报警。

def fatigue_decision(perclos, close_duration, mar_avg, pitch_avg): """ 简易投票: perclos: 当前窗口 PERCLOS 百分比(0-100) close_duration: 最近一次连续闭眼时长(秒) mar_avg: 窗口内平均嘴巴开合度 pitch_avg: 窗口内平均头部俯仰角 """ score = 0.0 # PERCLOS 超过 40% 视为高风险 if perclos > 40: score += 1.0 elif perclos > 25: score += 0.5 # 连续闭眼超过 2 秒 if close_duration > 2.0: score += 1.0 elif close_duration > 1.0: score += 0.5 # 哈欠频率高,MAR 均值大于 0.5 且持续时间长 if mar_avg > 0.5: score += 0.5 # 头部明显下垂,pitch 超过 25 度 if pitch_avg > 25: score += 0.5 elif pitch_avg > 15: score += 0.2 # score 满分为 3.0,达到 1.5 触发一级报警,2.0 触发二级报警 if score >= 2.0: return 2 elif score >= 1.5: return 1 else: return 0

这里的阈值来自我实测的样本,你换一个摄像头、换一个人脸角度分布后必须重新标定。核心原则是:宁可晚报警,不能误报太多。误报多了司机就直接把设备关了,整套系统形同虚设。实践里我会先用前 5 分钟采集该司机正常驾驶状态下的 PERCLOS、MAR、pitch 基线,然后用“基线 + 偏移量”动态生成阈值,而不是用固定值。动态阈值的好处是能适配单眼皮、眯眯眼、常带笑容等个体差异。

5. 避坑与排查:掉帧、漏检与误报的五个现场记录

5.1 人脸检测框剧烈抖动导致 EAR 突变

现象:系统在正常行驶时频繁触发闭眼报警,查看日志发现 EAR 在 0.15 到 0.4 之间剧烈跳动。原因:dlib 的 HOG 检测器在目标脸稍微转动或光照变化时,人脸框会小幅漂移,导致关键点整体偏移,EAR 分子(垂直距离)对框位置极其敏感。解决:给 EAR 序列加一个滑动平均,窗口取 5 帧即可。或者对单帧检测结果做“上一帧位置约束”,如果当前帧人脸框中心与上一帧中心距离超过 30 像素,则沿用上一帧的框继续追踪。这个处理在 dlib 没有内置追踪器时特别重要。

5.2 光照骤变导致整帧检测不到人脸

现象:车辆驶过立交桥下或隧道入口时,画面瞬间变暗或过曝,人脸检测输出为空,系统直接跳过该帧,PERCLOS 统计被稀释。原因:dlib 的 HOG 特征对光照对比度的依赖很强,纯暗光或纯过曝区域几乎没有可用的梯度信息。解决:在送入检测器之前做一次 CLAHE 自适应直方图均衡化。具体做法是把灰度图cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))处理后再传给 detector。处理后的帧会让关键点定位更稳定,代价是每帧增加 1 到 2ms 开销。同时统计窗口里必须记录“检测不到脸”的帧数,如果连续超过 30 帧丢脸,要触发“摄像头遮挡或司机脱离视野”的提示,而不是默默忽略。

5.3 戴墨镜导致眼睛关键点完全错误

现象:司机戴上偏光墨镜后,EAR 长期处于低值,系统误报疲劳。原因:墨镜遮挡了眼睛区域的纹理特征,dlib 的关键点回归无法在遮挡区域得到有效梯度,眼睛上下眼睑的预估位置会塌缩到镜框边缘附近。解决:这是一个无解的视觉问题,不要试图用算法硬扛。行业内的通行做法是接入红外摄像头——红外光可穿透常规墨镜镜片,或者把判定策略切换为“墨镜模式下以头部姿态和哈欠检测为主”。实操上可以在系统里加一个开关或自动检测逻辑:当眼睛区域的平均对比度低于某个值时,自动调低 EAR 指标的权重。

5.4 PERCLOS 窗口用帧数统计,在掉帧时严重失真

现象:在低配工控机上实际帧率只有 12FPS,但代码里写死PERCLOS_WINDOW = 60(即 2.4 秒),导致闭眼帧占比虚高。原因:用帧数作为窗口单位,隐式假设了帧率恒定。帧率只有一半时,60 帧窗口实际覆盖了 5 秒,统计的 PERCLOS 不是同一时间尺度上的值。解决:把窗口单位改成时间。做法是维护一个闭眼时间戳列表,只保留最近 3 秒内的闭眼时间段,用sum(闭眼时长) / 3.0计算 PERCLOS。同时打印实时 FPS,如果掉到 15 以下,阈值也要相应调整,否则系统实时性已经不达标了。

5.5 摄像头时间戳混乱导致连续闭眼时长算错

现象:用time.time()逐帧给闭眼状态打时间戳,报警时发现连续闭眼时长忽长忽短,甚至出现 10 秒的离谱值。原因:time.time()返回的是系统墙上时钟,当系统时间被 NTP 校正或虚拟机暂停恢复后,就会出现跳变;另外从摄像头读帧本身也有缓冲延迟,时间戳应该用帧到达时刻而不是处理完成时刻。解决:用cv2.getTickCount() / cv2.getTickFrequency()获取 OpenCV 内部的单调时钟,或者用time.monotonic()取代time.time()。在处理循环里,读帧后立刻打时间戳,再做检测和计算。这样闭眼时长的累计不会受到系统时间跳变的影响。

6. 进阶:用 30 秒自校准把阈值变成每个人的专属参数

固定阈值最大的问题是“别人的脸不是你的脸”。我的习惯是给系统加一个 30 秒的初始化校准阶段,在司机上车后、车辆行驶前完成。校准逻辑是连续采集 750 帧(25FPS × 30 秒),统计出正常睁眼时的 EAR 均值和标准差、正常说话时的 MAR 均值、正常坐姿下的 pitch 均值。然后按以下公式生成个人化阈值:

def calibrate_thresholds(ear_mean, ear_std, mar_mean, pitch_mean): """ 根据 30 秒校准数据生成个人化判定阈值 ear_mean: 正常睁眼时的平均 EAR ear_std: EAR 标准差,用来评估个体稳定性 """ ear_threshold = ear_mean - 1.8 * ear_std # 低于均值 1.8 个标准差视为闭眼 if ear_threshold < 0.15: ear_threshold = 0.15 # 兜底,防止单眼皮人群校准后阈值过低 if ear_threshold > 0.25: ear_threshold = 0.25 # 上限,防止阈值过高误报 mar_threshold = mar_mean + 0.25 # 哈欠阈值在个人正常值基础上加固定偏移 pitch_threshold = pitch_mean + 20 # 低头阈值相对正常坐姿偏移 20 度 return ear_threshold, mar_threshold, pitch_threshold

校准阶段要注意让司机保持自然状态,不能刻意睁大眼或端正坐姿,否则基线失真。把这套校准结果存成 JSON 文件,下次同一司机上车直接加载,省去重复校准。在实际项目里我还会把这些指标输出到 WebSocket 或 MQTT,后台实时看趋势曲线——单次报警是提醒,持续 20 分钟的 PERCLOS 抬高才是真正的疲劳信号。不少源码包只做到“报警”这一步,但如果你要落地到车队监管平台,趋势数据比报警记录更有运营价值。这套方案的工程量不大,最难的不是模型而是工程边界:图像预处理、阈值自校准、掉帧处理、时间戳统一,每一项都是能直接用代码解决的细节。希望这套流程能帮你少走我当初走过的弯路。

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

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

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

立即咨询