基于Python的驾驶员疲劳检测系统:CNN与关键点算法解析
发布时间:2026/9/26 18:56:25来源:尧图网络
简介一套基于Python与卷积神经网络构建的驾驶员疲劳检测与预警系统毕业设计资源集成了人脸识别与疲劳状态分析功能主要面向计算机、通信、人工智能、自动化等相关专业的学生和从业者。项目为作者个人毕设答辩评审98分代码经调试测试可直接运行非常适合小白入门也可作为期末课程设计、课程大作业或二次开发基础。资源包共37个文件涵盖16个Python脚本、9个pyc编译模块、5张测试图片、3个预训练权重、训练日志与说明文档并内置数据集压缩包整体约500.41MB源码模块涉及SSD/VGG网络结构、数据增强、损失函数、摄像头实时检测与视频文件检测等便于理解从算法到应用的完整链路。目前已有116人学习使用适合对照完整的高分毕业设计流程进行学习与功能扩展。1. 疲劳驾驶检测系统不是玄学是一套能从监控画面里算出“困了没”的工程深夜在高速上连续开三个小时眼睛闭合时间超过 0.4 秒这一瞬间的视觉特征足以让一个训练好的卷积神经网络抓住它——这就是基于 Python 的驾驶员疲劳检测与预警系统要做的事。这套资源包含两条主线一条用 CNN 做人脸识别确认当前坐在驾驶位的人是谁锚定司机身份另一条用 68 点关键点和 PERCLOS 近似算法把“眼皮半遮瞳孔”“持续眨眼变慢”量化成可以触发报警的 EAR 数值。只要摄像头正常、光照不过于极端单人驾驶场景下误报率能压到工程上可接受的范围。这份源码加数据集适合正在做毕业设计、想快速跑通一整套检测 demo、或者打算把方案移植到门禁考勤项目里的人拿到手就能从环境配置开始一步步复现。2. 人脸框不住后面全是空转MTCNN 与 68 点关键点的落地链路2.1 为什么这条链路不选 Haar 而选 CNNOpenCV 自带的 Haar 级联检测器一行代码就能调用但它在遮挡、暗光、侧脸场景下非常容易把检测框丢掉或者框偏。疲劳检测的后续所有计算都依赖第一步的人脸框框一旦歪了EAR 的分子分母就全乱套。所以资源里的检测器用的是基于卷积神经网络的 MTCNN它包含 P-Net、R-Net、O-Net 三级网络P-Net 先快速产出候选框R-Net 做精筛O-Net 在最后输出边框回归、置信度和 5 点关键点坐标。MTCNN 在 CPU 上处理一帧大约需要 30 到 80 毫秒加后面 68 点回归i5 级处理器能跑到 15 到 20 FPS对驾驶预警来说已经够用。如果你的机器只有核显把输入分辨率从 1080p 压到 720p检测框上限控制在 640 像素以内帧率能再翻一倍。这条选型路线在资源对应的代码里也是默认配置优先保证的是稳定而不是炫技。2.2 MTCNN 检测与 5 点 landmark 的代码实现先看基本的检测流程注意图像通道顺序在这个库里的特殊要求。from mtcnn import MTCNN import cv2 # 初始化检测器参数是资源和项目代码里的常见配置 detector MTCNN( min_face_size60, # 小于 60px 的人脸直接忽略减少误检 steps_threshold[0.6, 0.7, 0.7], # P-Net / R-Net / O-Net 置信度阈值 scale_factor0.709 # 图像金字塔缩放系数 ) frame cv2.imread(driver_frame.jpg) # 读进来是 BGR rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # MTCNN 内部用 RGB results detector.detect_faces(rgb) if results: x, y, w, h results[0][box] conf results[0][confidence] lmk results[0][keypoints] # lmk 里有 left_eye / right_eye / nose / mouth_left / mouth_right cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2)代码逻辑是先把 OpenCV 读进来的 BGR 图像转成 RGB因为 MTCNN 训练时用的是 RGB 输入不做转换会导致检测率和关键点位置出现肉眼可见的偏移。这里如果直接把 results 里的box拿去做裁剪要注意它给的是左上角坐标和宽高不是中心点后面切 ROI 时不要惯性除以 2。参数说明min_face_size60是实测里的一个折中值再往下调会把远处的人误检出来steps_threshold三级网络阈值保持前低后高P-Net 松一些先广撒网O-Net 收紧保证精度。实际夜间场景如果检测不到脸优先把 0.6 降到 0.5而不是去动后面两级。2.3 疲劳判据依赖的 68 点dlib 模型加载与调用MTCNN 输出的 5 点只能描述眼睛中心、鼻尖、嘴角的大概位置算不出眼睑张开程度。计算 EAR 需要眼睑边缘的精确坐标这一步资源里用的是 dlib 的 68 点回归模型输出 68 个脸部关键点分布覆盖眉毛、眼睛、鼻子、嘴巴和下颌轮廓。这个 68 点属于传统机器学习里的回归方法不是 CNN但它是疲劳判据的标准拍档卷积神经网络负责把你的人脸位置找出来回归器负责给出几何细节。两者分工明确谁也别想替代谁。import dlib predictor_path models/shape_predictor_68_face_landmarks.dat predictor dlib.shape_predictor(predictor_path) # rect 来自 2.2 节 MTCNN 的检测结果dlib 需要接收 dlib.rectangle rect dlib.rectangle(int(x), int(y), int(x w), int(y h)) # dlib 同样要求 RGB 图像直接用 2.2 节的 rgb 变量 shape predictor(rgb, rect) # 把 shape 转成 numpy 数组方便后续做向量运算 import numpy as np coords np.array([[p.x, p.y] for p in shape.parts()])代码逻辑是先把 MTCNN 的人脸框转换成 dlib 的 rectangle 类型然后调用回归器。注意这里用的输入图像还是 RGBdlib 如果收到 BGR 图关键点整体位置仍然正确但细节特征的位置会有系统偏移轻则 EAR 轻微偏高重则闭眼判断失效。提示资源里的models/目录如果缺了这个.dat文件程序会直接报错。这个模型文件开源自带不用训练放到代码指定的相对路径即可。3. EAR 与 PERCLOS把“眼神涣散”变成可以报警的数字3.1 EAR 眼睑闭合度的几何定义与计算EAREye Aspect Ratio算的是单只眼睛的纵横比原理很朴素眼睛睁开时垂直方向的两个关键点距离远EAR 值大眼睛闭合时垂直距离趋近于零EAR 值骤降。正常平视时左眼右眼的 EAR 通常在 0.25 到 0.35 之间闭眼时会跌到 0.1 以下。公式落在代码上是这样def eye_aspect_ratio(eye_pts): # eye_pts 是按顺序排列的 6 个关键点坐标形状为 (6, 2) p2_p6 np.linalg.norm(eye_pts[1] - eye_pts[5]) # 垂直距离 1 p3_p5 np.linalg.norm(eye_pts[2] - eye_pts[4]) # 垂直距离 2 p1_p4 np.linalg.norm(eye_pts[0] - eye_pts[3]) # 水平距离 return (p2_p6 p3_p5) / (2.0 * p1_p4)逻辑说明分子是两个垂直距离之和分母是水平距离的两倍。这么做是为了抵消人脸到摄像头的距离变化——人往后靠或者往前凑水平和垂直距离等比缩放比值基本不变。这也是 EAR 比直接看“眼睛高度像素数”更稳定的原因。参数说明眼周 6 个关键点在 68 点模型里的索引是固定的左眼 37 到 42右眼 43 到 48。写代码时用coords[37:43]和coords[43:49]切片不要自己手工去数坐标数错一个索引整个判断就反了。3.2 PERCLOS 时间窗统计与疲劳分级单帧 EAR 没有统计意义人本来就会眨眼一帧闭眼不能说明什么。PERCLOS 统计的是单位时间内眼睛闭合帧数占总帧数的比例工程实现上用一个滑动窗口队列每帧推入 0 或 1然后算均值。from collections import deque eye_closed_history deque(maxlen900) # 保持 60 秒的帧记录 def update_perclos(ear, ear_thresh0.22, historyeye_closed_history): # EAR 低于阈值记为闭眼 1否则记为 0 history.append(1 if ear ear_thresh else 0) closed_ratio sum(history) / len(history) return closed_ratio逻辑说明deque(maxlen900)满 900 个元素后自动弹出最旧的一帧这样闭眼占比是个滚动值不会因为跑到第 30 分钟累计数太大而失去敏感性。900 这个数字对应假设采集帧率 15 FPS、窗口 60 秒。参数说明ear_thresh0.22是参考值不是铁律。有人眼睛大有人眼睛小戴眼镜和不戴眼镜基线都不同后面第 5 章会讲怎么针对场景标定这里先用经典值跑通流程。疲劳分级可以这样切PERCLOS 值低于 0.15 视为正常0.15 到 0.3 轻度疲劳高于 0.3 直接报警。阈值最终可以在界面上做成可调参数方便现场调试。3.3 阈值标定三种参考标准怎么选疲劳判据在学术界有多个标准P80 表示眼睑遮住瞳孔超过 80% 才计为闭合FM 是眼皮遮住一半就算H3 是眼球运动频率的门槛。落到代码里P80 对应比较低的 EAR 阈值FM 对应用一个中间值。这个资源实现的方案是拿 0.22 作为通用基线同时留出配置接口。常见做法是把 EAR 阈值、时间窗口、报警级联做成一个 JSON 配置文件现场部署时改动配置不需要重新编译代码。我之前跑实验时会把不同阈值打印成一行日志存下来回头对比误报率哪个低最后发现纯靠调阈值改善有限真正影响大的是摄像头安装角度和下视角。摄像头放在方向盘正前方抬高 30 度看人脸和放在仪表台平视同一套阈值结果能差一档。4. 身份识别与预警联动从单帧算法到可交付的系统4.1 人脸识别embedding 比对与驾驶位绑定疲劳检测不关心坐在车里的是谁但系统如果要做多司机管理比如网约车司机轮班、车队后台记录是谁疲劳了就必须有人脸识别能力。资源里的做法是用预训练的 CNN 模型把每一帧人脸编码成一个高维向量然后和驾驶员库里提前注册的向量做余弦相似度比对。from scipy.spatial.distance import cosine # embedding_a 是当前帧的人脸向量embedding_b 是注册时的基准向量 def verify_driver(embedding_a, embedding_b, thresh0.6): # 余弦距离小于阈值认为同一个人 distance cosine(embedding_a, embedding_b) return distance thresh, distance逻辑说明预训练卷积网络输出的是 128 维或 512 维的特征向量这个向量对同一个人在不同光照下的表现足够稳定对不同人有区分度。比对过程本身是在向量空间里算距离不涉及传统意义上的“特征匹配”。参数说明thresh0.6是一个偏保守的口令安全优先宁可多问几次也别认错人。如果司机库只有三五个人阈值可以放到 0.65如果做陌生人拦截压到 0.5 会让误拒率变高。这里需要注意资源里配套的人脸识别模型若是在公开人脸数据集上预训练的直接拿来比对即可不需要在驾驶场景重新训练顶多补录几张注册照。4.2 系统流程与模块划分整个系统按职责拆成四个模块视频采集、检测与关键点回归、疲劳判据与身份比对、界面与报警。采集放子线程防止摄像头帧堆积推理放在单独线程UI 主线程只负责刷新画面和响应报警信号。模块之间通过队列传递数据不直接互相调用这样摄像头换了或者模型换了改动局部不影响整体。我通常在项目里维持三条队列原始帧队列、检测结果队列、疲劳统计队列。生产者的帧率比消费者的推理速度快如果队列堆积超过 10 帧就强制丢旧帧保证预警实时性。4.3 界面与报警线程PyQt5、声音与日志落盘界面这一层资源里用的是 PyQt5报警逻辑放到 QThread 里UI 线程通过信号槽接收预警等级避免在子线程里直接操作控件导致崩溃。from PyQt5.QtCore import QThread, pyqtSignal class AlarmThread(QThread): alarm_signal pyqtSignal(int) # 0 正常 1 轻度 2 严重 def __init__(self, perclos_getter, parentNone): super().__init__(parent) self.perclos_getter perclos_getter def run(self): while True: ratio self.perclos_getter() # 从共享队列读 PERCLOS 值 if ratio 0.30: self.alarm_signal.emit(2) elif ratio 0.15: self.alarm_signal.emit(1) else: self.alarm_signal.emit(0) self.msleep(1000) # 每秒判断一次避免高频刷信号逻辑说明msleep(1000)是报警轮询频率每秒一次足够声音和界面提示都不需要毫秒级刷新。信号只发整数枚举值UI 侧根据数值决定是亮黄灯还是红灯播放蜂鸣声。参数说明如果摄像头实际帧率只有 10 FPSPERCLOS 窗口 900 帧对应 90 秒报警响应会晚半拍这时应该把窗口长度改成 600让时间窗保持在 60 秒左右。报警响起的同时把当前帧、EAR 值、PERCLOS 值、司机 ID 写进 CSV 日志字段按时间,司机ID,EAR,PERCLOS,预警等级排列后面追责或者做数据分析都能回溯。5. 避坑清单光照、眼镜、口罩和“蹦迪”的关键点坐标5.1 光照直射导致检测框“蹦迪”现象白天经过高架桥下或者夜里对面来车远光灯一闪画面里的人脸框前后两帧位置跳变超过几十像素EAR 值跟着剧烈抖动偶尔还会误报疲劳。原因MTCNN 对极端亮度变化敏感过曝时脸部对比度下降候选框置信度在阈值附近震荡检测头一会儿锁住眼周一会儿锁住下巴。人脸框本身就抖68 点坐标随之飘移EAR 就会瞬时而快速地往下跌。解决先用 OpenCV 对每帧做 CLAHE 自适应直方图均衡再做检测。具体做法是cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8))作用在 YUV 空间的 Y 通道上。我实测加这一步后夜间检测率提高约两成框的稳定性明显改善。另外对前后帧框位置做一阶低通滤波也让坐标点输出平滑不少。注意直接对整个 RGB 图做 CLAHE 会把颜色特征破坏掉人脸识别 embedding 会受影响。处理顺序是原始帧给 MTCNN 和 dlibCLAHE 后的帧给疲劳判据身份比对那条路原样走原始帧。5.2 眼镜反光与闭眼误判的纠缠现象戴眼镜的司机录制测试时系统在正常睁眼状态下报出闭眼尤其在眼镜下方或者镜片边缘有反光的时候误判发生频率显著上升。原因68 点回归器把镜框边缘或反光高光误当成眼睑中点导致垂直距离被压扁EAR 掉到闭合阈值以下。本质原因是训练 68 点模型的数据集里眼镜样本不足眼镜腿和镜框的遮挡让关键点回归出现系统性偏移。解决不要试图靠提高 EAR 阈值来补偿那样会把真实闭眼也放过。我在代码里加了“左右眼一致性校验”如果左右眼 EAR 差值超过 0.12判定当前是外部干扰丢弃这一帧不参与 PERCLOS 统计。闭眼时两只眼几乎同步闭合差值很小反光通常只影响单侧这个简单规则能消掉大部分误报。5.3 口罩遮挡下嘴部特征失效现象司机戴上口罩后程序如果启用了张嘴打哈欠检测会频繁误报疲劳甚至报出陌生人脸。原因口罩把嘴部 68 点关键点区域完全遮住MAR嘴部纵横比计算出来的数值里全是噪声。更麻烦的是 MTCNN 的 mouth keypoint 被推到口罩织物表面上脸部对齐基准线整体偏移间接影响眼部区域的裁剪。解决资源里的疲劳判据以 EAR 和 PERCLOS 为主我在演示代码里把 MAR 模块默认关闭保留接口但是不参与报警决策。戴口罩场景下睁眼时间反而更长单纯靠眼部特征判断疲劳已经足够嘴部信息在完全遮挡状态下没有可信度。5.4 歪头睡导致坐标点整体旋转现象司机疲劳到一定程度会歪头靠在头枕上此时 EAR 数值整体变小系统误判为闭眼但司机实际眼睛半闭半睁。原因EAR 是比例算出来的不变量前提是脸部平面正对摄像头。歪头后眼睑中点到眼角的相对位置在投影几何上发生变化垂直距离被压缩比值失真。解决先估算头部欧拉角使用 68 点中的鼻梁、下巴和两眼中心三个点做姿态估计。如果测得 yaw 或 roll 超出 ±30 度就不是一个可靠的 EAR 测量姿态这时候改用闭眼持续时间作为备选判据——连续 15 帧 EAR 低于 0.15 且姿态角持续偏转直接触发严重疲劳报警。倾斜姿态下关键点回归本身也在失效把姿态角作为置信度开关加到判断逻辑里是最稳妥的做法。5.5 长跑几小时后内存缓慢增长现象系统开机运行四五个小时后内存占用从 500MB 慢慢涨到 2GB最后界面卡死。原因dlib 的 shape predictor 每次调用都会返回完整的 shape 对象如果代码里没有及时释放引用或者把每一帧的coords都累积进全局列表做统计内存会线性增长。解决PERCLOS 的deque(maxlen900)已经做了上限控制检查一下日志列表或者检测框绘制中间变量有没有做同样的限制。我一般对调试用的临时列表强制maxlen128不管攒什么历史数据都加一个上限写完了就清空。6. 进阶验证给自己模型一份“体检报告”再交差6.1 测试集与标注口径一套检测系统交出去之前至少要回答“误报率和漏报率分别是多少”。资源里准备的数据集如果是按帧划分的图片先按司机 ID 分组而不是按视频随机切分避免同一个人出现在训练侧和测试侧造成虚假高分。标注口径统一成四类睁眼、闭眼、打哈欠、目视前方每一帧标注对应 EAR 真值区间。6.2 用 ROC 曲线校准最终阈值跑测试集时把每个样本的 EAR 和 PERCLOS 存下来然后做一次二分类评估。import numpy as np from sklearn.metrics import roc_curve # labels 是人工标注的标签1 表示疲劳闭眼scores 是系统输出的 PERCLOS 值 fpr, tpr, thresholds roc_curve(labels, scores) # 选出等错误率最低的阈值距离左上角最近的点 distances np.sqrt(fpr ** 2 (1 - tpr) ** 2) best_idx np.argmin(distances) best_thresh thresholds[best_idx] print(calibrated threshold:, round(best_thresh, 3))逻辑说明ROC 曲线能直观看到阈值放到多少时误报率和漏报率平衡。源码里默认 0.22 或 0.3 都只是经验值数据集跑出来的最优阈值可能偏到 0.25拿这个值写进配置文件才算完成闭环。数据增强是另一个容易被忽略的环节。给数据集里每张图做亮度随机增减、水平翻转、加少量高斯噪声能让 MTCNN 和 68 点回归在夜间和摄像头差的环境里更稳。翻转要小心人脸是半脸对称的闭眼状态翻转后语义不变但左眼右眼的关键点顺序要跟着换。从那以后我每次交付疲劳检测项目都强制走一遍“采集样本 → ROC 标定 → 实车路测”路径上每抠出来一个阈值参数都写进配置文件的注释里。这套习惯帮我少背了不少锅也把误报率从 demo 阶段的 10% 压到了实车可接受的 2% 上下希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网