
简介一份基于计算机视觉的司机驾驶疲劳检测系统论文面向计算机视觉、图像处理方向的开发者与研究者主要解决驾驶过程中因疲劳导致注意力下降、交通事故高发的问题。内容围绕人脸识别、人眼定位与疲劳状态判定展开重点比较了OpenCV自带Haarcascades与dlib在人眼检测上的差异详述了利用68个人脸特征点中眼部特征点计算EAR值、量化眼睛张开程度的方法并给出从视频帧转灰度图、加载检测器、提取特征点到阈值判断与疲劳报警的完整系统流程。包内仅含1个PDF文档大小2.66MB内容集中、便于快速通读可直接阅读或摘录其中算法与实验设计目前已有165人学习。文档还报告了系统在不同环境下的测试结果显示检测成功率约90%并给出了总体设计图、接口设计与硬件设计思路。对需要快速了解疲劳检测原理、参考特征点提取实现或将其用于毕业设计、课程论文参考文献的读者这一PDF提供了从算法到工程实现的连贯参考与数据支撑。1. 看到「基于计算机视觉的司机驾驶疲劳检测系统」这个标题先别急着上深度学习这个标题第一眼容易把人带偏成“做个神经网络识别疲劳”。实际上它是一个非常典型的计算机视觉项目摄像头推流、人脸检测、眼睛和嘴巴的关键点定位、几何指标计算、状态机报警。这套链路并不要求你懂训练也不要求你搭深度学习框架它更考验你对图像处理和数据流的理解。适合做计算机视觉大作业、毕业设计也适合想搞清计算机视觉和机器学习区别的人拿来练手。别把它当成黑匣子拆开看就是四个环节检测到人、找到五官、量出疲劳特征、按时间窗口报警。下面按这条路走先讲为什么视觉方案是性价比最高的再给一套用 Python 和 OpenCV 就能跑起来的最小实现然后把最让人头疼的阈值和误报问题用状态机解掉最后放上我踩过几次之后攒下来的避坑清单。想直接看参数和排障的读者可以跳到第 4、5 章但建议还是先把链路读一遍后面调参时会少踩很多坑。2. 为什么疲劳检测先做几何特征而不是直接端到端分类2.1 疲劳信号有哪些视觉能拍到什么疲劳检测的输入信号大体分三类生理信号、车行为信号和视觉信号。生理信号要贴电极车行为信号要从转向扭矩、方向盘转角、车道偏移里提取这两类不是不能做而是设备成本和数据接入成本高。做课程设计或者工程验证时视觉信号是投入产出比最好的入口一个普通 USB 摄像头装在仪表台上就能连续采集驾驶员的眼睑开度、眨眼时长、打哈欠幅度和头部姿态。信号类型采集方式典型指标动手门槛生理电信号脑电/心电电极眨眼次数、脑疲劳指数高车辆行为转向传感器/车道相机方向盘修正频率、车道偏移中视觉信号普通摄像头EAR、MAR、PERCLOS、头部角度低所以大多数基于计算机视觉的司机疲劳检测系统都会选第三行。视觉能拍到的疲劳信号里最可靠的是眼睛正常驾驶时人以睁眼为主疲劳时会出现眨眼变慢、闭眼时间变长、哈欠增多。这些信号不需要用户戴任何设备用普通图像算法就能量化。视觉信号中被研究得最多的是 PERCLOS即瞳孔被眼睑遮挡超过 80% 的时间占比。工程上我们不用去精确追踪瞳孔而是用关键点算眼睛纵横比 EAR再用阈值近似闭眼状态。这也是为什么这个系统在理论上并不需要先跑一套大模型。2.2 从帧到结论检测、对齐、量测、判定的四段链路很多新手看到这个标题会想我采集一批疲劳和清醒的图片训练一个二分类模型不就行了吗这就是把计算机视觉和机器学习混为一谈了。真正的难点不在分类而在连续视频里如何把“眼睛闭着”和“低头看导航”“强光眨眼”区分开。分类器只能给你一个概率无法告诉你为什么报警也没法告诉你阈值调多少。我把系统拆成四段人脸检测从整帧里找到司机的人脸区域属于计算机视觉与目标检测里的基础子问题。关键点定位在人脸区域里回归出眼睛、嘴巴、鼻子、下巴的 68 个关键点。几何量测用 EAR、MAR 等公式把这些点变成一维疲劳特征。状态判定用滑动窗口统计闭眼比例和持续时长决定是否报警。这个拆分的好处是每一段都能单独替换和验证。人脸检测可以换成 dlib HOG、OpenCV Haar 或深度学习检测器关键点可以换成更大一点的模型几何特征可以换成头部姿态。只要第四段的接口不变前面换什么都不影响整体结构。这也是为什么做这个项目的最佳学习路线不是先啃完 CS231n 的讲义再动手而是从最小链路跑起来再逐段优化。这条链路里只有检测和关键点回归是模型疲劳判定不是机器学习问题是一组有物理意义的几何公式。理解这一点你才不会在调参失败时怀疑模型而是回头去检查输入图像质量、摄像头角度和阈值是否匹配。3. 最小可跑通实现OpenCV dlib 的人脸关键点与 EAR/MAR3.1 环境准备PyCharm/VS Code 选择与 dlib 安装先搭环境。这个项目不需要 GPU普通笔记本 CPU 就能跑。我的做法是建一个干净的虚拟环境避免把系统 Python 弄乱。python -m venv venv # Windows 用: venv\Scripts\activate # macOS/Linux 用: source venv/bin/activate pip install --upgrade pip pip install opencv-python dlib numpy scipy如果你在 Windows 上装 dlib 报错大概率是缺少 C 工具链。先补装 Visual Studio 的“使用 C 的桌面开发”组件再执行pip install cmake然后重试pip install dlib。想更省事用 Anaconda 环境直接conda install -c conda-forge dlib可以绕开编译问题。至于是选 PyCharm 还是 VS Code这个项目要反复调试摄像头和 dlib 的 shape 对象PyCharm 的变量查看窗口更直观VS Code 胜在轻量和远程部署方便。你不需要纠结学习计算机视觉需要 Visual Studio Code 和 PyCharm 安装哪个先把虚拟环境装好哪个顺手用哪个。真正卡人的是 dlib 编译不是编辑器。同时要从 dlib 官方仓库准备一个 68 点关键点模型shape_predictor_68_face_landmarks.dat放进项目的weights目录。模型是训练好的你不需要再跑训练步骤只要保证路径能正确读到。3.2 用 dlib 68 点计算眼睛纵横比 EAR疲劳检测最常引用的指标是 EAR。它用眼周 6 个点算一个比例睁眼时大约 0.25 到 0.35闭眼时降到 0.1 左右。这个比例对图像分辨率不敏感但前提是人脸基本正对摄像头如果司机侧脸左右眼各自的 EAR 会差很多这个问题在第 5 章会专门讲。import cv2 import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(weights/shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) rects detector(gray, 0) for rect in rects: shape predictor(gray, rect) for i in range(68): x shape.part(i).x y shape.part(i).y cv2.circle(frame, (x, y), 1, (0, 255, 0), -1) cv2.imshow(landmarks, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()上面这段代码先用dlib.get_frontal_face_detector()创建一个人脸检测器再用shape_predictor加载 68 点关键点模型。detector(gray, 0)的第二个参数是upsample_times0 表示不放大图像速度最快如果人脸在画面里偏小可以改成 1但帧率会下降。68 个关键点现在只是点坐标下一步要把它们按眼睛和嘴巴分组算指标。EAR 的计算函数如下from scipy.spatial import distance as dist def eye_aspect_ratio(eye_points): # eye_points 是 dlib 返回的 6 个关键点坐标 A dist.euclidean(eye_points[1], eye_points[5]) # 垂直方向1 B dist.euclidean(eye_points[2], eye_points[4]) # 垂直方向2 C dist.euclidean(eye_points[0], eye_points[3]) # 水平方向 return (A B) / (2.0 * C)关键点索引落在固定区间右眼 36 到 41左眼 42 到 47。对每一帧左右眼各算一次 EAR取平均值。取平均是为了抵消左右脸光照不均带来的单眼误差。注意eye_points必须按 dlib 返回的顺序传否则 A、B 对应的点不对比例会乱。left_eye [shape.part(i) for i in range(42, 48)] right_eye [shape.part(i) for i in range(36, 42)] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0这里的 EAR 是单帧值。直接拿它和阈值比较会踩坑眨眼时 EAR 会快速下探再恢复大约 0.2 秒。如果你看到 EAR 低于阈值就报警一次正常眨眼就会触发误报。真正报警逻辑要到第 4 章用时间窗口解决。3.3 用嘴角距离计算哈欠 MAR 与头部姿态的补充判据除了眼睛嘴巴也能提供疲劳信号。哈欠出现时嘴角张开的幅度大、持续长可以用类似 EAR 的 Mouth Aspect Ratio 来量化。def mouth_aspect_ratio(shape): # 外唇轮廓48 左嘴角54 右嘴角 A dist.euclidean(shape.part(49), shape.part(59)) B dist.euclidean(shape.part(53), shape.part(55)) C dist.euclidean(shape.part(48), shape.part(54)) return (A B) / (2.0 * C)MAR 的计算逻辑和 EAR 相同只是换到嘴巴上。说话时 MAR 会周期性变化打哈欠时会出现一个明显拉高且持续 1 到 2 秒的峰值。所以单看 MAR 会误报它要和眼睛特征联用只有 EAR 也偏低或头部后仰时才把高 MAR 判为哈欠。头部姿态可以作为第三路特征。最简单的方式是记录鼻尖到下巴的矢量长度变化低头时人脸检测框变扁EAR 会不可靠。更可靠的方案到第 6 章用 solvePnP 求欧拉角。你先把概念记下当前最小实现里不要因为姿态干扰 EAR 就贸然提高阈值否则会把真疲劳信号丢掉。除了 dlib你还会在类似项目里看到 MediaPipe Face Mesh 和 OpenCV DNN Face Detector。MediaPipe 的 468 点能给出更细的眼睑曲线但实时管线在无 GPU 设备上调度更重OpenCV DNN 对侧脸更好但需要额外准备模型结构文件。对课程设计和大作业来说dlib HOG 检测器加 68 点模型是最少装配先跑通主流程再换前端是更务实的选择。4. 把单帧指标变成疲劳报警状态机、PERCLOS 和参数标定4.1 单帧阈值最容易误报改成滑动窗口第 3 章的代码能画点和输出数值但它还不能报警。单帧阈值是抖动最大的量眨眼、压缩噪声、都不能让你连续报警。正确做法是统计一段时间内的闭眼比例也就是 PERCLOS。PERCLOS 的标准定义是眼睑遮挡瞳孔的时间占比。工程实现不需要精确追踪瞳孔用 EAR 低于阈值的帧数除以总帧数即可近似。下面是完整的状态判定代码from collections import deque import time EAR_TH 0.22 MAR_TH 0.60 PERCLOS_TH 0.40 WINDOW_SIZE 90 ALARM_COOLDOWN 5 ear_queue deque(maxlenWINDOW_SIZE) mar_queue deque(maxlenWINDOW_SIZE) last_alarm_time 0 no_face_start 0 while cap.isOpened(): ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) rects detector(gray, 0) if len(rects) 0: # 没有人脸时单独计时不要直接当成闭眼 no_face_start time.time() if no_face_start 0 else no_face_start if time.time() - no_face_start 0.5: print(no face for too long) continue no_face_start 0 # 选面积最大的脸作为驾驶员 rect max(rects, keylambda r: r.width() * r.height()) shape predictor(gray, rect) left_eye [shape.part(i) for i in range(42, 48)] right_eye [shape.part(i) for i in range(36, 42)] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(shape) ear_queue.append(1 if ear EAR_TH else 0) mar_queue.append(1 if mar MAR_TH else 0) if len(ear_queue) WINDOW_SIZE: perclos sum(ear_queue) / WINDOW_SIZE mar_ratio sum(mar_queue) / WINDOW_SIZE if perclos PERCLOS_TH: cv2.putText(frame, DROWSINESS, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) if time.time() - last_alarm_time ALARM_COOLDOWN: print(alarm: perclos%.2f % perclos) last_alarm_time time.time()这里没有把“没人脸”算进 PERCLOS是因为低头或转身时 EAR 不可信。如果硬算闭眼会让误报暴涨。实际系统里我一般允许 0.5 秒的人脸丢失超过之后才认为异常。rect max(rects, keylambda r: r.width() * r.height())是选面积最大的人脸避免副驾人脸比驾驶员更大时抢走检测结果。参数含义EAR_TH0.22是闭眼判定阈值MAR_TH0.60是哈欠判定阈值WINDOW_SIZE90在 30fps 下代表 3 秒窗口PERCLOS_TH0.40表示窗口内闭眼比例超过 40% 就报警。这个组合是针对摄像头放在仪表台、人脸大约占画面高度一半的场景标定的不是通用答案。4.2 需要调的核心参数表与初始值参数初始值作用调参方向EAR_TH0.22闭眼判定阈值常戴墨镜或眼睛小调低到 0.18睁眼时仍低于 0.3 要检查摄像头仰角MAR_TH0.60哈欠判定阈值说话幅度大调到 0.65避免普通说话触发PERCLOS_TH0.403 秒窗口内闭眼比例严格场景调到 0.30宽松场景调到 0.50WINDOW_SIZE90 帧统计窗口长度30fps 下 90 等于 3 秒低帧率要按 fps 换算ALARM_COOLDOWN5 秒报警后静默时间防止连续响按测试习惯调整不要直接照搬。每个人的 EAR 基线不同单眼皮、双眼皮、眯眼习惯都会影响。稳妥做法是系统启动后做 10 秒校准取正常睁眼 EAR 的 25% 分位作为阈值底价。这里的几何偏置在于摄像头安装位置镜头与司机眼睛齐平时 EAR 最准确如果摄像头过低闭眼和低头都会表现为 EAR 下降需要单独区分。4.3 报警逻辑与日志记录报警逻辑不要写成一触发就结束要用冷却时间和解除条件。司机闭眼 2 秒报警 1 秒可能还不够让人清醒如果报警后 EAR 恢复正常系统应立即复位等下一次疲劳事件再触发。state NORMAL last_alarm 0.0 alarm_count 0 # 在每帧 PERCLOS 算完后做状态迁移 if perclos PERCLOS_TH: if state NORMAL and time.time() - last_alarm ALARM_COOLDOWN: state ALARM last_alarm time.time() alarm_count 1 with open(fatigue_log.csv, a, encodingutf-8) as f: f.write({},alarm,perclos{:.3f}\n.format( time.strftime(%Y-%m-%d %H:%M:%S), perclos)) elif perclos PERCLOS_TH * 0.6: state NORMALperclos PERCLOS_TH * 0.6是一个滞回条件。只用perclos PERCLOS_TH会让系统在阈值边界反复抖动加 0.6 倍系数后一旦进入报警状态就需要更长时间才能解除行为更像报警器而不是比较器。日志文件建议固定写时间、EAR、MAR、PERCLOS 和报警标识。这样后期做误报分析时不用靠回放视频猜当时的数值。日志里再额外写一帧压缩图更容易复查但注意别写太频繁否则 CPU 和磁盘都会被拖累。5. 避坑/常见问题/排查从摄像头装不上到夜间误检的五个血泪经验这一章写的是这套系统从零到位最常遇到的五个坑每条按现象、原因、解决三个步骤讲。5.1 摄像头打不开或整个画面卡在 5fps现象cap.read()一直返回 False或者画面能出但延迟严重dlib 圈脸要等 1 秒以上。原因摄像头被其他软件占用Windows 下 OpenCV 默认用 MSMF 时容易和部分摄像头驱动打架dlib HOG 检测器在 720p 全尺寸上逐帧跑CPU 扛不住。解决Windows 上把采集后端改成 DirectShowcap cv2.VideoCapture(0, cv2.CAP_DSHOW) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30)把分辨率降到 640x480 后HOG 的耗时能少一半以上。如果还慢不要每帧都做人脸检测可以每隔 2 到 3 帧检测一次中间帧用上一帧的 rect 直接算关键点。这个优化在低速演示时几乎无感但对笔记本 CPU 来说能稳定保住实时性。5.2 侧脸和低头时关键点检测失效现象司机向右看或低头时 EAR 突然变小系统误报警。原因dlib 的 HOG frontal detector 对非正面人脸几乎没有召回68 点模型在极端角度下点会塌缩到眼窝边缘EAR 失去物理意义。这就是前面说的几何偏置问题不是司机闭眼是你在用正脸公式量一个偏转脸。解决只在人脸框宽高比接近合理范围时算 EAR当关键点找不到时不要用上一帧的 EAR 硬猜。更稳妥的做法是加一个头部姿态角估计低头超过一定角度就切换到姿态判据而不是继续用眼睛。如果你在项目里已经有深度学习目标检测环境可以把前端换成 OpenCV DNN 人脸检测器侧脸召回会好不少。5.3 夜间/逆光下 EAR 数值乱跳现象室内光线正常到了模拟夜间或者逆光窗口EAR 平均值从 0.28 掉到 0.18画面里明明睁着眼。原因低照度让关键点回归产生噪声眼白和皮肤边缘不稳定。关键点一抖水平方向宽度变化不大垂直方向距离却大幅变小EAR 比值被放大。解决先做直方图均衡化gray cv2.equalizeHist(gray)再把 EAR 做指数滑动平均smoothed_ear smoothed_ear * 0.9 ear * 0.1这两个手段能让夜间误报减少一大半。如果还要更可靠只能上红外补光让图像亮度与白天对齐。很多实际部署系统用红外摄像头不只是为了隐蔽更多是因为红外光源不受路灯和车灯影响。5.4 阈值调来调去仍然漏检现象把 EAR_TH 从 0.2 调到 0.3误报多调回 0.18连续闭眼 4 秒都不报警。原因你调的是单帧阈值不是系统行为。更有可能是 WINDOW_SIZE 太长或 PERCLOS_TH 太高导致短时间闭眼被平均掉也可能是这个司机天生 EAR 基线就低0.2 对他来说已经是睁眼状态。解决不要手工乱试做一个 10 秒睁眼校准。采集前 10 秒的正常 EAR 列表取中位数baseline sorted(ear_samples)[len(ear_samples) // 2] EAR_TH max(0.18, baseline * 0.75)这行代码保留了 0.18 的下限避免基线特别低的人总是报警同时给闭眼留出余量。校准完成后再开检测能明显减少把眯眯眼当闭眼的漏检。不同司机之间参数差异真的很大所以这个系统如果要做成多人轮流使用最好加一个人脸 ID 和参数绑定。5.5 dlib 安装失败与模型文件路径问题现象pip install dlib在 C 编译时报红一大片或者代码一启动就提示 couldnt open shape_predictor_68_face_landmarks.dat。原因dlib 需要 CMake 和 C 工具链Python 版本太新时经常没有现成 wheel模型文件用相对路径当前工作目录不在 weights 目录下自然找不到。解决优先用 Anaconda 的 conda-forge 安装 dlib这是计算机视觉入门里失败率最低的做法路径改成绝对路径或先切换工作目录再加载。文件名也别放中文路径OpenCV 和 dlib 对中文路径的支持时好时坏这一个问题能浪费掉半天时间。6. 从能跑通到能演示三个进阶技巧与验收方法当你有了一套能输出 PERCLOS 的代码下一步是让它在演示时好看、在误差分析时可解释。这里讲三个收益最高的改动。6.1 先做人脸校正EAR 会立刻变得可判读司机不会一直正对镜头。我建议在关键点出来后用左右眼角的直线计算旋转角把脸先转正再算 EAR。这个操作对睁眼状态的基线影响不大但闭眼时眼窝点更容易抖动转正后 EAR 的下探会更干净。left_eye_center ((shape.part(36).x shape.part(39).x) // 2, (shape.part(36).y shape.part(39).y) // 2) right_eye_center ((shape.part(42).x shape.part(45).x) // 2, (shape.part(42).y shape.part(45).y) // 2) angle math.degrees(math.atan2( right_eye_center[1] - left_eye_center[1], right_eye_center[0] - left_eye_center[0])) center (frame.shape[1] // 2, frame.shape[0] // 2) M cv2.getRotationMatrix2D(center, angle, 1.0) rotated cv2.warpAffine(frame, M, (frame.shape[1], frame.shape[0]))注意旋转后原人脸框也跟着转关键点也要坐标变换否则预测点还是落在原图上。角度小于 5 度时可以把整帧旋转后重新做人脸检测角度太大时重新检测容易丢脸不如直接用旋转后的关键点做 EAR。6.2 用 solvePnP 估计头部姿态补充低头判据眼睛特征会在低头时失效头部姿态反而最可靠。用 68 点里的鼻尖、下巴、左右外眼角、左右嘴角六个点和毫米级的人脸平均模型做对应可以解出俯仰角。model_points np.array([ (0.0, 0.0, 0.0), # 鼻尖 (0.0, -63.6, -12.5), # 下颏 (-43.3, 32.7, -26.0), # 左外眼角 (43.3, 32.7, -26.0), # 右外眼角 (-28.9, -28.1, -24.1), # 左嘴角 (28.9, -28.1, -24.1), # 右嘴角 ], dtypefloat64) image_points np.array([ shape.part(30), shape.part(8), shape.part(36), shape.part(45), shape.part(48), shape.part(54) ], dtypefloat64) success, rvec, tvec cv2.solvePnP( model_points, image_points, camera_matrix, dist_coeffs) R, _ cv2.Rodrigues(rvec) # 从旋转矩阵里按你采用的欧拉角顺序取俯仰角分量model_points的三维坐标来自典型人脸平均模型camera_matrix需要标定。没有标定的话可以先拿近似相机参数跑通流程但出来的角度有偏差不建议直接写进论文。几何偏置问题在这里最明显摄像头装在方向盘右侧和装在仪表台中间同一低头动作的 pitch 角度差会很大。6.3 验收方法录一段模拟疲劳视频代替真路测很多同学做这个系统最后栽在验收没有真实司机疲劳数据。我的做法是自己固定手机摄像头故意做三种动作正常驾驶看前方、频繁眨眼但持续睁眼、闭眼 3 秒模拟困意各录 60 秒。然后用系统回放这段视频统计三件事正常段的误报次数、模拟疲劳段的漏报次数、报警延迟。把结果画成时间线一眼就能看出阈值该往哪个方向调。如果你有第二个摄像头可以同时录司机画面和方向盘画面输出 PERCLOS 变化曲线验证疲劳报警是否正好落在方向盘开始偏移之前。这个回放验收方法还能帮你判断 dlib 检测器在侧脸、墨镜、逆光下的表现省去大量真车测试的时间。这套做法不算新东西但我每次重新搭都会在这几个点上花最多时间。希望帮到你。本文还有配套的精品资源点击获取