
简介这套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.dat68 点关键点回归。后者是一个在 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 ], dtypenp.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) ], dtypenp.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] ], dtypenp.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_threshold0.2, perclos_window60, min_eye_close_time2.0, fps25): 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_window60表示统计 60 帧的窗口在 25FPS 下就是 2.4 秒。窗口太短会让 PERCLOS 波动剧烈窗口太长则反应迟钝建议在 1.5 秒到 3 秒之间调。min_eye_close_time2.0是判定“疲劳性闭眼”的最短持续时间正常人眨眼只持续 100 到 200 毫秒不会超过这个值所以 2 秒能有效过滤正常眨眼。注意这个阈值和 PERCLOS 阈值是两套独立的逻辑前者强调“连续闭眼”后者强调“频繁闭眼”两者同时超过才触发报警比较稳妥。4.2 眨眼频率统计单次闭眼不够频率异常才有意义疲劳的另一个早期信号是眨眼频率降低或异常升高。正常驾驶时人每分钟眨眼 10 到 20 次疲劳时会先减少到 5 次以下或者出现“长时间不眨眼再连续快速眨眼”的模式。实现眨眼次数检测需要在状态机里识别“闭眼-睁眼”的这个变化沿而不是简单地统计闭眼帧数class BlinkCounter: def __init__(self, ear_threshold0.2, min_blink_interval0.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(clipLimit2.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 抬高才是真正的疲劳信号。不少源码包只做到“报警”这一步但如果你要落地到车队监管平台趋势数据比报警记录更有运营价值。这套方案的工程量不大最难的不是模型而是工程边界图像预处理、阈值自校准、掉帧处理、时间戳统一每一项都是能直接用代码解决的细节。希望这套流程能帮你少走我当初走过的弯路。本文还有配套的精品资源点击获取