☰
疲劳驾驶检测实战:Python+OpenCV从人脸关键点到PERCLOS预警
2026/9/28 3:26:08 网站建设 项目流程

简介:一套基于驾驶员面部特征的疲劳检测Python源码,面向自动驾驶辅助、长途运输及安全监控领域的开发者与研究者。系统通过摄像头实时捕捉面部图像,分析眼睛闭合频率、眨眼模式等特征,结合OpenCV实现面部与眼部定位,并内置预警机制,适用于车载、公共交通及货运车辆监控等场景,可有效提醒驾驶员适时休息。压缩包内含22个文件,以Python脚本和XML配置文件为主,另有项目结构文件、IDE工程配置、README说明文档及提示音频,总大小约68MB。目前已有93人学习下载。源码结构清晰,模块划分明确,覆盖面部检测、眼睛区域定位、疲劳特征分析和预警响应等完整流程,可直接运行或二次开发,同时保留了版本管理文件,便于工程化使用。对理解计算机视觉在疲劳驾驶检测中的落地实现有较高参考价值,也适合作为相关课程与毕业设计的实践参考。

1. 疲劳检测不是玄学:用Python和摄像头把「困意」变成可量化的数字

开长途车最怕的不是路况,而是自己眼皮开始打架的那几分钟。很多团队想做一个基于驾驶员面部特征的疲劳检测系统,第一反应就是上深度学习模型,训练一个能识别"困不困"的分类器。但实际做下来你会发现,真正稳定可靠的方案,反而是用传统图像处理加人脸关键点,把眼睛开合程度、嘴巴张合频率、头部点头幅度这些物理量算出来,再按PERCLOS标准去判定疲劳等级。这套逻辑不算新,但想把它写成一套能跑的Python源码,从摄像头采集到报警输出,中间要过的坎不少。本文就按一个可复现的源码包来拆:拿到手怎么跑通、每个参数代表什么、哪些地方一不留神就翻车,以及怎样改造成你自己的版本。适合正在做课程设计、毕设,或者准备在公司内部做驾驶员状态监控原型的工程师。

2. 拿到源码先别跑:项目结构、依赖安装与最小复现

2.1 源码包里到底有什么:从目录看懂一条检测流水线

一个标准的疲劳检测Python源码包,通常不是单个脚本,而是一组按职责拆开的文件。打开.rar解压后,我一般会先看目录结构,而不是急着执行python main.py。常见的结构大致长这样:

driver-fatigue-detection/ ├── main.py # 程序入口,负责启动摄像头和UI ├── config.yaml # 所有阈值和参数的集中配置 ├── requirements.txt # 第三方依赖列表 ├── utils/ │ ├── __init__.py │ ├── face_detector.py # 人脸检测封装 │ ├── landmarks.py # 关键点提取 │ ├── metrics.py # EAR/MAR/头部姿态计算 │ └── alarm.py # 报警逻辑 ├── models/ # 放dlib模型或onnx模型 │ ├── shape_predictor_68_face_landmarks.dat │ └── face_detector.dat ├── ui/ │ └── dashboard.py # 实时画面与告警展示 └── tests/ └── test_metrics.py

这个结构的意义在于,检测与决策分离。metrics.py只负责算物理量,config.yaml负责告诉你哪些数字算疲劳,alarm.py负责在疲劳时触发声音或界面闪烁。你改阈值时不需要去翻代码,改config.yaml即可。如果拿到的包把所有逻辑都塞进一个main.py里,也不算错,但维护性会差很多,后面调参时会很痛苦。

models/目录里的shape_predictor_68_face_landmarks.dat是dlib的经典模型,约99MB,能输出人脸68个关键点。如果包里没有这个文件,通常是因为体积太大被单独放下载链接了,记得先补上。另外有些现代源码会改用MediaPipe或OpenCV自带的FaceDetectorYN,模型文件小一个数量级,但关键点数量可能只有468点或6点,后续算法要跟着改。先确认你手里的是哪种,再看下面的依赖安装。

2.2 用conda搭一个不污染环境的Python运行环境

依赖管理是复现这类项目最容易翻车的地方。dlib需要CMake编译,Python版本不对直接报错;MediaPipe的版本又和numpy有兼容性坑。我习惯用conda新建一个独立环境,而不是直接装到base里,这样项目之间互相不污染。

