ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Dlib驾驶员疲劳检测实战:眨眼、哈欠、点头识别与PyQt5界面

Dlib驾驶员疲劳检测实战:眨眼、哈欠、点头识别与PyQt5界面 简介基于Python与Dlib模型的驾驶员疲劳检测毕业设计资源面向计算机、人工智能、通信工程、自动化等专业学生和开发者适用于课程设计、项目初期演示或二次开发。项目从人脸朝向、眼睛开合度、眨眼频率、瞳孔收缩率等维度实时分析驾驶员状态对打哈欠、眨眼异常和瞌睡点头行为作出安全提示。压缩包共17个文件约85.92MB含6个Python源码、2个ipynb交互式演示、界面工程fbp、人脸关键点模型dat、操作视频mp4、可执行exe及说明文档等源码均已测试运行答辩评审平均分达96分并配有可视化界面。目前已有232人学习下载。除完整代码外还附带安装包、运行效果图、README和模型文件方便对照阅读与快速复现其中dat模型与exe可直接使用降低环境配置门槛。整体目录结构清晰适合作为毕业设计参考也可在此基础上扩展更丰富的驾驶安全提醒功能。1. 为什么选 Dlib 做驾驶员疲劳检测而不是直接上深度学习毕业设计里做驾驶员疲劳检测最常见的坑是方案选太重。一上来就上目标检测网络环境、标注、训练走完一轮一半时间没了。Dlib 的 68 点关键点模型在 CPU 上单帧 20~40ms用几何比例就能量化眨眼、打哈欠、点头三个动作。这套方案最大的优点是阈值可解释调低闭眼阈值只影响眨眼判定不会像端到端网络那样改一个参数就要重训。学生能由此快速跑通完整系统有经验的工程师也能拿它作为实时行为检测的基线方案用来和深度学习结果做对比。下文先实现眨眼检测并标定参数再补上哈欠和瞌睡点头最后用 PyQt5 汇总成可视化界面并用回放视频做回归验证全程围绕 Dlib 模型展开所有结论都能在拿到源代码后直接复现。2. Dlib 人脸关键点检测用 EAR 实现眨眼检测与参数标定疲劳检测的第一步是把人脸转成可计算的数值。Dlib 的 68 点模型会输出眉毛、眼睛、鼻子、嘴巴、下巴等位置的坐标其中每只眼睛对应 6 个点这 6 个点在一个经典公式里叫 EAREye Aspect Ratio眼部纵横比。基于 Dlib 的疲劳检测系统绝大多数实现的核心就是先算 EAR再顺着它扩展出哈欠和点头。2.1 EAR 公式推导为什么闭眼时比值会掉下来眼睛关键点的编号是固定的36~41 是右眼的 6 个点以受测者自身左右为准在画面里位于左侧42~47 是左眼每个点按逆时针排列。EAR 取水平方向一对点的距离做分母垂直方向两对点的距离求和做分子再除以 2。睁眼时垂直距离接近水平距离的一半EAR 落在 0.25~0.35闭眼时上下眼睑重合两个垂直距离趋近于 0EAR 会掉到 0.15 以下。因为分子分母都来自同一张脸EAR 天然对画面中脸的大小、离摄像头远近做了归一化这个性质让它成为疲劳检测里最稳定的输入。from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # eye: [(x, y)] * 6, 按 dlib 输出的逆时针顺序传入 vertical_1 dist.euclidean(eye[1], eye[5]) # 上眼睑两端的对角距离 vertical_2 dist.euclidean(eye[2], eye[4]) # 下眼睑两端的对角距离 horizontal dist.euclidean(eye[0], eye[3]) # 内眼角到外眼角 return (vertical_1 vertical_2) / (2.0 * horizontal)代码把 6 个点拆成 3 对欧氏距离。vertical_1 和 vertical_2 是上下眼睑的两组对角距离horizontal 是内眼角到外眼角。用平均垂直距离除以水平距离就得到与脸大小无关的比值。这里有一个很多人踩过的坑eye 的坐标顺序不能打乱一旦把 6 个点重排成顺时针vertical 和 horizontal 对不上EAR 会变成无效值。scipy 里的 distance.euclidean 是纯数值计算直接传入 (x, y) 元组即可不需要手动写平方和开方。2.2 python 安装与 dlib 模型文件放置先跑通最小检测在动手写计数逻辑前先把环境处理利落。python 建议用 3.8~3.10按 python 安装教程装完勾选 Add to PATHdlib 在 Windows 上最容易卡在编译环节如果 pip install dlib 报 C 编译错误常见做法是下载与 python 版本、系统位数匹配的预编译 wheel再离线安装能省掉 CMake 和 Visual Studio Build Tools 的折腾。模型文件名为 shape_predictor_68_face_landmarks.dat从 dlib 模型库下载后放在和主程序相同的目录代码里用相对路径就能加载。提示Windows 下如果 dlib 安装卡在编译先确认是否缺少 Microsoft C Build Tools下载预编译 wheel 是最省事的路径。如果你刚接触 python建议先把下面这个最小例子跑通再往下加逻辑。最小例子的目标只有一个在画面里看到实时的 EAR 数值。开发时用 pycharm 配置 python 环境记得在 Run/Debug Configurations 里确认解释器选的是安装了 dlib 和 opencv-python 的那个虚拟环境否则运行时报 ModuleNotFoundError那不是代码问题是环境串了。import dlib import cv2 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) # 第二个参数是图像上采样次数 for face in faces: landmarks predictor(gray, face) left_eye [(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)] right_eye [(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 cv2.putText(frame, fEAR: {ear:.2f}, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(dlib EAR, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()detector 负责在灰度图里找人脸predictor 在返回的人脸框上做关键点回归。取点左右眼各 6 个点算出 EAR 后直接画到帧上。dlib 的检测器对灰度图更稳定所以先 cvtColor 再做检测左右眼取的编号 36~41 和 42~47 与画面里的左右相反这是 dlib 标注的语义决定的不需要改。detector 的第二个参数是上采样次数调成 2 更容易检出较远、较小的脸代价是单帧耗时上升。这一步跑通后画面里应该有实时 EAR 数值如果完全检测不到人脸优先检查摄像头权限和光线。2.3 眨眼计数逻辑与三个必调参数有了 EAR 序列眨眼就是一次“低于阈值、持续若干帧、再恢复”的完整波形。简单做法是维护一个 frame_counterEAR 低于阈值时累加高于阈值时判断累加值是否超过连续帧下限超过才把眨眼次数加一。用连续帧过滤的原因在于正常快速眨眼只有 100~200ms而眼睛眯一下、眨一半、刻意眨眼都会产生短暂的 EAR 波动不设下限会出现一次眨眼被拆成两次计数。EAR_THRESH 0.22 # EAR 低于该值, 认为眼睛处于闭合状态 CLOSED_FRAMES 3 # 连续闭合多少帧才算一次眨眼 frame_counter 0 blink_total 0 while True: # ... 沿用 2.2 的检测循环, 拿到 ear 之后做计数 if ear EAR_THRESH: frame_counter 1 else: # 只有连续闭合超过 CLOSED_FRAMES 帧, 才累积一次眨眼 if frame_counter CLOSED_FRAMES: blink_total 1 frame_counter 0帧率变化会直接影响 CLOSED_FRAMES 的意义30fps 下 3 帧约 100ms15fps 下 3 帧就是 200ms。换摄像头或改分辨率前先确认帧率别把参数从旧环境直接搬过来。下面这张参数表是这类 Dlib 疲劳检测项目里最常用的起步组合。参数推荐初值作用调节方向EAR_THRESH0.22判定眼睛是否闭合戴眼镜、离摄像头偏远时下调到 0.18~0.20CLOSED_FRAMES3一次眨眼最少持续帧数帧率 30fps 用 2~4低于 15fps 用 2统计窗口60 s计算眨眼频率与 PERCLOS 的时间窗标定时先跑 2 分钟取平均值EAR_THRESH 是三个参数里最敏感的一个。调高会让“眯眼”被记成眨眼调低会漏掉闭眼幅度小的人。比较稳的标定顺序是保持其他参数不动只调 EAR_THRESH让标注视频里的眨眼全部被检出且不产生多余计数。第 5 章会给出用回放视频自动找 F1 最大值的方法这里先记住一个原则一次只动一个参数改完立刻看波形不要同时调三个值。3. 打哈欠与瞌睡点头嘴巴开合比和鼻尖位移的联合检测眨眼解决的是“眼睛闭合”这一维哈欠和点头分别对应“张嘴持续”和“头部下压”。三者共用同一批 dlib 关键点判定逻辑都是“几何比例 持续帧数”所以放进同一个检测循环里最合理。单独实现哈欠或点头意义不大疲劳检测系统的完整度体现在这三个指标能互相印证。3.1 嘴巴开合比 MAR为什么不用嘴部面积打哈欠时嘴张得大且持续 0.5 秒以上和说话、打喷嚏的关键区别在持续时长。嘴巴开合程度用 MARMouth Aspect Ratio度量取外唇上沿中心点 51 和下沿中心点 57 的垂直距离除以嘴角点 48 到 54 的水平距离。平时嘴巴闭合时这个比值在 0.1~0.2打哈欠时会超过 0.6。有人会直接用嘴唇轮廓围出的像素面积判断但这依赖人脸框大小的稳定性摄像头远近一变面积就失真。用比值做归一化后远近和脸型差异基本被抵消阈值可以跨设备沿用这正是几何方法比面积法更适合疲劳检测的原因。def mouth_aspect_ratio(shape): # 索引对应 dlib 68 点标注: 51 上唇中心, 57 下唇中心 top (shape.part(51).x, shape.part(51).y) bottom (shape.part(57).x, shape.part(57).y) # 48 和 54 分别是左右嘴角 left (shape.part(48).x, shape.part(48).y) right (shape.part(54).x, shape.part(54).y) return dist.euclidean(top, bottom) / dist.euclidean(left, right)哈欠判定加一层持续帧过滤MAR 连续超过阈值达到 20 帧30fps 下约 0.7 秒才累加一次哈欠。普通说话偶尔也会让 MAR 短暂越过 0.6但一般撑不过 0.5 秒所以持续帧数比阈值本身更能区分“说话”与“哈欠”。打完哈欠后人的嘴会先张大再闭合如果只想统计完整哈欠可以用和眨眼类似的恢复判定避免一次哈欠被记成两次。3.2 点头检测用鼻尖到眼线的距离而不是解头部姿态角理论上点头可以用 solvePnP 解出头部俯仰角但需要知道相机内参换摄像头就得重新标定毕业设计里不划算。常见做法是度量鼻尖相对眼线的位置把左右眼各自的中点连成一条线取鼻尖点 30 到这条线的垂直距离再除以双眼距离做归一化。正常抬头时鼻尖明显低于眼线比值较大低头时人脸在画面里纵向压缩鼻尖向眼线靠近比值缩小。瞌睡点头的动作特征是“比值先低于阈值、保持一小段时间、再恢复”和一直低头看仪表盘的区别在于它有回弹所以计数逻辑必须等回弹出现才算一次完整的点头。def head_nod_ratio(shape): # 左眼中点: 外眼角 36 与内眼角 39 取平均 left_mid ((shape.part(36).x shape.part(39).x) // 2, (shape.part(36).y shape.part(39).y) // 2) # 右眼中点: 内眼角 42 与外眼角 45 取平均 right_mid ((shape.part(42).x shape.part(45).x) // 2, (shape.part(42).y shape.part(45).y) // 2) eye_line_y (left_mid[1] right_mid[1]) / 2.0 nose_y shape.part(30).y # 鼻尖 eye_width dist.euclidean(left_mid, right_mid) return (nose_y - eye_line_y) / eye_width运行这段代码前先记录一个正常坐姿下的基线值比如前 30 帧的均值。点头判定用相对基线的比例当 nod_ratio 低于基线值的 60% 且持续 8 帧以上记为一次下压恢复之后再检测回弹才算完整的一次点头。只记下压不记回弹会把“低头捡东西”误判成瞌睡这是实车测试最容易暴露的问题。3.3 合并进同一个检测循环一次 dlib 推理同时喂给三个检测器这一个循环里同时维护眨眼、哈欠、点头三组计数器它们共享 detector 和 predictor 的输出所以整帧只做一次人脸检测和一次关键点回归CPU 占用不会因为指标变多而翻倍。整个 Dlib 疲劳检测的源代码主干实际上就是下面这个循环后续加可视化界面时也只改数据出口。YAWN_THRESH 0.6 # 嘴部开合比阈值 YAWN_FRAMES 20 # 张嘴持续帧数下限, 约 0.7s NOD_BASE_RATIO 0.6 # 下压阈值 基线值 * 该系数 NOD_FRAMES 8 # 下压持续帧数下限 while True: # ... 前面的人脸检测与 landmarks 获取 left_eye [(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)] right_eye [(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(landmarks) nhr head_nod_ratio(landmarks) if mar YAWN_THRESH: yawn_counter 1 else: if yawn_counter YAWN_FRAMES: yawn_total 1 yawn_counter 0 nod_down nhr nod_baseline * NOD_BASE_RATIO if nod_down: nod_counter 1 else: # 只统计完成下压 回弹的点头, 上下限过滤抖动 if NOD_FRAMES nod_counter 30: nod_total 1 nod_counter 0nod_baseline 用前 30 帧的 nhr 均值初始化之后每帧用指数移动平均缓慢更新防止坐姿调整后基线失效。三个计数器都在 60 秒窗口内累计窗口结束时重置同时把统计结果交给界面层。这里有个容易被忽略的坑nod_counter 上限写死 30 帧如果摄像头帧率低于 20fps正常点头可能超过上限被漏记需要根据实际帧率放宽。下面这张表概括了本章四个参数的失败模式标定时按这个方向排查。参数推荐初值作用失败时的表现YAWN_THRESH0.6嘴部开合比阈值阈值太低会把说话当哈欠YAWN_FRAMES20张嘴持续帧数下限太大漏检短哈欠太小误报NOD_BASE_RATIO0.6下压深度的相对阈值太小点头被当成正常低头NOD_FRAMES8下压持续帧数下限太大漏检快速点头4. 可视化界面用 PyQt5 把眨眼、哈欠、点头聚合成疲劳等级三个检测器产出的是原始计数可视化界面要回答的问题是“这个人现在疲劳吗”。界面在毕业设计里的作用不只是展示它承担实时状态显示、警报触发和参数调节三个职责。我用 PyQt5 搭界面因为它和 OpenCV 同属 python 生态信号槽机制天然适合把检测线程和界面线程分开。4.1 界面线程拆分为什么检测不能放在 UI 线程dlib 检测在 CPU 上每帧 20~40ms如果直接写在 PyQt 的事件循环里鼠标拖动窗口、点击按钮的响应都会被检测过程卡住界面上视频会一卡一卡。常见做法是开一个 QThread 专门跑摄像头读取和检测通过 signal 把处理后的帧和统计数据发回主线程主线程只负责显示和更新控件。线程分离之后即使检测线程偶发掉帧界面依然能即时响应。from PyQt5.QtCore import QThread, pyqtSignal class FatigueWorker(QThread): frame_ready pyqtSignal(object, dict) # 一帧图像 当前统计 level_changed pyqtSignal(str) # 疲劳等级变化时发出 def __init__(self): super().__init__() self.running True def run(self): # 把第 3 章的 while True 循环搬进来 while self.running and cap.isOpened(): ret, frame cap.read() # ... 三个检测器更新 ear/mar/nhr, 维护三组计数 stats { ear: ear, mar: mar, blink: blink_total, yawn: yawn_total, nod: nod_total, } self.frame_ready.emit(frame, stats) # 每 1 秒调用一次疲劳等级判定, 触发 level_changedQThread 里不能直接操作界面控件数据只能通过信号传出去。frame_ready 携带一帧图像和当前统计level_changed 在疲劳等级变化时发出主线程对应的槽函数只做赋值和重绘。写的时候注意把 cap 和 detector 都作为 worker 的属性方便 stop 时统一释放cap 不要在线程外用全局变量引用否则退出时容易报“QThread: Destroyed while thread is still running”的警告。4.2 疲劳等级判定PERCLOS、哈欠、点头怎么汇总单一指标误报率太高有人天生眨眼频率低有人习惯性张嘴。更稳的做法是三个指标在 60 秒窗口内各算一个状态再按规则合成疲劳等级。PERCLOS 是闭眼帧数占总帧数的比例是驾驶疲劳研究里最经典的标准通常以 P80 判据为准即眼睑遮挡瞳孔面积超过 80% 的时间占比。指标60s 窗口正常轻度疲劳重度疲劳PERCLOS 0.30.3 ~ 0.4≥ 0.4哈欠次数≤ 12 ~ 3≥ 4点头次数01≥ 2综合规则任一指标达到重度判定重度疲劳两个及以上指标达到轻度判定轻度疲劳其余情况正常。这个规则的好处是可解释论文里能直接写清楚每个等级对应什么行为答辩时不会被追问“你这个分数是怎么算出来的”。想改成打分制也可以比如给 PERCLOS、哈欠、点头分别配 0.45、0.3、0.25 的权重但规则制在实车标定时更容易定位问题建议先把规则制跑通再考虑加权。4.3 界面骨架视频区、疲劳等级、统计面板的代码组织界面大致分三块左侧是实时视频QLabel 显示帧右侧上方是疲劳等级指示灯用 QLabel 换背景色右侧下方是统计面板每秒刷新眨眼、哈欠、点头计数。OpenCV 的帧是 BGR送到 QLabel 前要转成 RGB 的 QImage否则颜色会偏蓝。这里有一个必须记住的细节从 numpy 数组构造 QImage 时要加 copy()否则帧缓冲被下一帧覆盖后界面会出现花屏或条纹。def update_view(self, frame, stats): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape # .copy() 必须加, 否则 QImage 引用的是可复用的帧缓冲 img QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy() self.video_label.setPixmap(QPixmap.fromImage(img)) self.blink_count_label.setText(f眨眼: {stats[blink]}) self.yawn_count_label.setText(f哈欠: {stats[yawn]}) self.nod_count_label.setText(f点头: {stats[nod]}) def set_level(self, level): color {正常: #2ecc71, 轻度疲劳: #f1c40f, 重度疲劳: #e74c3c} self.level_label.setText(level) self.level_label.setStyleSheet(fbackground-color: {color[level]};)启动和停止按钮分别控制 worker 的 start() 和 running 标志。关窗口时先置 runningFalse再调用 worker.wait() 等线程退出否则会在退出时弹出线程销毁相关的警告。界面上建议再放两个阈值输入框分别控制 EAR_THRESH 和 YAWN_THRESH调试时不用改代码就能调参。这个能力在实车测试时很实用因为光照和摄像头角度变化后阈值基本都要微调。提示关闭窗口时先置 runningFalse 再 worker.wait()避免线程未结束时销毁 QThread。5. 阈值调优用录制视频回放评估眨眼检测的准确率5.1 用回放视频替代摄像头让检测结果可复现实车测试最大的问题是场景不可控同一段动作每次跑都不一样。常见做法是提前录制几段包含对照行为的视频正常驾驶 2 分钟、低头看仪表盘、刻意打哈欠、快速眨眼。检测代码只改一行把 VideoCapture(0) 换成 VideoCapture(test.mp4)检测循环完全不变。回放视频带有固定帧率参数标定和回归测试都基于同一份数据改完参数能立刻对比效果这对毕业设计的验收和答辩论据都很有价值。5.2 用人工标注文件计算 F1 值标定眨眼阈值时我会录一段包含 20 次左右眨眼的视频人工标注每一帧眼睛是否闭合存成文本标注文件。然后让检测循环输出每一帧的预测值与标注逐帧比对统计 TP、FP、FN 算出 F1。这个过程完全自动化比肉眼盯着 EAR 曲线调参收敛快得多。调 EAR_THRESH 时先以 0.02 为步长从 0.14 扫到 0.30记录每个阈值下的 F1取最大值对应的阈值作为最终配置。def evaluate(gt_list, pred_list): # gt_list: 人工标注, 1 表示该帧闭眼; pred_list: 检测结果 tp sum(1 for g, p in zip(gt_list, pred_list) if g 1 and p 1) fp sum(1 for g, p in zip(gt_list, pred_list) if g 0 and p 1) fn sum(1 for g, p in zip(gt_list, pred_list) if g 1 and p 0) precision tp / (tp fp) if tp fp else 0.0 recall tp / (tp fn) if tp fn else 0.0 # 只有 precision 和 recall 都不为 0 时, F1 才有意义 return 2 * precision * recall / (precision recall) if precision recall else 0.0F1 对阈值扫描的意义在于precision 高说明误报少recall 高说明漏检少两者在阈值变大或变小时是反向变化的F1 取最大值的位置就是当前环境下最平衡的阈值。哈欠和点头的参数也可以用同样的流程标定区别只是标注文件里记录的是“该帧是否张嘴”和“该帧是否处于低头状态”。5.3 光线处理与参数收敛顺序夜间或者逆光时关键点检测会不稳定EAR 曲线噪声明显变大。我会在灰度图进 detector 前加一步 CLAHE 对比度受限自适应直方图均衡它对暗部细节的提升比普通 equalizeHist 温和不会把噪声一起放大。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))参数标定的顺序固定为先确认帧率再标 EAR_THRESH然后标 YAWN_THRESH最后标 NOD_BASE_RATIO。一次只动一个参数每次标定复用同一段回放视频记录 F1 随阈值的变化曲线。全部参数收敛后把阈值写进一个 config.py 或 JSON 文件检测代码里只读配置不写死数值这样白天、夜间、戴眼镜三组标定结果可以并存按场景切换配置即可。本文还有配套的精品资源点击获取
返回列表