
简介面向计算机视觉与图像处理方向的学习者及相关专业学生一份PDF技术文献系统介绍基于计算机视觉的司机驾驶疲劳检测方案可作为课程设计、毕业设计或相关研究的参考。内容涵盖人脸识别与人眼定位算法设计、基于dlib库的六十八个脸部特征点提取、眼部纵横比EAR值计算以及基于闭眼阈值与连续帧计数的疲劳判定流程实验结果显示系统成功率高达百分之九十。资源包内为单个PDF文档文件压缩包大小约2.66MB十分便于离线阅读截至目前已有165人浏览学习。文中还给出了从视频帧预处理、人脸检测到EAR判断的完整实现步骤并讨论了不同环境下检测可信度及局限性阅读后可快速把握疲劳驾驶检测系统的设计与实现要点为后续改进提供参考。1. 基于计算机视觉的司机驾驶疲劳检测为什么我要拆这份论文做计算机视觉大作业的同学十有八九会搜到吉林大学黄永平老师这篇《基于计算机视觉的司机驾驶疲劳检测系统》。论文思路很清晰先用 dlib 检测人脸 68 个特征点再用其中 36~47 号眼周特征点计算眼睛纵横比 EAR通过 EAR 低于阈值的持续帧数判断司机是否闭眼疲劳。系统实验室内成功率能到 90% 上下但论文里也承认实验室环境光线稳定、驾驶员正对镜头一到实际道路场景就会暴露不少问题。这份资源适合两类人一是做 CV 课程设计、需要一套能跑通并写进报告的技术路线二是想入门人脸关键点检测、想搞懂 EAR 阈值怎么标定的初学者。我将从选型对比、特征点坐标、EAR 算法推导、代码落地到参数坑位逐一拆解。2. 从 haarcascades 到 dlib人脸检测器的选型理由与特征点坐标解读2.1 为什么 haarcascades 在人眼闭合时直接翻车论文开头提到先用了 OpenCV 自带的 haarcascades 包做人脸检测和人眼定位效果不理想光线好、眼睛睁大时勉强能用一旦眼睛闭合程度大、人眼区域在画面里占比小就经常定位不到眼睛甚至人脸都检测失败。这个现象我复现过原因在于 haar 特征本质是基于像素灰度差异的弱分类器级联它对睁开的眼睛这种有明确纹理对比的区域敏感而闭眼状态下眼睑纹理被皮肤褶皱替代灰度梯度特征消失分类器自然失效。另外 haar 检测器输出的是矩形包围盒不给关键点坐标你拿到眼睛框后还得自己从框内继续找瞳孔或眼睑位置多做一步就多一个误差源。所以论文选 dlib 是合理的dlib 的 68 点人脸关键点检测器基于 HOG 特征和线性分类器加上形状回归的级联思想对局部纹理变化更鲁棒且直接输出每个关键点的 (x, y) 坐标省掉了二次定位的麻烦。2.2 68 点模型中 36~47 号点的位置含义与坐标提取dlib 的 68 点模型把面部关键区域划分为下颌轮廓 0~16左眉 17~21右眉 22~26鼻梁 27~30鼻翼 31~35左眼 36~41右眼 42~47嘴巴 48~67。注意论文里写36-47 为左右眼的特征点严格讲是左眼 36~41、右眼 42~47。每个点是按固定语义顺序输出的左眼 36 是外眼角、39 是内眼角37、38 是上眼睑、40、41 是下眼睑。import dlib import cv2 # 加载预训练模型shape_predictor_68_face_landmarks.dat 需要另行下载 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) img cv2.imread(driver_face.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) # 打印左眼6个点坐标索引36~41 for i in range(36, 42): pt shape.part(i) print(fPoint {i}: ({pt.x}, {pt.y})) # 打印右眼6个点坐标索引42~47 for i in range(42, 48): pt shape.part(i) print(fPoint {i}: ({pt.x}, {pt.y}))detector(gray, 0)的第二个参数 0 表示不使用图像金字塔上采样检测速度快但小脸可能漏检如果摄像头离人较远、人脸像素少改成 1 或 2 会增加一次上采样能多检出小脸但耗时翻倍。predictor接收灰度图和检测到的人脸框输出 68 个点的坐标集合。这里有个经验输入图像分辨率不要太低dlib 官方建议人脸区域至少 80x80 像素否则关键点回归精度会明显下降。拿到坐标后别直接拿去做距离计算最好先按人脸框大小做归一化。因为不同人离摄像头远近不同同一双眼睛在画面里的绝对像素距离可以差一倍以上直接比绝对值没有意义。EAR 算法的妙处就在于它是一个比值分子分母同时缩放距离影响被抵消了。2.3 论文没细说的特征点检测失败的常见输入原因用 dlib 时最容易翻车的不是算法本身而是输入图像质量。灰度转换是必须的dlib 的 HOG 检测器在灰度图上提取梯度特征彩色图会被内部转灰度提前转换能省一次转换开销。光照过暗时梯度信息弱检测器可能整个人脸都框不出来强侧光会在面部形成大块阴影区关键点回归容易偏移。我实测过当人脸偏航角超过 45 度即司机转头看右侧后视镜时68 点模型会在可见的半张脸上输出全部 68 个点其中被遮挡侧的点坐标基本是猜的计算出的 EAR 值完全不可信。提示实际做车载场景时优先保证正脸采样。摄像头装在方向盘前仪表盘位置比装在 A 柱更利于关键点检测。3. 核心算法 EAR眼睛纵横比的计算逻辑与闭眼判定原理3.1 从眼周 6 点到纵横比公式的推导论文的疲劳判定核心是 EAR全称 Eye Aspect Ratio眼睛纵横比。睁眼时上下眼睑距离大闭眼时上下眼睑几乎贴合通过计算上下眼睑特征点之间的垂直距离与内外眼角水平距离的比值就能得到一个对距离不敏感、对睁闭眼状态敏感的量。左眼的 6 个特征点编号为 36~41定义如下p36 为左外眼角p39 为左内眼角p37、p38 为上眼睑左右两点p40、p41 为下眼睑左右两点。EAR 公式为EAR (||p37 - p41|| ||p38 - p40||) / (2 * ||p36 - p39||)分子是两条垂直方向上的眼睑距离之和分母是眼角水平距离的两倍。睁眼时分子较大EAR 通常在 0.25~0.35 之间闭眼时分子趋近于零EAR 会掉到 0.1 以下。论文提到一般睁眼时上下特征点距离较大闭眼时上下距离较小说的就是这个几何关系。两只眼睛分别计算后取平均能抵消单眼误检带来的抖动。3.2 完整可运行的 EAR 检测代码与逐行参数说明import dlib import cv2 import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def eye_aspect_ratio(eye_points): # eye_points 是由6个 (x, y) 坐标组成的数组顺序为外眼角到内眼角环绕 p2_p6 np.linalg.norm(eye_points[1] - eye_points[5]) p3_p5 np.linalg.norm(eye_points[2] - eye_points[4]) p1_p4 np.linalg.norm(eye_points[0] - eye_points[3]) ear (p2_p6 p3_p5) / (2.0 * p1_p4) return ear cap cv2.VideoCapture(0) frame_count 0 ear_sum 0.0 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) left_eye [] right_eye [] for i in range(36, 42): pt shape.part(i) left_eye.append((pt.x, pt.y)) for i in range(42, 48): pt shape.part(i) right_eye.append((pt.x, pt.y)) left_ear eye_aspect_ratio(np.array(left_eye, dtypenp.float64)) right_ear eye_aspect_ratio(np.array(right_eye, dtypenp.float64)) ear (left_ear right_ear) / 2.0 cv2.putText(frame, fEAR: {ear:.2f}, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()eye_aspect_ratio函数里eye_points[0]是外眼角、eye_points[3]是内眼角这是因为 dlib 输出顺序固定为顺时针环绕眼睛索引 0 对应 36 外眼角索引 3 对应 39 内眼角。np.linalg.norm计算两点间欧氏距离p2_p6对应上眼睑点 37 到下眼睑点 41 的距离p3_p5对应 38 到 40 的距离这两个都是垂直方向。p1_p4是水平方向的内外眼角距乘以 2 是为了让睁眼时的 EAR 值落在 0.25~0.35 这个便于比较的区间。实时视频流里我一般直接取相邻帧的 EAR 值做均值滤波比如滑动窗口取最近 5 帧的平均可以有效抑制单帧抖动。但注意窗口别太大否则闭眼瞬间的 EAR 变化会被平滑掉导致漏检。3.3 EAR 阈值与连续帧判定论文流程的两处关键参数论文的判定流程是EAR 小于闭眼阈值则计数器加一否则计数器清零当连续帧数大于疲劳阈值时发出警告。这里有两个参数需要标定一个是闭眼阈值另一个是疲劳阈值即连续多少帧判定为疲劳。闭眼阈值我一般取 0.2~0.25 之间。太大会把正常眨眼误判为闭眼太小则闭眼不彻底时检不出来。论文提到眨眼时眼睛的宽度会迅速下降到零但实际视频流里受帧率和运动模糊影响EAR 很难降到零通常闭眼瞬间 EAR 在 0.1 左右睁眼在 0.3 左右所以阈值取 0.2 是比较稳妥的分界线。疲劳阈值和帧率直接相关30fps 下闭眼 0.5 秒就是 15 帧所以连续帧阈值取 15~20 比较合理。如果摄像头帧率只有 15fps同等闭眼时长对应帧数减半疲劳阈值要相应下调。4. 从视频流到疲劳报警系统实现的完整流程与边界条件4.1 六步处理流程的代码化落地论文 3.1 节给出的流程可以概括为灰度转换、加载检测器、检测人脸、提取眼部特征点、计算 EAR、阈值比较与计数器累加。用代码把这几步串起来加上报警逻辑就是一个最小可用的疲劳检测系统。import dlib import cv2 import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) EYE_AR_THRESH 0.20 EYE_AR_CONS_FRAMES 15 frame_counter 0 alarm_on False def eye_aspect_ratio(eye): A np.linalg.norm(eye[1] - eye[5]) B np.linalg.norm(eye[2] - eye[4]) C np.linalg.norm(eye[0] - eye[3]) return (A B) / (2.0 * C) cap cv2.VideoCapture(driver_video.mp4) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) if len(faces) 0: cv2.putText(frame, No face, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) for face in faces: shape predictor(gray, face) left_eye np.array([(shape.part(i).x, shape.part(i).y) for i in range(36, 42)], dtypenp.float64) right_eye np.array([(shape.part(i).x, shape.part(i).y) for i in range(42, 48)], dtypenp.float64) ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 if ear EYE_AR_THRESH: frame_counter 1 if frame_counter EYE_AR_CONS_FRAMES: alarm_on True cv2.putText(frame, FATIGUE ALERT!, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) else: frame_counter 0 alarm_on False cv2.putText(frame, fEAR: {ear:.2f} Frames: {frame_counter}, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()EYE_AR_THRESH和EYE_AR_CONS_FRAMES分别对应论文中的闭眼阈值和疲劳阈值。计数器逻辑是关键每帧 EAR 小于阈值就累加直到达到疲劳阈值才报警这样一次快速眨眼只有三四帧低于阈值不会触发报警而真正打瞌睡时闭眼会持续半秒以上必然突破连续帧阈值。这里报警只做了画面提示实际车载系统可以接蜂鸣器或方向盘震动模块。4.2 接口与硬件设计论文图 4 的实际含义论文 3.3 节提到接口设计虽然没有给出详细电路图但从系统架构能推断出基本的硬件链路USB 摄像头采集模拟视频信号通过 USB 接口传入车载工控机或嵌入式板卡板卡上的 OpenCV/dlib 处理完帧数据后通过 GPIO 或串口输出报警信号给蜂鸣器或震动座椅。我重新搭过一套类似的用的树莓派 4B 加 USB 摄像头处理 640x480 分辨率视频能达到 20fps 左右EAR 计算耗时约 4ms瓶颈在 dlib 的人脸检测器上。嵌入式部署有一个容易被忽视的问题dlib 模型文件约 60MB加载进内存后树莓派这类设备的内存占用会偏高。解决办法是换用 OpenCV 的 DNN 模块加载小型人脸检测模型或者干脆用 MediaPipe 的 Face Mesh它在嵌入式设备上更轻量。但论文场景下的核心算法逻辑是通用的换检测器不影响 EAR 计算与疲劳判定部分。4.3 论文没提但必须考虑的多人脸、侧脸与遮挡论文实验环境是单人正对摄像头实际驾驶场景会出现副驾驶有人、司机转头、手部遮挡面部。dlib 检测器返回的faces是一个列表代码里for face in faces会遍历所有人脸如果副驾人脸更靠近镜头可能先被处理并占用 EAR 计算资源。需要加一个人脸排序逻辑取画面中面积最大的脸作为驾驶员。侧脸场景更麻烦当偏航角超过 30 度时左右眼特征点会重叠计算出的 EAR 值变得极小可能持续低于阈值系统误报疲劳。处理方法是用人脸姿态估计筛选有效帧我一般用 OpenCV 的solvePnP配合 68 点中的鼻尖、下巴、左右眼角做姿态解算偏航角超过 30 度的帧直接跳过不参与 EAR 统计。5. 避坑与常见问题我用这份论文方案时踩过的五个坑5.1 帧率不匹配导致连续帧阈值失效现象按论文参数设置EYE_AR_CONS_FRAMES 15在低帧率摄像头下正常眨眼都被报警。原因15 帧这个数字只有在 30fps 下才对应闭眼 0.5 秒。15fps 摄像头下 15 帧已经是闭眼 1 秒了反而容易漏报如果帧率只有 10fps15 帧对应 1.5 秒漏报更严重。反之帧率 60fps 时 15 帧仅对应 0.25 秒正常眨眼就可能触发报警。解决处理前先读取cap.get(cv2.CAP_PROP_FPS)把连续帧阈值换算成时间按int(fps * 0.5)方式动态设置。从那以后我每次接入新摄像头第一件事就是打印帧率再决定阈值取多少。5.2 光照突变导致 EAR 曲线出现断崖式下跌现象车辆驶出隧道瞬间画面亮度骤变EAR 值突然从 0.3 掉到 0.1触发报警。原因dlib 关键点检测对光照变化敏感光线突变时眼睑特征点回归不稳定上下眼睑点可能收敛到同一位置导致分子趋近于零。解决对 EAR 序列做低通滤波我用的是滑动窗口均值窗口大小取 5~7 帧。光照突变引起的异常 EAR 通常只持续一两帧均值滤波后会被平抑掉。同时可以检测画面整体亮度变化率变化超过 30% 时短暂冻结疲劳判定等光线稳定后再恢复。5.3 眼镜反光让上眼睑点漂移现象佩戴反光较强的镜片时左眼 EAR 正常但右眼 EAR 长期偏低系统频繁报警。原因眼镜片反射的灯光或阳光会在眼周区域形成高光HOG 特征提取时高光区域梯度极大关键点回归被高光吸引上眼睑点被拉到镜片反光位置。解决先用 Haar 检测眼镜区域或者对眼部 ROI 做直方图均衡化压低高光影响。更简单的做法是直接提高闭眼阈值到 0.22并加大连续帧阈值到 20牺牲一点灵敏度换取稳定性。论文没提眼镜问题但在中国驾驶员里戴眼镜的比例很高这一条不做肯定翻车。5.4 人脸检测器在低分辨率下漏检现象摄像头分辨率设为 320x240 时人脸稍远就检测不到程序直接输出No face。原因dlib 的 HOG 人脸检测器要求人脸区域至少 80x80 像素320x240 画面中人的头部可能只有 60x60 像素。解决把detector(gray, 0)改成detector(gray, 1)开启一次图像金字塔上采样相当于把图像放大一倍再检测小脸也能框出来。代价是每帧耗时从约 15ms 涨到约 30ms。另一个方案是缩小输出画面但不缩小处理画面即摄像头采集 640x480检测时用完整分辨率显示时才缩放。5.5 连续帧计数器没有设置上限现象司机确实闭眼睡着了系统报警一次后EAR 一直低于阈值计数器持续累加报警逻辑反复触发。原因计数器无上限时闭眼 10 秒和闭眼 1 秒的最终结果一样都是超过阈值后报警但无法区分轻微疲劳和深度睡眠。解决为计数器设置一个最大值比如frame_counter min(frame_counter, EYE_AR_CONS_FRAMES 30)达到最大值后保持报警状态但不再累加。更进阶的做法是分两级判定连续 15 帧触发一级警告连续 40 帧触发二级强警告对应论文里闭眼或眯眼时间过长的表述。6. 进阶把 PERCLOS 指标引入系统替换简单的连续帧计数论文用连续帧计数判断疲劳这在工程上过于粗暴。真实驾驶场景中司机可能不会一次性闭眼超过 0.5 秒而是频繁出现微闭眼每次只持续 0.2~0.3 秒。这类情况用连续帧阈值完全检测不到。我在论文方案基础上引入 PERCLOS 指标即单位时间内眼睛闭合时间占比这是交通心理学研究里公认的疲劳度量。具体做法是计算最近 60 秒内眼睛闭合帧数占总帧数的比例。统计窗口用 60 秒闭眼帧定义为 EAR 小于 0.2 的帧。当 PERCLOS 超过 0.15即每分钟闭眼累计 9 秒以上判定为疲劳状态。这个指标对频繁短闭眼更敏感因为它是统计量而不是连续量。实现上只需要在原有 EAR 循环里增加一个环形缓冲区from collections import deque ear_history deque(maxlen1800) # 30fps下60秒的帧数 closed_frames 0 window_frames 0 while True: # 原有EAR计算逻辑... ear_history.append(ear) if len(ear_history) 1800: closed_frames sum(1 for e in ear_history if e 0.20) perclos closed_frames / 1800 if perclos 0.15: cv2.putText(frame, FATIGUE (PERCLOS), (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 3) ear_history.clear()deque(maxlen1800)自动丢弃最老的数据帧保持窗口长度为 60 秒省去手动管理列表头尾的麻烦。perclos 0.15这个阈值参考了驾驶疲劳研究的常模但实际使用时需要根据目标人群微调经常熬夜的司机的正常闭眼频率本身就高阈值要放宽到 0.18 左右。验证这个进阶方案是否有效你可以录制一段 5 分钟视频前 3 分钟正常开车状态后 2 分钟模拟频繁短闭眼。用原有连续帧逻辑大概率报不出警换 PERCLOS 后能稳定检出。从那以后我做人脸疲劳检测默认就是连续帧阈值加 PERCLOS 双通道并行判定单一指标太容易出边界 case。希望帮到你。本文还有配套的精品资源点击获取