基于dlib人脸关键点的疲劳驾驶检测系统实现与优化

发布时间:2026/10/6 6:30:01
基于dlib人脸关键点的疲劳驾驶检测系统实现与优化 简介一份基于dlib模型的疲劳驾驶检测系统毕业设计文档面向计算机视觉、图像处理及毕业设计相关学习者。文档完整梳理了从研究背景到系统实现的流程对比生理信息、车辆信息、面部信息三类疲劳检测方法的优劣重点讲解如何利用dlib提取面部68个特征点通过计算长宽比统计眨眼、打哈欠次数结合人体姿态估计检测点头行为并引入PERCLOS指标量化疲劳程度。内容覆盖OpenCV、dlib、人体姿态估计等关键技术以及系统需求分析、架构设计、可行性分析并给出前端采集、图像预处理、特征点检测、疲劳识别与报警提示等模块的完整设计思路。压缩包内共1个docx文档大小约1.13MB目录结构清晰便于按章节查阅与二次开发。已有1955人学习适合需要完整论文参考与算法设计思路的计算机专业学生及开发者。1. 疲劳驾驶检测为什么绕不开dlib一个摄像头加几行代码能做成什么样中午在高速服务区眯了十分钟方向盘报警灯已经亮了好几次。市面上大部分疲劳驾驶报警器的算法核心其实就是“盯眼睛”——把摄像头架在仪表台用dlib模型的人脸关键点定位实时算出眼睛的开合比例闭眼持续超过一定时长就判定疲劳。这个方案不依赖重力感应不用接OBD一张常用的640×480灰度图就能跑起来。对正在做课程设计、毕设或者想快速做一个车载疲劳检测原型的工程师来说dlib模型是入门成本最低的一条路先花两小时把环境跑通再花两天把误报率压下来。本文就按这个路径讲。2. 选型与原理dlib的68点特征定位和EAR/MAR为什么能当疲劳监测主力很多第一次做疲劳检测的人会把“检测疲劳”直接理解成“分类闭眼”然后再去找一个图像分类模型。实际工程里更常见的做法是先做几何测量dlib模型输出人脸的68个关键点眼睛和嘴巴的位置被固定成坐标点再通过这些点的距离比值判断开闭。这一节先讲清楚dlib模型在系统里承担什么角色、EAR和嘴部开合度的数学基础以及为什么传统几何方法在这个场景下比深度学习更划算。2.1 dlib在疲劳检测里的角色人脸检测与68点关键点定位整个系统的输入是普通RGB或灰度图像输出是“疲劳/清醒”标签。中间由两个dlib模型串起来人脸检测器dlib.get_frontal_face_detector()负责在整张图中定位人脸框。内部是HOG特征加线性SVM分类器速度很快CPU上跑640×480的单人脸大概10毫秒上下。它不像深度学习检测器那样能输出大量属性但对固定机位的疲劳检测场景已经够用。关键点预测器dlib.shape_predictor()加载的是dlib官方训练的shape_predictor_68_face_landmarks.dat模型文件。输入是灰度图和人脸框输出68个二维坐标点覆盖下颌轮廓、眉毛、眼睛、鼻子、嘴巴。为什么必须用68点模型而不是单独的眨眼检测器因为疲劳判定需要的是“上下眼皮之间的距离”“嘴角与上下唇中点的距离”这类几何量而且这些量必须稳定、连续。用目标检测框提取眼睛区域框的轻微晃动就会把距离测量直接带偏68点模型是整体回归的单个眼睛点位的预测会参考整张脸的姿态即使侧脸十几度眼睛点依然能落在相对合理的位置。需要特别区分的是这里说的dlib模型不是做人脸识别的那种embedding模型。疲劳驾驶检测系统只用人脸检测器和特征点估计器不输出身份特征。后续所有判定完全围绕这68组坐标点做算术运算。2.2 眼睛纵横比EAR用6个坐标点算出一个开合指数眼睛的开合程度不能直接用“眼皮点之间的距离”判断因为摄像头距离和脸型都会放大或缩小这个距离值。Soukupová和Čech在2016年提出的eye aspect ratioEAR把这个问题解决了对一只眼取6个特征点计算两个垂直距离的平均值再除以水平距离得到一个对尺度不敏感的比值。按照dlib的68点顺序一只眼睛的6个点是点0和点3左右眼角点1、点2上眼皮的中间两个点点4、点5下眼皮的中间两个点。EAR的计算公式是EAR (||P1-P5|| ||P2-P4||) / (2 * ||P0-P3||)睁眼时垂直距离大概是水平距离的0.25到0.35倍闭眼时垂直距离趋近于0EAR掉到0.05甚至更低。因为这是一个比值把摄像头拉远水平和垂直距离同时缩小EAR几乎不变这让我们不需要对每个驾驶员重新校准摄像头距离。实际实现中通常取左右眼的EAR平均值作为当前帧的眼睛开合值。这里有一个容易被忽视的细节EAR对头部姿态敏感。驾驶员低头或者侧身时眼角和眼皮点的投影距离会改变EAR可能出现假性的偏高或偏低。所以正式流程里不能只用单帧EAR要配合连续帧计数和滑动窗口过滤。2.3 嘴部开合度用MAR判断打哈欠打哈欠是疲劳判定的另一个重要特征和眼睛闭合是两条互补信号。实现上可以定义嘴部纵横比Mouth Aspect RatioMAR也可以更简单地取“上下唇中点距离 / 嘴角宽度”。在dlib的68点中左右嘴角分别是点48和点54上嘴唇外沿中点是点51下嘴唇外沿中点是点57。MAR的计算就是MAR ||P51-P57|| / ||P48-P54||正常闭合状态下嘴部垂直距离大约是嘴角宽度的0.15到0.25倍打哈欠时上下唇距离明显拉大这个比值会跳到0.5以上。不过单独看MAR容易误判——说话、微笑瞬间嘴也会张开所以工程上必须加时间条件连续多帧超过阈值才算一次哈欠。比如在30fps帧率下连续15帧MAR大于0.40大约对应0.5秒的张嘴时间才记一次哈欠事件。嘴部检测还有一个好处它和眼睛检测是独立通道在驾驶员戴墨镜或者眼睛被遮挡的场景下至少还能靠打哈欠检测兜底。很多实际项目里这两种信号是加权融合的单看哪一路都有局限。2.4 为什么用dlib而不是YOLO或OpenCV级联一笔实时性账新手经常纠结要不要直接上深度学习。放在疲劳驾驶场景里这个问题要分清楚来看。OpenCV的Haar级联人脸检测器非常快但检测框抖动明显导致提取出的关键点位置噪声很大EAR曲线锯齿感严重很容易误触发阈值YOLO、MTCNN这类深度学习模型检测精度高但通常需要GPU才能达到实时帧率纯CPU推断会吃掉大量核心在一些没有独显的工业主机或嵌入式板子上跑不动dlib的HOGSVM检测器出生在2010年代设计目标就是CPU实时。68点模型体积几十MB推理开销也比深度卷积网络低一个量级单核就能跑。再从部署角度看YOLO通常要搭配PyTorch或TensorRT环境配置复杂度直线上升dlib只需要编译好原生库配合OpenCV和numpy即可运行。疲劳驾驶检测摄像头机位固定人脸在画面中的占比通常比较大dlib的不足极端侧脸、暗光鲁棒性差可以通过摄像头选型和图像预处理弥补这一点第5章会展开。3. 把检测系统跑起来从环境安装到疲劳判定的一整套代码这一章直接给一个可运行的骨架目标是让读者在半天内看到控制台打印出闭眼事件。系统整体流程并不复杂摄像头取帧 → 灰度化 → dlib人脸检测 → dlib关键点预测 → 计算EAR和MAR → 状态机判定 → 输出告警。先跑通流程再聊参数优化。3.1 环境安装dlib、OpenCV、numpy的版本搭配与验证先装依赖。Python 3.8以上都可以但推荐3.9或3.10兼容性最稳。常见做法是把opencv、numpy、imutils先装上pip install opencv-python numpy imutils pip install dlibdlib安装失败的报错多半是“CMake must be installed”或者缺少C编译器。Linux端需要先解决编译链sudo apt-get install build-essential cmakeWindows端则要安装Visual Studio Build Tools里的“使用C的桌面开发”组件再装CMake重新执行pip install。另外也可以用conda直接装预编译包跳过本地编译conda install -c conda-forge dlib模型文件方面需要单独下载shape_predictor_68_face_landmarks.dat放到脚本同级目录。注意dlib的pip包不附带这个模型必须手动下载文件名必须严格匹配。装完之后做一个最小验证import dlib, cv2 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) img cv2.imread(test.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) print(detected faces:, len(faces))这里有三个关键点第一detector的第一个参数必须是灰度图直接传彩色图会在运行时抛异常第二第二个参数0表示不做图像金字塔放大速度快适合近景第三如果faces为空先检查图片里人脸大小是否小于30×30像素dlib对极小脸召回率有限换一张大脸的图验证环境是否正常。3.2 人脸检测与特征点提取每一帧都经过的两个步骤跑通环境之后写主循环。先用最朴素的方式把每一帧的68个关键点拿到手import cv2 import dlib import numpy as np from imutils import face_utils detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 参数0表示不做放大检测近景场景下更快 faces detector(gray, 0) for face in faces: shape predictor(gray, face) landmarks face_utils.shape_to_np(shape) # shape为(68,2) # 打印左右眼中心坐标确认特征点已经落在眼睛区域 left_eye_center landmarks[42:48].mean(axis0) right_eye_center landmarks[36:42].mean(axis0) print(left eye center:, left_eye_center) print(right eye center:, right_eye_center) if cv2.waitKey(1) 27: # Esc键退出 break cap.release()逻辑说明face_utils.shape_to_np是imutils提供的工具函数把dlib返回的shape对象转成numpy数组每一行是(x,y)坐标。landmarks[36:42]是右眼的6个点landmarks[42:48]是左眼的6个点。用切片时要注意顺序36:42切出来是索引36到4142:48切出来是索引42到47。这里最常见的坑是如果摄像头光轴和驾驶员视线不平行左右眼切片的顺序不能想当然换位。代码里36:42和42:48对应的是模型输出的固定顺序不是人的左右手方向。打印眼中心坐标就是用来验证这一步的中心位置如果落在颧骨上说明切片索引用错了。3.3 EAR/MAR计算与疲劳判定逻辑闭眼、哈欠、状态切换拿到关键点之后下一步就是计算EAR和嘴部开合度。from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # eye是6个点顺序外眼角、上眼皮1、上眼皮2、内眼角、下眼皮1、下眼皮2 A dist.euclidean(eye[1], eye[5]) B dist.euclidean(eye[2], eye[4]) C dist.euclidean(eye[0], eye[3]) return (A B) / (2.0 * C) def mouth_ratio(landmarks): # 48和54是嘴角51是上唇中点57是下唇中点 v dist.euclidean(landmarks[51], landmarks[57]) h dist.euclidean(landmarks[48], landmarks[54]) return v / h逻辑说明eye_aspect_ratio接收的eye参数是六个点的坐标数组A和B分别是两对上下眼皮点的垂直距离C是左右眼角的水平距离。EAR公式把垂直距离和水平距离放在比值里所以摄像头距离变化不会导致这个数值大幅改变。mouth_ratio更直接用上下嘴唇中点的距离除以嘴角宽度。注意这里的竖直线是计算51和57两个点之间的欧氏距离不是单纯取y坐标差因为人脸在画面中可能有轻微旋转欧氏距离更能反映真实开合程度。疲劳判定用“连续帧计数”而不是只看单帧这个设计很关键。EAR_THRESH 0.22 CLOSED_FRAMES 5 MAR_THRESH 0.40 YAWN_FRAMES 15 ear_closed_count 0 yawn_count 0 closed_events 0 while True: # ... 取帧、检测、得到landmarks ... if len(faces) 0: # 没有人脸时不计数避免把黑屏当闭眼 ear_closed_count 0 yawn_count 0 continue ear (eye_aspect_ratio(landmarks[36:42]) eye_aspect_ratio(landmarks[42:48])) / 2.0 mar mouth_ratio(landmarks) if ear EAR_THRESH: ear_closed_count 1 else: if ear_closed_count CLOSED_FRAMES: closed_events 1 print(闭眼事件持续帧数:, ear_closed_count) ear_closed_count 0 if mar MAR_THRESH: yawn_count 1 else: if yawn_count YAWN_FRAMES: print(哈欠事件持续帧数:, yawn_count) yawn_count 0参数说明EAR_THRESH设置成0.22是大部分人的睁眼EAR基线0.25到0.35的下沿。CLOSED_FRAMES设置为5表示连续5帧低于阈值才判定为一次闭眼正常眨眼的持续时间在6到9帧左右所以5帧这个值能过滤掉大部分瞬时噪声。MAR阈值0.40和YAWN_FRAMES 15的组合对应大约0.5秒的张嘴时间。这里有一个边沿触发的细节closed_events只在“持续低于阈值结束后”递增也就是说状态从“低于阈值”恢复到“高于阈值”时才记录一次完整闭眼事件。好处是驾驶员闭眼5秒时不会每帧都触发告警告警终端不会被刷屏。3.4 单线程还是多线程控制实时性的主循环写法上面的主循环是单线程的检测、关键点提取、距离计算全挤在同一个循环里。在一台普通的4核CPU笔记本上这个流程大概能跑12到15帧每秒看起来像慢动作。常见做法是把摄像头读取放到独立线程主循环只处理最新一帧减少I/O等待带来的掉帧。import threading import queue frame_q queue.Queue(maxsize2) def grab_frames(cap): while True: ret, frame cap.read() if ret: if frame_q.full(): # 队列满了就丢旧帧保留最新帧避免延迟累积 try: frame_q.get_nowait() except queue.Empty: pass frame_q.put(frame) threading.Thread(targetgrab_frames, args(cap,), daemonTrue).start()逻辑说明frame_q的maxsize设置成2目的是让生产者与消费者解耦。当主循环处理速度慢于摄像头帧率时队列会自动丢弃旧帧保证主循环拿到的始终是最新的画面。很多新手会用list当缓冲区结果无人消费时list无限增长画面越来越旧疲劳判定滞后好几秒。使用方式是在主循环从队列取帧while True: if frame_q.empty(): continue frame frame_q.get() # ... 继续做检测和判定 ...常见做法是先在单线程模式下跑通逻辑再改成线程模型提升帧率。OpenCV的VideoCapture在某些USB摄像头上有线程安全问题直接把read放在线程里有时会阻塞如果遇到这种问题改用RTSP拉流或者调低帧率再试。4. 参数调整与系统集成让检测结果真正能触发告警很多项目能算出EAR但一上车就误报不断。原因多半是参数是拍脑袋定的EAR阈值0.2、连续闭眼3帧、哈欠阈值0.5这些数字在实验室屏幕上看起来合理上车后立刻翻车。这一章把参数之间的关系讲清楚并给一个能落地的告警与日志方案。4.1 EAR阈值、连续闭眼帧数与PERCLOS窗口的联动关系先看一张参数表这是比较通用的初始值参数常见范围一个可用初值参数行为EAR_THRESH0.150.300.22越低越难触发越高越灵敏CLOSED_FRAMES310帧5帧决定“持续闭眼多久算一次闭眼事件”MAR_THRESH0.300.550.40嘴部开合比值阈值YAWN_FRAMES1030帧15帧决定“张嘴多久算一次哈欠”PERCLOS_WINDOW30120秒60秒统计窗口内闭眼时间占比这些参数不是独立的。EAR阈值调低之后就不需要过高的连续帧数来防误报因为低阈值本身已经过滤了噪声导致的短暂下穿。反过来如果EAR阈值定得宽松比如0.25连续帧数就要加大到6到8帧否则正常眨眼瞬间就会触发。调整顺序建议是先定EAR阈值再看误报率调连续帧数最后用PERCLOS做最终告警判断。PERCLOS全称是眨眼闭合度衡量的是单位时间内眼睑闭合时间的占比。它的优势是能平滑瞬时抖动驾驶员闭眼1秒后又睁眼连续N帧法可能把这1秒计为一次事件但PERCLOS会把这个闭眼时间累积进统计窗口更适合长时间驾驶场景。实现PERCLOS可以维护一个长度为帧率×窗口秒数的环形数组记录每一帧是否闭眼窗口内闭眼帧数除以总帧数就是当前PERCLOS值。一个可用的经验阈值是0.4也就是一分钟内闭眼时间超过24秒判定疲劳。不同国家有不同行业标准但0.4作为粗线下限在公开文献里比较常见。4.2 摄像头调度分辨率、帧率、曝光与硬件选型建议摄像头这一环看起来和技术无关实际对检测效果的影响比参数更大。分辨率720p看起来更清晰但dlib的关键点预测器不是线性受益于分辨率。超过640×480后计算量明显上升而关键点的像素精度提升有限。车载场景建议用640×480再低也不要低于320×240。帧率20fps以上才能稳定捕捉睁眼到闭眼的连续过程。低于15fps时一次正常眨眼可能只有两三帧被采到连续帧计数很难工作。曝光过曝会让眼睛区域变成一片亮白关键点会被预测到眼眶边缘甚至镜框上。不要指望HOG检测器对过曝图像有强鲁棒性。红外摄像头夜间行驶时普通USB摄像头几乎没有可用画面红外摄像头是疲劳驾驶检测的硬件标配。dlib的检测器和关键点预测器在红外灰度图上仍然能正常工作这一点经常被忽略。调试时有个高效技巧先用手机前置摄像头在预计安装位置拍一张照片离线用dlib跑一次确认眼睛和嘴部的点都落在正确位置上再上摄像头联调。很多现场问题都能通过这个离线测试提前暴露。4.3 告警与日志声音提示、状态叠加与CSV落盘告警不能做成单次闭眼就响否则驾驶员偶尔揉眼睛、戴眼镜调整都会被误报。工程上常用的做法是“事件加时间窗口累计”双重确认。import time event_ts [] # 每次闭眼事件发生时执行 event_ts.append(time.time()) # 只保留最近60秒的事件 event_ts [t for t in event_ts if time.time() - t 60] if len(event_ts) 4: # 触发声光告警 import winsound winsound.Beep(1200, 300) # 蜂鸣一声逻辑说明event_ts列表保存每次闭眼事件的时间戳每次有新事件时就清理掉超过60秒的旧事件。如果一分钟内闭眼事件达到4次就触发告警。这里用时间戳而不是帧数做统计好处是帧率变化不影响结果。想更严谨一点可以给不同事件加权比如闭眼一次权重1哈欠一次权重2窗口内总权重超过5才告警。这样既能容忍偶发眨眼又能融合多种疲劳信号。叠加显示方面OpenCV的putText不支持中文要显示指标就用英文标签或PIL渲染中文字体。一个简短的显示代码cv2.putText(frame, fEAR:{ear:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.putText(frame, fMAR:{mar:.2f}, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) if fatigue_warn: cv2.putText(frame, DROWSY!, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 3)参数说明putText的坐标是画面左上角起点的像素位置。EAR显示在左上角比较不干扰视线DROWSY用红色显示。如果你要把画面存证作为事后证据文字区域的背景可以再垫一个黑色矩形提高可读性。CSV日志建议记录帧序号、时间戳、EAR、MAR、人脸框坐标、状态标记。不用每帧都写盘每10帧批量写一次就够了否则频繁IO会掉帧。日志是后面调优和验证的基础没有日志第6章的准确率评估无从谈起。5. 避坑排查dlib疲劳驾驶检测最常见的5个翻车现场这章写的是实际项目中反复出现的坑每一条都按“现象 → 原因 → 解决”来讲很多问题不是代码逻辑错而是环境、硬件和使用方式与dlib模型的假设不匹配。5.1 现象pip install dlib编译失败报CMake或编译器错误很多人在Windows上跑pip install dlib直接看到一片红字“Cmake must be installed”或者“error: command gcc failed”然后就开始怀疑人生。原因dlib是一个C编写的库pip在找不到匹配wheel的时候会走源码编译。Windows下没有Visual Studio的C编译环境Linux下没有build-essential编译必然失败。解决Windows先装VS Build Tools勾选“使用C的桌面开发”然后再装CMake最后重新pip install。如果还不想装VS就用conda跑conda install -c conda-forge dlib它会直接拉编译好的二进制包。另外注意Python版本过于新的Python版本缺少官方wheel时也会触发编译建议先用3.9或3.10。5.2 现象戴眼镜的驾驶员睁眼时EAR突然掉到0.05以下明明眼睛睁着系统却判定闭眼仪表台上的报警灯一直闪。戴眼镜、尤其是镜片反光时最容易出现。原因镜片反光导致眼睛区域出现高光串扰关键点预测器把上眼皮的点拉向了镜框边缘垂直距离被压缩EAR掉到和闭眼一样的水平。这不是阈值问题是特征点本身漂移。解决先在图像进入dlib之前做一次CLAHE自适应直方图均衡化可以稍微缓解镜片反光造成的高低光不均。更好用的办法是在标定阶段就戴着同一副眼镜采集数据让EAR基线本身包含镜片影响这样睁眼和闭眼的EAR差依然能区分。不要在镜片反光明显时盲目调整阈值那只会让真正的闭眼漏检。5.3 现象CPU占用拉满帧率不到15fps笔记本风扇狂转画面像PPT这时候要么硬件太弱要么代码里有明显性能浪费。原因最常见的是每一帧都调用detector(gray, 1)这个参数1表示先放大图像再检测计算量翻好几倍。另外有人把人脸检测直接放在最高分辨率帧上比如1080pHOG检测器在这个尺寸下非常吃力。解决把摄像头分辨率压到640×480detector(gray, 0)不放大。还有一个有效手段每隔3帧才跑一次人脸检测检测结果出来之前用上一帧的人脸框直接传给关键点预测器。因为人脸在驾驶场景中移动不会特别快3帧内头部位移很小关键点依然可用。这样可以把人脸检测的开销降到原来的三分之一。5.4 现象驾驶员正常眨眼每一次都触发疲劳告警闭眼事件和哈欠事件频繁打印把告警窗口塞满真正疲劳时反而被淹没。原因一是CLOSED_FRAMES设得太小比如3帧。在30fps下3帧只有100毫秒而正常眨眼大约持续200到300毫秒也就是6到9帧。二是闭眼事件处理采用“每帧计数、每帧可能告警”的逻辑没有做事件去重。解决把CLOSED_FRAMES调到5以上并且只在一个连续闭眼片段结束时登记一次事件。更稳妥的做法是不用“连续帧数”作为唯一判据改用PERCLOS统计一整个时间窗口内的闭眼时间占比这样正常眨眼会被平滑掉而持续疲劳积累会被放大。5.5 现象逆光、暗光或夜间场景下人脸检测直接漏检白天逆光或者傍晚暗光下faces返回空列表后续所有判定全被跳过相当于系统失灵。原因dlib的HOG检测器在灰度图上工作如果人脸区域过暗或者过曝梯度特征被破坏检测器认不出人脸。这个不是参数能救回来的是成像质量的问题。解决软件层面可以先做伽马校正或CLAHE提升暗部对比度后再进dlib。但夜间没有光源时任何图像增强都是有限的。工程上更可靠的方案是换红外摄像头或者加红外补光灯这样白天晚上都能稳定出图。把这一点写进需求文档比在软件里死磕更有价值。6. 进阶技巧用眨眼时长统计和PERCLOS把误报率再压一半前面几章讲的是能跑起来的系统这一章讲怎么把误报率真正压下来。我的经验是单看EAR阈值和连续帧数只能做到“能用但不稳定”真正让准确率上一个台阶的是切换到时间域统计。先从眨眼时长说起。正常眨眼是200到300毫秒疲劳时的闭眼往往持续1秒以上。与其用“连续帧数”当阈值不如记录每次闭眼的开始时间和结束时间得出真实的闭眼时长然后对闭眼时长做分布统计。实现上只需要在闭眼事件发生时记录time.time()在睁眼恢复时计算差值就能画出一条眨眼时长曲线。PERCLOS是对这段时域特征的进一步整合。做法是维护一个60秒的滑动窗口统计窗口内闭眼时间占总时间的百分比。下面是一个最小实现import time from collections import deque eye_close_window deque() perclos_window_sec 60 while True: # ear已计算closed_flag为True表示当前帧闭眼 if closed_flag: eye_close_window.append((time.time(), True)) # 清理窗口外的记录 while eye_close_window and eye_close_window[0][0] time.time() - perclos_window_sec: eye_close_window.popleft() closed_frames sum(1 for _, v in eye_close_window if v) perclos closed_frames / len(eye_close_window) if eye_close_window else 0.0 if perclos 0.4: alert()代码里用deque保存闭眼帧的时间戳每帧清理一次过期的记录然后统计窗口内闭眼帧占比。这样比数组快时间复杂度是均摊O(1)。更有效的一步是给每名驾驶员做个人基线标定。让驾驶员在系统启动后保持睁眼3秒、闭眼3秒各取EAR平均值然后把个人阈值设为睁眼基线和闭眼基线的中点。第一次做这个功能时我还有点怀疑后来发现同事里有人EAR基线只有0.18固定阈值0.2会导致他一直被误报。做完个人标定后误报率降了一半都不止。验证方法也很重要。录一段5分钟视频人工标注哪些时间段是疲劳状态然后离线回放检测脚本对比识别结果和标注结果算出准确率和误报率。没有这个评估流程参数调优就是玄学。最后说一个教训我之前把阈值写死到0.2结果在车里实测时误报到同事直接把系统关了。后来改成先标定、再做PERCLOS统计系统才真正能让人放心用。疲劳驾驶检测这个方向dlib只是起点真正有价值的工程能力是懂得如何用时间和统计维度来对冲单帧图像的不确定性。希望帮到你。本文还有配套的精品资源点击获取