conda create -n fatigue python=3.8 conda activate fatigue pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

requirements.txt里通常会包含这些核心依赖:

opencv-python==4.8.0.76 dlib==19.24.2 numpy==1.24.3 PyYAML==6.0 pyttsx3==2.90

为什么不直接pip install opencv-python?因为新版OpenCV和旧版numpy之间有ABI冲突,dlib编译时也会因为Python版本不同产生一堆C++错误。锁版本号能省掉大量玄学问题。pyttsx3用于语音播报“请注意休息”,如果你的系统是macOS,它底层走的是nsss,有时候会没声音,后面避坑章会提到。

装完后跑一句验证导入:

python -c "import cv2, dlib, yaml; print('deps ok')"

看到deps ok说明环境没问题。如果你用VSCode写代码,记得在右下角重新选择解释器,选到fatigue这个conda环境,否则import dlib会报ModuleNotFoundError。很多新手在这里卡半小时,其实是解释器没切换。

2.3 跑通最小demo:一张静态图验证人脸关键点

不要一上来就连摄像头。先用一张包含正脸的照片,验证人脸检测和关键点提取链路是否通。写一个小脚本test_landmarks.py:

import cv2 import dlib detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat") img = cv2.imread("test_face.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 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(img, (x, y), 2, (0, 255, 0), -1) cv2.imwrite("output_landmarks.jpg", img) print(f"detected {len(faces)} face(s)")

这段代码的逻辑很直接:get_frontal_face_detector用方向梯度直方图做人脸检测,返回人脸矩形框;shape_predictor把68个关键点坐标填出来;最后把每个点画成绿色小圆点。detector(gray, 1)里的1表示在图像金字塔里上采样一次,对远距离小脸明显更友好,但会慢不少。如果你要检测近处司机,保持默认0就行。

跑完后打开output_landmarks.jpg,如果68个点能贴合眉毛、眼睛、嘴巴和下颌轮廓,说明模型加载正常。如果点乱飞,或者直接把背景当成人脸,多半是模型文件损坏或输入图像太小。此时不要急着进实时阶段,先换一张更高清的正脸照试,保证人脸宽度至少占图像宽度的四分之一。

3. 核心算法拆解:从面部关键点到疲劳分数

3.1 人脸关键点检测:dlib与MediaPipe的取舍

疲劳检测的上游是人脸关键点,因为后面所有指标都建立在眼睛和嘴巴的坐标上。当前开源领域两条主流路线:dlib的68点方案和Google MediaPipe的468点方案。dlib的模型大、速度中等,但离线运行稳定,不依赖网络,且对CPU优化成熟;MediaPipe模型小、速度快,但关键点坐标定义不同,某些点(比如眼皮中点)需要从网格坐标里推算,代码会绕一点。

从工程角度,我推荐先用dlib打底,理由有三个:第一,68点语义明确,第36到第42点是右眼,第43到第48点是左眼,第49到第68点是嘴巴和下颌,喂给EAR公式直接能用;第二,dlib的模型文件是纯文件,不需要额外运行时;第三,网上关于dlib的眼部检测教程最多,遇到问题容易搜到解决方案。MediaPipe的优势在移动端部署,或者你需要同时追踪头部的六自由度姿态时,它有现成的头部位姿估计算法,但那是另一套API了。

选择关键点是第一步,真正影响判定结果的是接下来怎么用这些点。下面的EAR和MAR都是纯几何计算,不涉及神经网络,因此可解释性很强,也方便你根据实际驾驶室环境调参。这也是为什么这套方案在工业落地时仍然被广泛采用——你不希望司机被警察拦下时,你解释不清系统为什么报警。

3.2 眼睛纵横比EAR:困意的最直接物理量

PERCLOS标准(单位时间内眼睛闭合时间占比)是疲劳检测的行业基础,而计算PERCLOS的前提是判断每一帧眼睛是否闭合。最常用的指标是眼睛纵横比EAR(Eye Aspect Ratio),由6个关键点算出。右眼取第37到第42点,左眼取第43到第48点,公式为:

EAR = (|P2 - P6| + |P3 - P5|) / (2 * |P1 - P4|)

其中P1到P6是眼睛轮廓点,按顺序从外眼角到内眼角排列。实现代码大致如下:

