ARTICLE DETAIL

资讯详情

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

疲劳驾驶检测实战:Python+OpenCV从人脸关键点到PERCLOS预警

疲劳驾驶检测实战:Python+OpenCV从人脸关键点到PERCLOS预警 简介一套基于驾驶员面部特征的疲劳检测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 python3.8 conda activate fatigue pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simplerequirements.txt里通常会包含这些核心依赖opencv-python4.8.0.76 dlib19.24.2 numpy1.24.3 PyYAML6.0 pyttsx32.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.pyimport 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(fdetected {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的前提是判断每一帧眼睛是否闭合。最常用的指标是眼睛纵横比EAREye 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 嘴巴与头部姿态打哈欠和点头怎么算单看眼睛还不够有些疲劳状态是频繁打哈欠或无意识点头。嘴巴开合度可以用嘴巴纵横比MARMouth 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_size300, ear_threshold0.2, closed_ratio_threshold0.3): self.eye_closed_history deque(maxlenwindow_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(maxlen300)假设视频帧率是25FPS300帧对应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(maxsize2) result_queue queue.Queue(maxsize2) 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, keylambda 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(maxsize2)限制了队列长度当帧处理速度跟不上时新的结果会挤掉旧的保证UI拿到的永远是最新帧。这个drop策略比无界队列更合理无界队列在检测慢时会积压几百帧内存和延迟都失控。maxsize2意味着最多积压1帧新的旧帧被丢弃UI画面和实时检测的偏差始终控制在两帧以内。参数说明如果检测线程输出结果的速度是15FPSUI线程绘制能力是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毫秒如果帧率是25FPS3帧只有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(clipLimit2.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增强色度通道不变避免偏色。clipLimit2.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(fmemory delta: {mem_after - mem_before:.1f} MB)如果内存涨幅超过10MB每帧在0.01MB左右基本合理如果每1000帧涨几十MB那就是实锤泄漏。5.5 语音报警在笔记本上无声现象程序里用pyttsx3播报“请注意休息”在Windows台式机上正常换到macOS或Linux工控机就没声音。原因pyttsx3在不同平台的底层引擎不同Windows走SAPImacOS走nsssLinux走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阈值反复停车重启了十几次后来改成热加载把笔记本放在副驾一边开车一边让助手改阈值的感受完全不同。这个系统的核心从来不是模型多高级而是你能不能把误报和漏报调整到一个让驾驶员愿意接受的平衡点。愿你拿到的源码包也能顺利跑起来希望帮到你。本文还有配套的精品资源点击获取
返回列表