简介:这份资源是一套基于Python开发的驾驶员面部特征疲劳检测系统源码,面向计算机相关专业学生、毕业设计选题者以及需要人脸识别与状态监测方案的开发者。项目通过摄像头采集驾驶员面部信息,识别其疲劳状态并判断是否需要休息,可迁移至交警监控、高速收费站等场景,用于排查疲劳驾驶行为。压缩包共24个文件,约68.33MB,包含5个py核心脚本、10个xml配置与界面文件、iml与gitignore等工程配置、md说明文档、docx文档、dat数据文件及mp3提示音等,覆盖从模型调用到界面交互的完整流程。目前已有1358人学习下载,说明该方案在同类选题中具备一定参考价值。源码包下载后可直接运行,目录结构清晰,便于读者快速理解人脸检测、特征提取与疲劳判定的实现思路,并在此基础上进行功能扩展或二次开发,适合作为毕业设计原型或课程实践项目。
1. 从一张摄像头截图说起:疲劳检测到底在检测什么
你打开电脑摄像头,画面里是一张普通的人脸。系统要在几百毫秒内判断:这个人是不是困了。这不是玄学,而是把面部特征转成可量化的数字——眼睛睁多大、嘴巴张多开、头低了多少度——再和阈值比对。标题里的“Python基于驾驶员面部特征的疲劳检测系统源码”,本质就是一套用 Python 把摄像头画面变成疲劳分数的流水线。它适合做毕业设计的学生、想入门计算机视觉的开发者,以及需要给车载或工位场景加一层轻量预警的工程师。核心链路只有四步:人脸检测、关键点定位、特征计算、疲劳判定。每一步都有现成库,但参数和阈值才是决定这套系统能不能用的关键。下面我把这条链路拆开,从环境搭到调参,再到踩过的坑,全部讲清楚。
2. 环境搭建与依赖选型:为什么是 OpenCV + dlib 而不是别的
2.1 三个库的分工与版本选择
这套系统最稳的组合是 OpenCV 负责读摄像头和画框,dlib 负责 68 点人脸关键点,NumPy 负责算距离和比例。不推荐用 MediaPipe 替代 dlib 做毕业设计,原因有两个:一是 dlib 的 68 点模型输出稳定,论文里好写公式;二是 MediaPipe 的 API 变动频繁,你照着半年前的教程跑很可能报错。Python 版本选 3.8 到 3.10 之间,3.11 以上 dlib 的预编译包不好找,自己编译要装 CMake 和 Visual Studio Build Tools,对新手不友好。
安装顺序很重要,先装 NumPy 再装 OpenCV,最后装 dlib。dlib 在 Windows 上直接 pip install dlib 大概率失败,常见做法是下载对应的 .whl 文件本地安装。下面是一段可复现的安装命令,假设你用的是 Windows + Python 3.9。
# 先升级 pip,避免旧版解析 wheel 出错 python -m pip install --upgrade pip # 安装基础库,指定版本避免兼容问题 pip install numpy==1.23.5 pip install opencv-python==4.7.0.72 # dlib 不要直接 pip install dlib,先下 whl # 去 dlib 官方或可信镜像找 dlib-19.24.0-cp39-cp39-win_amd64.whl pip install dlib-19.24.0-cp39-cp39-win_amd64.whl # 验证三个库都能导入 python -c "import cv2, dlib, numpy; print(cv2.__version__, dlib.__version__, numpy.__version__)"逻辑说明:NumPy 是 dlib 和 OpenCV 的底层依赖,先装它能让后续库直接复用。OpenCV 用 4.7 版本是因为它的 VideoCapture 在 Windows 上对多数摄像头兼容性最好。dlib 用 whl 安装绕开编译,省去装 Visual Studio 的麻烦。参数上,numpy 1.23.5 和 opencv-python 4.7.0.72 是经过验证不冲突的组合,你如果装最新版可能会遇到 dlib 导入时报 DLL 缺失。
2.2 摄像头初始化与分辨率设置
很多教程直接 cv2.VideoCapture(0) 就完事,但实际跑起来会发现帧率不稳、画面延迟。我一般会显式设置分辨率和缓冲区大小。分辨率不是越高越好,640x480 足够做面部特征检测,再高只会拖慢帧率。下面这段代码是摄像头初始化的最小可用版本。
import cv2 # 打开默认摄像头,0 表示第一个设备 cap = cv2.VideoCapture(0) # 设置分辨率,640x480 是疲劳检测的甜点值 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 减少缓冲区,降低画面延迟 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 检查是否成功打开 if not cap.isOpened(): raise RuntimeError("摄像头打开失败,检查是否被其他程序占用") while True: ret, frame = cap.read() if not ret: break # 水平翻转,符合照镜子习惯 frame = cv2.flip(frame, 1) cv2.imshow("frame", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:CAP_PROP_BUFFERSIZE 设为 1 是关键,默认缓冲区可能缓存好几帧,导致你看到的画面比实际慢半秒。翻转画面是为了让用户操作更自然,不影响检测结果。参数上,640x480 下 dlib 的 68 点检测单帧大约 30 到 50 毫秒,普通 CPU 就能跑到 15 帧以上,满足实时性。如果你用 1280x720,检测时间会翻倍,帧率掉到 8 帧以下,疲劳判定就会滞后。
2.3 dlib 关键点模型加载与首次运行
dlib 需要单独下载 shape_predictor_68_face_landmarks.dat 模型文件,大约 100MB。这个文件不在 pip 包里,要去 dlib 官网下载。加载模型只需要一行,但首次加载会花一两秒,建议放在循环外面。
import dlib # 初始化人脸检测器和关键点预测器 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 在灰度图上检测人脸 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) # 第二个参数 0 表示不放大图像 for face in faces: landmarks = predictor(gray, face) # landmarks.part(i) 返回第 i 个点的坐标 print(landmarks.part(36).x, landmarks.part(36).y)逻辑说明:detector 的第二个参数是上采样次数,设 0 表示原图检测,速度快但小脸可能漏检;设 1 会放大一倍,检测更稳但耗时增加。毕业设计场景下人脸通常占画面比例较大,用 0 就够。landmarks 的 68 个点里,36 到 41 是左眼,42 到 47 是右眼,48 到 67 是嘴巴,这些索引后面算 EAR 和 MAR 要用到。
3. 面部特征计算:EAR、MAR 和头部姿态的代码实现
3.1 眼睛纵横比 EAR 的公式与代码
EAR 是 Eye Aspect Ratio 的缩写,思路很简单:眼睛睁大时上下眼睑距离大,闭眼时距离趋近于零。用六个眼睛关键点算一个比值,就能量化睁闭状态。公式是(上眼睑到下眼睑的距离之和)除以(左右眼角距离的两倍)。下面是对左右眼分别计算再取平均的代码。
import numpy as np from scipy.spatial import distance as dist def eye_aspect_ratio(eye_points): # eye_points 是 6 个 (x, y) 坐标,顺序为左角、上左、上右、右角、下右、下左 # 垂直距离:上左到上右,下左到下右 A = dist.euclidean(eye_points[1], eye_points[5]) B = dist.euclidean(eye_points[2], eye_points[4]) # 水平距离:左角到右角 C = dist.euclidean(eye_points[0], eye_points[3]) # EAR 公式,乘以 2.0 是为了归一化 ear = (A + B) / (2.0 * C) return ear # 从 landmarks 提取左右眼坐标 left_eye = [] right_eye = [] for i in range(36, 42): left_eye.append((landmarks.part(i).x, landmarks.part(i).y)) for i in range(42, 48): right_eye.append((landmarks.part(i).x, landmarks.part(i).y)) left_ear = eye_aspect_ratio(left_eye) right_ear = eye_aspect_ratio(right_eye) ear = (left_ear + right_ear) / 2.0逻辑说明:scipy 的 euclidean 算两点距离,比手写平方根更稳。EAR 的正常睁眼值在 0.25 到 0.35 之间,闭眼会降到 0.15 以下。注意左右眼要分别算再平均,因为侧脸时一只眼可能被遮挡,单眼 EAR 会失真。参数上,如果发现 EAR 整体偏低,检查关键点索引有没有错位,36 到 41 是左眼还是右眼取决于 dlib 的定义,实际跑一遍打印坐标就能确认。
3.2 嘴巴纵横比 MAR 与打哈欠判定
MAR 是 Mouth Aspect Ratio,和 EAR 思路一样,用嘴巴的垂直距离除以水平距离。打哈欠时嘴巴张开,MAR 会明显升高。代码结构和 EAR 几乎一样,只是关键点索引换成 48 到 67。
def mouth_aspect_ratio(mouth_points): # 取上下唇中间的几个点算垂直距离 # 48 是左嘴角,54 是右嘴角,51 是上唇中点,57 是下唇中点 A = dist.euclidean(mouth_points[2], mouth_points[10]) # 上唇到下巴 B = dist.euclidean(mouth_points[4], mouth_points[8]) # 上唇到下巴 C = dist.euclidean(mouth_points[0], mouth_points[6]) # 嘴角到嘴角 mar = (A + B) / (2.0 * C) return mar mouth = [] for i in range(48, 68): mouth.append((landmarks.part(i).x, landmarks.part(i).y)) mar = mouth_aspect_ratio(mouth)逻辑说明:这里用的索引是 48 到 67 里的相对位置,mouth_points[0] 对应 48,mouth_points[6] 对应 54。垂直距离取了两组,是为了减少单点抖动。MAR 正常闭嘴在 0.1 到 0.3,打哈欠能到 0.6 以上。参数上,如果 MAR 一直偏高,可能是嘴巴关键点把下巴也算进去了,检查 48 到 67 的坐标分布,正常应该集中在嘴唇周围。
3.3 头部姿态估计:用 solvePnP 算俯仰角
疲劳时人往往会低头,所以头部俯仰角是一个重要特征。OpenCV 的 solvePnP 可以用 2D 关键点和 3D 人脸模型算旋转向量,再转成欧拉角。3D 模型点我一般用六个稳定点:鼻尖、下巴、左右眼角、左右嘴角。
# 3D 人脸模型点,单位任意,比例对就行 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.9, -24.1), # 左嘴角 (28.9, -28.9, -24.1) # 右嘴角 ], dtype=np.float64) # 对应的 2D 关键点索引 image_points = np.array([ (landmarks.part(30).x, landmarks.part(30).y), # 鼻尖 (landmarks.part(8).x, landmarks.part(8).y), # 下巴 (landmarks.part(36).x, landmarks.part(36).y), # 左眼角 (landmarks.part(45).x, landmarks.part(45).y), # 右眼角 (landmarks.part(48).x, landmarks.part(48).y), # 左嘴角 (landmarks.part(54).x, landmarks.part(54).y) # 右嘴角 ], dtype=np.float64) # 相机内参,用画面宽高估算 focal_length = frame.shape[1] center = (frame.shape[1] / 2, frame.shape[0] / 2) camera_matrix = np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtype=np.float64) # 假设没有镜头畸变 dist_coeffs = np.zeros((4, 1)) success, rotation_vector, translation_vector = cv2.solvePnP( model_points, image_points, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_ITERATIVE ) # 旋转向量转欧拉角 rotation_matrix, _ = cv2.Rodrigues(rotation_vector) # 这里只取俯仰角,即绕 X 轴的旋转 pitch = np.degrees(np.arcsin(-rotation_matrix[2][1]))逻辑说明:solvePnP 需要至少四个点,我用六个是为了更稳。相机内参用焦距等于画面宽度是粗略估计,毕业设计够用,精确标定要另做。pitch 为负表示低头,正常坐姿在 -10 到 10 度之间,低于 -20 度持续几秒就可以算疲劳特征。参数上,如果 pitch 跳变厉害,检查 image_points 的顺序和 model_points 是否一一对应,顺序错一个点结果就全乱。
4. 疲劳判定逻辑与阈值调参:从单帧到时间窗口
4.1 用连续帧计数器替代单帧阈值
单帧 EAR 低于阈值不能直接判定疲劳,因为眨眼也会让 EAR 瞬间降低。常见做法是设一个计数器,连续 N 帧 EAR 都低于阈值才触发报警。N 的取值和帧率有关,15 帧下 N 取 3 到 5 比较合适,对应 0.2 到 0.3 秒。下面是一个可调参数的判定逻辑。
# 可调参数 EAR_THRESHOLD = 0.21 # 闭眼阈值 EAR_CONSEC_FRAMES = 3 # 连续帧数 MAR_THRESHOLD = 0.6 # 打哈欠阈值 PITCH_THRESHOLD = -20 # 低头角度阈值 eye_counter = 0 yawn_counter = 0 fatigue_score = 0 if ear < EAR_THRESHOLD: eye_counter += 1 if eye_counter >= EAR_CONSEC_FRAMES: fatigue_score += 1 else: eye_counter = 0 if mar > MAR_THRESHOLD: yawn_counter += 1 if yawn_counter >= 5: fatigue_score += 1 else: yawn_counter = 0 if pitch < PITCH_THRESHOLD: fatigue_score += 1 # 疲劳分数超过阈值就报警 if fatigue_score > 3: cv2.putText(frame, "FATIGUE ALERT", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) fatigue_score = 0 # 报警后重置,避免持续刷屏逻辑说明:eye_counter 在 EAR 恢复正常时清零,保证只有持续闭眼才计数。fatigue_score 是累加分数,不同特征各占一分,超过 3 分报警。参数上,EAR_THRESHOLD 是最关键的,不同人眼型差异大,戴眼镜的人 EAR 整体偏低,建议先用默认值跑一遍,打印每个人的 EAR 分布再微调。MAR_THRESHOLD 对打哈欠的判定比较宽松,因为打哈欠持续时间长,不容易误判。
4.2 阈值标定:用一段视频跑出个人基线
没有一套阈值适合所有人。我一般会让使用者先正常睁眼坐 10 秒,记录 EAR 均值,再闭眼 3 秒记录最低值,取中间值作为阈值。下面这段代码可以帮你采集基线。
import time # 采集 10 秒正常状态的 EAR baseline_ears = [] start = time.time() while time.time() - start < 10: ret, frame = cap.read() gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: landmarks = predictor(gray, face) # ... 计算 ear ... baseline_ears.append(ear) cv2.imshow("calibrate", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break mean_ear = np.mean(baseline_ears) std_ear = np.std(baseline_ears) # 阈值设为均值减去 1.5 倍标准差 EAR_THRESHOLD = mean_ear - 1.5 * std_ear print(f"基线 EAR: {mean_ear:.3f}, 建议阈值: {EAR_THRESHOLD:.3f}")逻辑说明:用均值减 1.5 倍标准差是统计上的常用做法,能覆盖大部分正常波动。如果标准差很大,说明采集时头部晃动多,建议重新采集。参数上,采集时间 10 秒是经验值,太短不稳定,太长用户不耐烦。标定完把阈值写进配置文件,下次直接读,不用每次重跑。
4.3 多特征融合的权重分配
EAR、MAR、pitch 三个特征对疲劳的贡献不一样。闭眼是最直接的信号,权重可以给 0.5;打哈欠给 0.3;低头给 0.2。加权求和后和阈值比较,比简单累加更合理。
| 特征 | 权重 | 正常范围 | 疲劳触发条件 |
|---|---|---|---|
| EAR | 0.5 | 0.25-0.35 | 连续 3 帧低于 0.21 |
| MAR | 0.3 | 0.1-0.3 | 连续 5 帧高于 0.6 |
| Pitch | 0.2 | -10 到 10 | 低于 -20 度持续 2 秒 |
逻辑说明:权重可以根据场景调,比如夜间行车闭眼更危险,EAR 权重可以提到 0.6。表格里的正常范围是统计经验值,实际用的时候以个人基线为准。注意 pitch 的触发条件加了持续时间,因为低头一瞬间可能是看仪表盘,不一定是疲劳。
5. 避坑与排查:那些让我熬夜的翻车现场
5.1 摄像头被占用导致 cap.read() 返回 False
现象:程序跑起来第一帧就退出,cap.isOpened() 返回 True 但 read() 一直 False。原因:摄像头被其他程序占用,比如微信视频、Zoom 或者另一个 Python 进程没释放。解决:关掉所有可能用摄像头的软件,在任务管理器里结束残留的 python.exe 进程。如果还不行,换一个 USB 接口,有些笔记本的内置摄像头和某些外接设备冲突。
5.2 dlib 关键点抖动导致 EAR 跳变
现象:人坐着不动,EAR 却在 0.2 到 0.35 之间来回跳,计数器频繁触发。原因:dlib 的 68 点检测本身有像素级抖动,尤其是光线不足时。解决:对 EAR 做滑动平均,取最近 5 帧的均值再判定。代码上用一个 deque 存历史值,每次算完 append 再求平均。另外保证面部光照均匀,背光会让关键点漂移。
5.3 戴眼镜时 EAR 阈值失效
现象:戴眼镜的人正常睁眼 EAR 只有 0.18,用默认阈值 0.21 会一直报警。原因:镜片反光和镜框遮挡让眼睛关键点偏移,上下眼睑距离被压缩。解决:让戴眼镜的用户单独标定基线,或者把 EAR_THRESHOLD 降到 0.15。更稳的做法是检测眼镜区域,但毕业设计没必要,标定就能解决。
5.4 多线程读取摄像头导致帧率不稳
现象:加了报警声音播放后,画面卡顿,EAR 计算滞后。原因:声音播放是阻塞操作,占用了主循环时间。解决:把声音播放放到独立线程,用 threading 模块。主循环只负责读帧和计算,报警时往队列里丢一个信号,声音线程从队列取。这样主循环帧率不受影响。
5.5 打包成 exe 后模型文件找不到
现象:PyCharm 里跑得好好的,用 PyInstaller 打包后报错找不到 shape_predictor_68_face_landmarks.dat。原因:打包时模型文件没有一起打进去,或者路径用了相对路径。解决:用 --add-data 参数把 dat 文件加进去,代码里用 sys._MEIPASS 获取临时目录路径。下面是一段兼容开发和打包的路径处理。
import sys import os def resource_path(relative_path): # 打包后 sys._MEIPASS 是临时解压目录 if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath("."), relative_path) predictor = dlib.shape_predictor(resource_path("shape_predictor_68_face_landmarks.dat"))逻辑说明:sys._MEIPASS 是 PyInstaller 运行时创建的临时目录,打包时用 --add-data "shape_predictor_68_face_landmarks.dat;." 把文件放进去。参数上,分号前面是源文件,后面是目标目录,Windows 用分号,Linux 用冒号。
6. 进阶技巧:用 EAR 历史曲线做趋势预警
前面讲的都是瞬时判定,但疲劳是一个累积过程。我后来加了一个趋势预警:把最近 30 秒的 EAR 画成曲线,如果曲线整体下移且波动变小,说明人在进入微睡眠状态,这时候即使还没触发阈值,也可以提前提醒。实现上用一个固定长度的 deque 存 EAR,每帧更新,用 matplotlib 或者 OpenCV 的折线画在画面角落。
from collections import deque # 存最近 30 秒的 EAR,假设 15 帧每秒 ear_history = deque(maxlen=450) # 每帧计算完 ear 后 ear_history.append(ear) # 计算趋势:最近 5 秒均值和前 5 秒均值比较 if len(ear_history) > 150: recent = np.mean(list(ear_history)[-75:]) previous = np.mean(list(ear_history)[-150:-75]) if recent < previous * 0.85: cv2.putText(frame, "DROWSINESS TREND", (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 165, 255), 2)逻辑说明:deque 的 maxlen 自动丢弃旧数据,不用手动清理。recent 和 previous 各取 75 帧,对应 5 秒。比值 0.85 是经验值,表示 EAR 下降了 15% 以上。这个趋势预警比瞬时阈值早 2 到 3 秒发现疲劳,给驾驶员留出反应时间。参数上,如果你的帧率不是 15,按实际帧率调整窗口大小,公式是 帧率乘以秒数。
验证这套系统好不好用,我一般会做两个测试:一是正常看视频 10 分钟,看误报次数;二是模拟疲劳,闭眼 3 秒、打哈欠 5 次、低头 10 秒,看能不能全部触发。误报超过 3 次就调高阈值,漏报就调低。最后说一个血泪经验:不要用网上的默认阈值直接交毕业设计,答辩老师让你现场演示,换个人脸可能就翻车。花 10 分钟做个人基线标定,比调一天参数都管用。希望帮到你。
本文还有配套的精品资源,点击获取