def eye_aspect_ratio(eye_points): """ 计算单只眼睛的纵横比。 eye_points: 6个(x, y)坐标,顺序符合dlib的68点定义 """ p1, p2, p3, p4, p5, p6 = eye_points dist_vertical_1 = euclidean_distance(p2, p6) dist_vertical_2 = euclidean_distance(p3, p5) dist_horizontal = euclidean_distance(p1, p4) ear = (dist_vertical_1 + dist_vertical_2) / (2.0 * dist_horizontal) return ear

这里用到的euclidean_distance可以用numpy.linalg.norm直接实现:

import numpy as np def euclidean_distance(a, b): return np.linalg.norm(np.array(a) - np.array(b))

逻辑说明:人眼正常睁开时,上下眼皮的距离(垂直距离)相对稳定,EAR值大约在0.25到0.35之间;当眼皮开始闭合,垂直距离迅速缩小,EAR值会掉到0.15以下。横向距离基本不变,所以EAR能很好反映开合程度。代码里除以2.0是取平均的归一化处理,防止左右眼大小不一致带来偏差。实际测量时,两只眼睛的EAR需要分别计算后取平均,作为当前帧的综合眼部开合度。

参数说明:EAR阈值通常设置成0.2到0.25。如果设0.2,系统会更迟钝,只在几乎闭眼时才报警;设0.25会更灵敏,但可能把正常眯眼也算进去。建议先在驾驶模拟场景下采集一段视频,画出EAR的曲线,再根据曲线低谷值来定阈值,而不是拍脑袋。持续帧数一般设为2到3帧,因为单帧的EAR波动可能是眨眼造成的,眨眼时间通常不超过400毫秒,而疲劳导致的闭眼会持续更久。后面第4章会讲怎么用持续帧数过滤短暂眨眼。

3.3 嘴巴与头部姿态:打哈欠和点头怎么算

单看眼睛还不够,有些疲劳状态是频繁打哈欠或无意识点头。嘴巴开合度可以用嘴巴纵横比MAR(Mouth Aspect Ratio)来量化。取dlib的第61到第68点作为嘴巴外轮廓,其中第61和67点分别是左右嘴角,第63和65点分别对应上嘴唇和下嘴唇的纵向点。代码与EAR类似:

def mouth_aspect_ratio(mouth_points): # mouth_points: 按dlib顺序取出的嘴巴轮廓点 dist_horizontal = euclidean_distance(mouth_points[0], mouth_points[6]) # 嘴角距 dist_vertical_1 = euclidean_distance(mouth_points[2], mouth_points[10]) # 上唇到下颌 dist_vertical_2 = euclidean_distance(mouth_points[4], mouth_points[8]) mar = (dist_vertical_1 + dist_vertical_2) / (2.0 * dist_horizontal) return mar

正常说话时MAR在0.2到0.4之间,打哈欠时会超过0.5,且会持续1到3秒。疲劳判定的逻辑通常是:在30秒窗口内,如果MAR大于0.5的帧数占窗口总帧数的比例大于某个阈值,比如15%,判定为一次哈欠事件。注意,说话时嘴巴也会张大,所以需要配合语音或单纯的时间连续性来进一步区分。简单做法是要求高MAR状态连续维持超过1秒,才认为是哈欠,因为说话时嘴巴会频繁闭合。

头部低头或点头动作,可以用关键点之间的几何关系估算。最粗粒度的方法是取鼻子尖(第30点)和左右耳垂(第2点和第14点)之间的角度变化,但这需要正脸角度相对稳定。更可靠的做法是使用OpenCV的solvePnP结合一个标准3D人脸模型,得到三个旋转向量(pitch, yaw, roll)。pitch角向下增大,就是点头;持续低头超过3秒,说明司机可能已经睡着。这个计算量适中,但需要相机内参,实际装车时要先做标定。如果源码包里没有标定环节,那你只能靠外眼角连线和下巴点之间的相对距离来近似低头,精度会差不少。

3.4 PERCLOS与综合评分:把单帧指标变成时间窗口结论

单帧的EAR也好,MAR也好,都只是瞬时值,直接拿来做报警会误报不断。工程上必须引入时间窗口。最经典的指标是PERCLOS,定义为在某段时间内眼睛闭合帧数占总帧数的比例。例如在60秒时间内,摄像头采集了1500帧,其中EAR低于阈值的帧有375帧,那PERCLOS就是25%。研究普遍认为,PERCLOS超过30%就该判定为疲劳驾驶。

实现上不需要真的存一整个窗口的帧布尔值,用一个滑动窗口队列即可:

from collections import deque class FatigueAnalyzer: def __init__(self, window_size=300, ear_threshold=0.2, closed_ratio_threshold=0.3): self.eye_closed_history = deque(maxlen=window_size) self.ear_threshold = ear_threshold self.closed_ratio_threshold = closed_ratio_threshold def update(self, ear): is_closed = 1 if ear < self.ear_threshold else 0 self.eye_closed_history.append(is_closed) if len(self.eye_closed_history) < self.window_size: return 0.0 perclos = sum(self.eye_closed_history) / len(self.eye_closed_history) return perclos def is_fatigue(self, ear): perclos = self.update(ear) return perclos >= self.closed_ratio_threshold, perclos

这里的deque(maxlen=300)假设视频帧率是25FPS,300帧对应12秒窗口。closed_ratio_threshold设0.3,表示在最近12秒里眼睛闭合时间超过30%就提示疲劳。这个窗口长度可以调:窗口越短,反应越快,但误报越多;窗口越长,越平滑,但发现疲劳更晚。实际驾驶场景建议15秒到30秒窗口,因为真正疲劳时的闭眼是持续性的,给它时间积累反而更稳定。

把EAR、MAR和头部姿态三者加权得到综合评分,也是源码包里常见做法。权重需要根据场景定:高速公路上疲劳主要体现在眼睛闭合,市区拥堵路段哈欠和低头更多。一个粗略的评分规则是:PERCLOS占60%,哈欠频率占30%,低头时长占10%。这部分建议在config.yaml里配成可调节的权重,方便不同场景切换。

4. 把检测变成实时监控:摄像头采集、线程与界面

4.1 用OpenCV接管摄像头:帧率与分辨率的平衡

疲劳检测要看到眼睛细节,但分辨率不是越高越好。摄像头输出1920x1080时,dlib人脸检测在CPU上可能要跑到100毫秒一帧,加上关键点检测,整体帧率跌到5FPS以下,这时候PERCLOS统计就失去意义了。我一般会把视频流先缩放到640x480,甚至可以降到480x360,只要人脸区域在画面中宽度超过100像素,68个关键点就够用。

OpenCV读摄像头并缩放的主流写法:

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 # 如果摄像头不支持当前分辨率,用resize强制缩放 if frame.shape[0] != 480 or frame.shape[1] != 640: frame = cv2.resize(frame, (640, 480)) # 检测、计算、展示逻辑

参数说明:CAP_PROP_FRAME_WIDTH和CAP_PROP_FRAME_HEIGHT设置的是摄像头驱动层输出的分辨率,但很多USB摄像头只会选择最接近的等比分辨率,比如1280x720,所以读取后要再校验一次frame.shape。用resize强制缩放会造成每帧多一重拷贝,但对实时性影响不大。帧率设为30是给后续检测预留余量,实际检测速度达不到30FPS也没关系,滑动窗口按实际到达帧数计算即可。

这里要提醒一点,不要在while循环里同时做人脸检测、关键点提取和UI绘制,否则每帧总耗时是三者累加,很容易卡顿。正确做法是把检测放到独立线程里,主线程只负责显示和接收结果。下一步就来说线程结构。

4.2 多线程与队列:别让UI卡住检测

单线程跑实时检测时,只要一次检测耗时超过100毫秒,画面就会出现明显撕裂和延迟。更糟的是,UI的按钮响应也会被拖死,用户想改个阈值还要等检测循环结束。常见做法是生产者消费者模型:摄像头线程负责读帧和检测,把结果(帧、关键点、EAR值)放入队列;UI线程只从队列取最新结果绘制。

import threading import queue frame_queue = queue.Queue(maxsize=2) result_queue = queue.Queue(maxsize=2) def capture_and_detect(cap, detector, predictor, analyzer): while running: ret, frame = cap.read() if not ret: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) ear = 0.0 mar = 0.0 if len(faces) > 0: # 取最大的人脸,假设离摄像头最近的是司机 face = max(faces, key=lambda f: f.width() * f.height()) landmarks = predictor(gray, face) # 提取左右眼关键点坐标并计算EAR left_eye, right_eye = extract_eye_points(landmarks) ear = (eye_aspect_ratio(left_eye) + eye_aspect_ratio(right_eye)) / 2.0 mar = mouth_aspect_ratio(extract_mouth_points(landmarks)) perclos = analyzer.update(ear) frame_queue.put(frame) result_queue.put((ear, mar, perclos))

关键逻辑说明:queue.Queue(maxsize=2)限制了队列长度,当帧处理速度跟不上时,新的结果会挤掉旧的,保证UI拿到的永远是最新帧。这个drop策略比无界队列更合理,无界队列在检测慢时会积压几百帧,内存和延迟都失控。maxsize=2意味着最多积压1帧新的,旧帧被丢弃,UI画面和实时检测的偏差始终控制在两帧以内。

参数说明:如果检测线程输出结果的速度是15FPS,UI线程绘制能力是60FPS,那么UI会重复绘制同一帧,画面看起来依然是15FPS,这是正常的。不要试图让UI线程等检测线程,那样又会回到单线程卡顿。另外,running标志位用threading.Event来控制退出,不然关闭窗口时线程会挂在cap.read()上。

4.3 报警阈值怎么调:EAR阈值、持续帧数与灵敏度

源码包里最需要用户自己调的三个参数,就是EAR阈值、持续帧数、PERCLOS窗口。它们在config.yaml里通常是这样:

fatigue: ear_threshold: 0.22 # EAR低于此值视为闭眼 eye_close_frames: 3 # 连续闭眼帧数超过此值触发预报警 perclos_window_sec: 15 # PERCLOS统计窗口,单位秒 perclos_threshold: 0.3 # 窗口内闭眼帧占比超过30%报警 mar_threshold: 0.55 # 嘴巴张合比,打哈欠阈值 yawn_frames: 20 # 持续20帧(约1秒)视为一次哈欠 head_low_pitch_deg: 25 # 低头角度阈值 head_low_frames: 45 # 低头持续45帧(约2秒)报警

这些数字不是拍脑袋来的。EAR阈值要看你用的摄像头安装高度和角度:摄像头装在方向盘前方,人眼和镜头几乎水平,EAR基线大约0.3;如果摄像头装在仪表盘上俯拍人眼,EAR基线会低到0.25左右。所以我建议每个目录先做一次30秒标定:让被测司机正常平视前方,统计EAR平均值,然后设定阈值为平均值的70%到75%。比如平均EAR是0.28,那阈值设在0.2左右。

持续帧数与帧率强相关。如果实际检测帧率是15FPS,那么eye_close_frames设为3帧,表示眼睛连续闭合200毫秒;如果帧率是25FPS,3帧只有120毫秒。眨眼时长通常在100到150毫秒,为了不把眨眼当闭眼,持续帧数对应的实际时间至少要大于200毫秒。我的经验是:无论帧率多少,先换算时间,再反推帧数。上面配置里head_low_frames: 45对应2秒,也是同样的思路。

报警灵敏度还有一个隐藏变量:摄像头曝光。夜间或隧道内,摄像头为了补偿低光会降低曝光时间,帧率从30掉到10,但每帧噪声变大,EAR值抖动也变大。此时建议把eye_close_frames从3提高到4,不然噪点会导致单帧EAR跌破阈值,误报率直线上升。这是疲劳系统在实车上最需要根据环境动态调整的地方。

5. 避坑指南:疲劳检测系统最常见的5个翻车现场

5.1 戴眼镜时眼睛关键点乱跳

现象:司机戴着镜框眼镜或墨镜,检测时68个点中的眼睛轮廓点会频繁上下跳动,EAR值在0.15到0.4之间剧烈波动,系统频繁触发报警。

原因:dlib的68点模型是在无遮挡人脸上训练的,镜框会把人眼轮廓的上半部分挡住,使得模型把镜框上沿误判成眼皮。特别是深色镜框,梯度信息强于真实眼皮,坐标直接被“吸引”过去。

解决:优先换用MediaPipe的面部网格模型,它对眼镜遮挡鲁棒性稍好;其次是在关键点提取前做图像增强,例如用cv2.equalizeHist对灰度图做直方图均衡,提高眼区对比度,让真实眼皮边缘更突出。如果这两招都不行,就把EAR的垂直距离计算点从6个点缩减成上下眼皮各取一个更靠近瞳孔的点,同时把EAR阈值调低到0.15,宁可漏报一些轻微疲劳,也不能让正常睁眼被误判。

5.2 车内光线不足,全程误报疲劳

现象:傍晚或夜间开车,画面整体很暗,面部关键点置信度下降,EAR普遍偏低,系统把清醒状态判成闭眼。

原因:摄像头自动增益把图像提亮的同时,噪声也跟着放大,关键点的亚像素精度下降;而且暗光下人眼瞳孔边缘模糊,模型提取的眼皮点会更靠近瞳孔中心,导致EAR从0.28掉到0.2。

解决:先检查摄像头是否支持宽动态(WDR),支持则开启;软件层面,在视频帧进入检测前做一次CLAHE对比度增强,比普通直方图均衡更能保留边缘。我习惯的写法是:

lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB) lab_planes = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8)) lab_planes[0] = clahe.apply(lab_planes[0]) frame = cv2.cvtColor(cv2.merge(lab_planes), cv2.COLOR_LAB2BGR)

这段代码把图像从BGR转到LAB色彩空间,只对亮度通道做CLAHE增强,色度通道不变,避免偏色。clipLimit=2.5控制对比度限制幅度,太大可以增强但会放大噪声,太小没啥效果;tileGridSize=(8, 8)表示把图像分成8x8块,每块独自计算直方图。处理后再做疲劳检测,EAR基线能回升到正常范围的80%左右。

5.3 坐姿偏移后,人脸不在取景区

现象:司机习惯半躺着开车,或者座椅调得靠后,摄像头只能拍到额头或下巴,关键点寥寥几个,检测直接失败。

原因:固定摄像头的视野范围有限,AOI(感兴趣区域)设计不合理。很多源码默认摄像头装在车顶中央,但实际驾驶员的身高、座椅前后位置差异很大,视野中心可能偏离驾驶人面部。

解决:不要把整个画面都做检测,先设定一个ROI区域,只取画面中心偏上的矩形区域做人脸检测,减少误检也提高速度。另外,在装摄像头时,应让镜头光轴对准驾驶员右耳方向,而不是正对前挡风玻璃,这样能同时覆盖眼睛和仪表台。代码层面,ROI可以这样算:

h, w = gray.shape[:2] roi_top = int(h * 0.25) roi_bottom = int(h * 0.85) roi_left = int(w * 0.15) roi_right = int(w * 0.85) gray_roi = gray[roi_top:roi_bottom, roi_left:roi_right] faces = detector(gray_roi, 0) offset_x, offset_y = roi_left, roi_top

逻辑说明:先裁剪出一个纵向居中偏高的ROI,排除副驾和车窗外的干扰,检测到的人脸坐标要加上offset_x, offset_y才能映射回原图坐标。参数0.25到0.85的上下界,基本覆盖正常坐姿下的人脸活动范围。如果司机座得很低,下巴会被裁掉,那就要调低roi_bottom。这个参数在实车标定时必调。

5.4 检测线程越跑越慢,最后画面卡死

现象:程序刚启动时画面流畅,运行几分钟后帧率越来越低,最后cap.read()返回空帧,窗口白屏。

原因:内存泄漏。最常见的是在检测循环里不断创建dlib.rectangle或numpy.array对象,但上一帧的引用没有被释放;或者用了cv2.imshow却没有cv2.waitKey(1),导致OpenCV的窗口事件循环卡死。另一个原因是模型对象被反复加载,比如把dlib.shape_predictor写在检测函数内部,每帧都从磁盘读一遍模型。

解决:第一,确保检测模型在初始化时只加载一次,用全局变量或单例模式;第二,随时在循环末尾调用cv2.waitKey(1),这一步表面没用,却是OpenCV窗口正常刷新和处理键盘事件的关键;第三,用tracemalloc或简单打印psutil.Process().memory_info().rss来观察内存走势,一旦发现每1000帧内存增幅超过50MB,就要回头检查有没有把图像加入全局列表。排查代码片段如下:

import psutil process = psutil.Process() mem_before = process.memory_info().rss / 1024 / 1024 # ... 运行检测循环若干帧后再取一次 mem_after = process.memory_info().rss / 1024 / 1024 print(f"memory delta: {mem_after - mem_before:.1f} MB")

如果内存涨幅超过10MB,每帧在0.01MB左右,基本合理;如果每1000帧涨几十MB,那就是实锤泄漏。

5.5 语音报警在笔记本上无声

现象:程序里用pyttsx3播报“请注意休息”,在Windows台式机上正常,换到macOS或Linux工控机就没声音。

原因:pyttsx3在不同平台的底层引擎不同,Windows走SAPI,macOS走nsss,Linux走espeak。工控机通常没有安装espeak,而且没有音频输出设备驱动,导致引擎初始化失败但异常被吞掉。

解决:不依赖系统的TTS,改用预生成WAV文件播放。先用在线TTS或本机文字转语音工具生成几段“请注意休息”“检测到疲劳驾驶”的音频,然后用pygame.mixer或playsound播放。这样不仅跨平台稳定,还能自定义音色。播放代码:

import pygame pygame.mixer.init() pygame.mixer.music.load("alerts/take_rest.wav") pygame.mixer.music.play()

注意,pygame.mixer.init()必须在加载音频前调用,而且只能在主线程调用,放在检测线程里会出现SDL音频设备冲突。如果你的告警频率很高,还需要在播放前检查pygame.mixer.music.get_busy(),避免前一条还没播完又触发下一条,导致语音叠成乱码。

6. 从能跑到好用:模型轻量化、日志落盘与实测验证

当你的疲劳检测系统能在摄像头上稳定跑起来,下一个值得花时间的方向,是把模型换成一个更轻的替代品,并让系统具备可追溯性。dlib的68点模型在纯CPU上跑30FPS比较吃力,我通常把它替换成OpenCV 4.5以上的FaceDetectorYN,它内置的人脸检测比dlib的HOG方法召回率更高,在低光下表现也更好,只是关键点数量下降到6个,这时EAR没法直接算。我的习惯做法是保留dlib做人脸检测,但把关键点模型换成shape_predictor_5_face_landmarks.dat,这个小模型只有5个点,只覆盖两个眼睛中心和两个嘴角加鼻尖,速度提升一倍,配合眼部区域裁剪,依然能算出可靠的EAR。换模型后,原本的predictor调用路径不用大改,只要把点索引从68点改成5点的内侧眼角和外侧眼角序号即可。

系统跑通后,一定要做日志落盘,否则出事了没有后悔药。我会在检测线程里把每帧的时间戳、EAR、MAR、PERCLOS以及报警状态写入一个CSV文件,而不是只在界面上闪示。CSV行格式这样设计:

timestamp,ear,mar,perclos,is_alarm 2025-01-15 22:31:07.235,0.27,0.31,0.12,0

写日志不要每帧都open open close,应该在初始化时打开文件,循环中追加写入,并在程序退出时统一关闭。同时每5分钟对日志做一次分段归档,防止单个CSV文件过大导致后续分析困难。这个日志文件的价值在于,你能用Excel或Python脚本回放一段驾驶过程,看PERCLOS曲线在哪个时间点超过阈值,报警是否在合理时机触发,而不是靠目测视频判断系统好坏。

实测验证时,我一般会做两组测试。一组是“清醒基线”,让一个正常人盯着前方路面连续开模拟器10分钟,统计PERCLOS的平均值和最大值;另一组是“疲劳模拟”,让同一个人每隔30秒故意闭眼2到3秒,看系统能否在闭眼开始后0.5秒内把PERCLOS抬高并触发报警。通过这两组数据对比,你可以反推阈值是否需要继续调整。如果清醒基线的PERCLOS偶尔超过10%,说明阈值太低或EAR基线被噪声干扰;如果疲劳模拟中报警延迟超过2秒,说明窗口太长或持续帧数太多。

最后说一个我这些年养成的习惯:任何疲劳检测系统的参数,都不要在代码里硬编码。把它们全部放进config.yaml,并把配置文件做成可热加载——检测线程每30秒重读一次配置,这样你现场调参时不用重启程序。我吃过一次亏,当时在车上测试,为了调一个EAR阈值,反复停车重启了十几次,后来改成热加载,把笔记本放在副驾,一边开车一边让助手改阈值的感受完全不同。这个系统的核心从来不是模型多高级,而是你能不能把误报和漏报调整到一个让驾驶员愿意接受的平衡点。愿你拿到的源码包也能顺利跑起来,希望帮到你。

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

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

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

立即咨询