简介:这是一份面向毕业设计、计算机视觉与嵌入式交叉方向学习者的Python版驾驶员疲劳检测系统源码。系统基于OpenCV与Dlib实时处理摄像头视频流,通过眼睛闭合比例、头部姿态等特征判断疲劳状态,并结合单片机串行通信完成预警联动,覆盖图像预处理、关键点定位、疲劳指数计算与报警触发等核心环节。资源共45个文件,包体约135.75MB,其中包含20个XML配置、10个Python源码、4个IML工程文件,并另附MP3提示音、DAT模型数据及TXT/MD说明文档;目录结构清晰,便于按模块查阅。目前已有61人学习下载,适合作为课程设计或毕业设计二次开发的基础。读者可借助源码与说明文档快速还原开发环境,重点理解OpenCV/Dlib在实时检测中的应用方式,以及软硬件协同的通信设计思路;XML配置文件与IML工程文件还能帮助定位项目依赖,缩短环境搭建成本。
1. 基于Python的驾驶员面部特征疲劳检测:一套能直接跑起来的毕业设计源码
晚上十点开车回家,眼皮开始打架的那几秒,比酒驾更危险。这套源码做的事情很简单:用摄像头盯着驾驶员的脸,计算眼睛闭合的程度和时长,一旦超过阈值就触发蜂鸣器报警。它不是深度学习方案,而是OpenCV + Dlib的68点关键点检测加上EAR眼睛纵横比计算,CPU就能实时跑,延迟不到半秒。源码包里带着模型文件和PyCharm工程配置,明显是学生毕业设计原样导出的结构,适合做计算机视觉方向课设、毕业设计,或者想在树莓派上加一个报警模块的嵌入式爱好者。如果你是准备用现成代码交作业的应届生,或者是想快速验证疲劳检测算法的在职开发者,这套东西值得先下载解压再改。
2. 技术底座:OpenCV + Dlib + EAR,为什么疲劳检测首选这套组合
2.1 面部关键点定位:从HOG到68点模型
疲劳检测的第一步是找人脸,再找眼睛。Dlib提供的人脸检测器用的是HOG特征加线性SVM分类器,扫描图像金字塔,对正面人脸召回率相当高,在720P画面下单帧检测耗时大约20到40毫秒。相比近年流行的YOLO系列,HOG方案不需要GPU,也不用准备标注数据集训练,对毕业设计这种场景是最省事的。
检测到人脸框之后,Dlib用shape_predictor_68_face_landmarks.dat模型回归出68个面部关键点。这个模型基于ERT(Ensemble of Regression Trees)训练,输入是一张灰度图和一个人脸框,输出是68个点的坐标。眼睛周围的关键点分别在36到41(右眼)和42到47(左眼),每个眼睛用6个点描述轮廓。这套模型对光照变化比较敏感,所以代码里通常要先做灰度化和直方图均衡化。
import dlib detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # gray是灰度帧,face是detector返回的人脸框 landmarks = predictor(gray, face) # 右眼6个点,索引36~41 right_eye = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)] # 左眼6个点,索引42~47 left_eye = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)]注意这里用的是0基索引,和Dlib官网示例一致。如果你在网上搜到1基索引的版本,效果相同,只是数值范围不同,别混用就行。predictor输入必须是灰度图,彩色图直接传进去会报类型错误,这是新人最容易踩的坑之一。
2.2 EAR眼睛纵横比:为什么用比例而不是像素距离
找到眼睛的6个关键点之后,很多人第一反应是量眼高,眼高低于某个像素值就认为闭眼。这个思路有个致命问题:摄像头安装距离不同、驾驶员头部前后移动,同一个眼睛在画面里的像素高度差好几倍,固定阈值根本没法用。所以成熟的方案是Soukupova和Cech在2016年提出的EAR(Eye Aspect Ratio,眼睛纵横比)。
EAR的计算公式是:眼睛高度方向上两对关键点的欧氏距离之和,除以眼睛宽度方向上一对关键点的距离的两倍。因为分子分母都是像素距离,最后算出来是一个无量纲比值,跟摄像头距离、画面分辨率基本无关。正常睁眼时EAR大约在0.25到0.33之间,闭眼时会掉到0.1以下,这个数值分布对阈值选择非常友好。
from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # 计算眼睛两组垂直方向关键点的欧氏距离 A = dist.euclidean(eye[1], eye[5]) B = dist.euclidean(eye[2], eye[4]) # 计算眼睛水平方向关键点的欧氏距离 C = dist.euclidean(eye[0], eye[3]) return (A + B) / (2.0 * C)上面对6个点的排列顺序是固定的:eye[0]是内眼角,eye[3]是外眼角,eye[1]和eye[2]是上眼皮,eye[4]和eye[5]是下眼皮。如果你自己采集关键点坐标做可视化,发现顺序不对,EAR计算结果就会明显异常。我一般会在调试阶段把6个点画在帧上,确认点序正确再往下走,这一步能省很多排错时间。
2.3 疲劳判据:单帧EAR加上连续帧判定
单帧的EAR低并不能说明驾驶员在打瞌睡,可能是低头看手机、笑的时候眼睛眯起来,或者转头导致关键点检测抖动。可靠的做法是统计连续多少帧EAR低于阈值。假如摄像头是30帧每秒,连续40帧低于阈值,相当于眼睛持续闭合1.3秒以上,这已经是比较明确的疲劳信号。
有些项目还会同时统计PERCLOS(单位时间内眼睛闭合时间占比),这个指标在交通工程领域用得比较多。实现方式是在滑动窗口里统计闭眼帧数和总帧数的比值,超过比如40%就触发一级报警。双阈值判断的好处是能区分短暂的眨眼和真正的闭眼。
EYE_AR_THRESH = 0.22 EYE_AR_CONSEC_FRAMES = 40 frame_counter = 0 alarm = False while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) faces = detector(gray, 0) for face in faces: landmarks = predictor(gray, face) left_eye = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)] right_eye = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)] left_ear = eye_aspect_ratio(left_eye) right_ear = eye_aspect_ratio(right_eye) ear = (left_ear + right_ear) / 2.0 if ear < EYE_AR_THRESH: frame_counter += 1 if frame_counter >= EYE_AR_CONSEC_FRAMES: alarm = True else: frame_counter = 0 alarm = Falsedetector(gray, 0)的第二个参数是图像金字塔的上采样次数,0表示用原始分辨率检测,速度最快。如果你发现人脸太小检测不到,可以改成1,召回率会提高但帧率明显下降。equalizeHist在这里的作用是均衡光照,逆光场景下效果很明显,但也会放大图像噪声,所以后面检测眼睛时对关键点精度要求更高。
3. 环境搭建与源码运行:Dlib安装、目录解读、阈值调到能跑
3.1 Python环境准备:OpenCV、Dlib、imutils的安装顺序
拿到压缩包第一件事是解压,但先别急着跑主程序。这套依赖里最难装的是Dlib,它在Windows上经常编译失败,这是疲劳检测项目里最常见的劝退点。我的建议是用Python 3.8或3.10配合Dlib 19.24.0这个版本,先装好CMake和Visual Studio Build Tools再pip install,能避开大部分编译报错。
python --version pip install cmake pip install dlib==19.24.0 pip install opencv-python imutils numpy scipy pyserial如果你是在Windows上装Dlib直接报错,还可以试试conda渠道的预编译包,一条命令搞定,不需要本地编译工具链:
conda install -c conda-forge dlibDlib版本和Python版本强相关,如果用的Python 3.12还指定安装旧版Dlib,pip会提示找不到匹配的wheel,然后开始现场编译,耗时十几分钟而且大概率失败。我的血泪经验是:别追新,Python 3.10配Dlib 19.24.0是最省心的组合。装完之后用import dlib验证一下,顺便看一下版本号,后面排错时能用上。
3.2 源码包结构:从新建文本文档.txt到Fatigue-detection-system-branch
解压zip之后,你会看到一个典型的、没有整理过的学生项目目录。Fatigue-detection-system-branch这个目录名暗示源码可能是从某个Git分支直接导出的,.idea文件夹是PyCharm的工程配置,新建文本文档.txt这种占位文件说明作者没有做系统性的文档整理,属于原汁原味的毕业设计产物。
| 文件/目录 | 作用 |
|---|---|
| Fatigue-detection-system-branch/ | 源码根目录,也可能包含子模块 |
| .idea/ | PyCharm配置,里面记录了运行环境和解释器路径 |
| 新建文本文档.txt | 占位说明,通常写着运行步骤或依赖 |
| shape_predictor_68_face_landmarks.dat | 68点关键点模型,运行必需 |
| main.py 或类似主程序 | 摄像头采集、EAR计算、报警逻辑所在 |
拿到这种包,我建议先花10分钟把目录结构摸一遍,把主程序、模型文件、依赖清单分别找出来,然后手动整理一次目录。把模型文件和数据文件放到单独目录,主程序放到根目录,不然写到一半发现模型路径写死成相对路径,怎么调都找不到文件。
3.3 跑起来的第一步:摄像头索引和启动参数
主程序里最需要改的参数是摄像头索引。笔记本自带摄像头一般是0,USB外接摄像头可能是1,如果你在笔记本上插了USB摄像头但没改索引,启动后画面要么是黑屏要么报错。用下面这段代码可以快速探测当前机器有哪些摄像头可用。
import cv2 for index in range(3): cap = cv2.VideoCapture(index) if cap.isOpened(): print(f"摄像头 {index} 可用") cap.release()启动主程序之后如果画面正常,先保持正常睁眼状态看终端输出,EAR值应该在0.25左右。如果明显偏低,说明摄像头安装位置偏高或者人脸离得太远,需要调整阈值。常见做法是先采集10秒睁眼数据算平均值,再用这个平均值的70%作为阈值。这一条做得好,后面误报能减少一大半。
4. 避坑与排查:五个跑了就翻车的经典问题
4.1 dlib导入报错或者编译中途失败
现象:pip install dlib之后,import dlib直接报错,或者安装时卡在Building wheel for dlib然后失败。
原因:Python版本过新、缺少CMake编译工具链、dlib版本与Python不兼容,三个原因至少占一个。
解决:先降Python版本到3.8到3.10之间,再指定dlib版本安装。Windows用户如果conda环境可用,直接用conda install -c conda-forge dlib走预编译,全程不到两分钟。配套的opencv-python、imutils都跟着已装好的环境一起装,别开多个虚拟环境来回切。
4.2 摄像头黑屏或者程序直接崩溃
现象:程序启动后没有画面,或者运行几秒后报错VIDEOIOC ERROR: V4L2。
原因:摄像头索引写死为0但实际设备是1;笔记本摄像头被别的程序占用;Linux下当前用户没有video设备权限。
解决:先用上章的摄像头探测代码确认索引,再把主程序里的VideoCapture参数改成实际索引。Linux下把用户加入video组,命令是sudo usermod -aG video $USER,然后注销重新登录。如果Windows下摄像头被微信或浏览器占用,关掉所有可能占用摄像头的程序再重跑。
4.3 正常睁眼却一直报警
现象:人坐得端端正正,眼睛完全睁开,系统却每几秒就响一次蜂鸣器。
原因:EAR阈值设置太高,或者直方图均衡化把脸上高光区域的纹理放大,导致关键点定位在眼皮上跳动。另一种情况是戴了眼镜,镜片反光让上下眼皮关键点距离被高估。
解决:先把阈值降到0.18试跑,观察正常睁眼的EAR输出值。如果还是误报,就把equalizeHist去掉,改成只用灰度图。戴眼镜的驾驶员场景在毕业设计里不用强求,答辩时说明这个限制即可。
4.4 树莓派或者单片机端报警不动作
现象:PC端程序已经打印出报警指令,但连着的蜂鸣器模块一点反应没有。
原因:串口号不对、波特率不匹配、TTL电平不一致,或者单片机供电不足。树莓派的UART引脚输出的是3.3V电平,接到5V的单片机系统需要额外处理,接反了还可能烧引脚。
解决:先用串口工具确认PC端实际挂载的串口设备名,比如/dev/ttyUSB0还是/dev/ttyAMA0,然后确认两边波特率完全一致。单片机供电不要从树莓派GPIO的5V引脚直取大电流蜂鸣器,单独供电更稳。
4.5 报警触发后一直响不停
现象:触发一次疲劳报警之后,蜂鸣器一直响,直到人脸离开画面才停。
原因:程序里没有设计复位逻辑。驾驶员闭眼触发报警后睁开眼,如果EAR恢复到正常值但软件端的alarm标志没有被清除,就会一直处于报警状态。
解决:在代码里加一个冷却时间,或者用连续N帧正常EAR来复位报警。比如连续20帧EAR大于阈值,就自动关闭报警。这个逻辑在真实驾驶场景里必须做,不然后果就是驾驶员被吵到直接关系统。
if ear > EYE_AR_THRESH: frame_counter = 0 if alarm: # 连续20帧睁眼成功,清除报警状态 alarm = False提示:报警复位逻辑建议用独立参数控制,不要复用疲劳判定的frame_counter,避免两边互相干扰。
5. 集成单片机报警:串口通信、UART数据帧、蜂鸣器联动
5.1 为什么毕业设计里要接单片机
纯PC端的疲劳检测在答辩演示时够用,但离“系统”还差一步。摘要里提到的单片机,在这里的意义是承担报警输出和外围控制。PC端负责计算疲劳状态,通过串口把一条指令发给单片机,单片机再驱动蜂鸣器、LED灯条或者座椅震动模块。这样设计的好处是报警动作不依赖PC的音频系统,电源和控制逻辑独立,后续扩展方向盘震动这类功能也方便。
Python和单片机之间最常见的通信方式是UART串口,一根TXD一根RXD一根GND就搞定。摘要里也提到SPI,但SPI需要CS片选信号,接线多一层,对这种单字节指令传输场景用UART更合适。
5.2 Python端串口发送报警信号
PC端这边用pyserial库,发送一个单字节指令就够。报警用A字符,复位用B字符,简单明了。串口的波特率我一般固定用9600,这个速率对单片机来说最稳,不用担心高频干扰。
import serial try: ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=0.5) except serial.SerialException: print("串口打开失败,检查设备号和权限") ser = None if alarm and ser is not None: ser.write(b'A') elif not alarm and ser is not None: ser.write(b'B')每次状态变化时发送一次指令,而不是每一帧都发。你想一下,30帧每秒的视频流,如果每一帧都发一个A,单片机那边收到一堆重复指令,逻辑上虽然能处理,但串口带宽被无关数据占满,得不偿失。我一般只在alarm状态翻转时发送,这个习惯能让整个通信过程清爽很多。
5.3 单片机端最小实现:识别指令、驱动蜂鸣器
单片机端的固件也很简单。以STM32标准外设库风格为例,UART接收中断里判断收到的字节,是A就打开蜂鸣器和LED,是B就全部关闭。以下是参考实现,不是源码包里自带的,但和这套系统的上位机协议直接配套。
// UART接收中断服务函数(伪代码风格,需要按具体芯片适配) void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { ch = USART_ReceiveData(USART1); if (ch == 'A') { GPIO_SetBits(GPIOB, GPIO_Pin_0); // 蜂鸣器 GPIO_SetBits(GPIOB, GPIO_Pin_1); // LED指示 } else if (ch == 'B') { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); } } }这里要注意两个细节。第一,串口波特率必须和Python端完全一致,9600对9600,差一个数字都收不到正确数据。第二,单片机的GND必须和树莓派或PC的GND共地,否则串口电平没有参考基准,数据会乱码。这是串口通信里最容易忽略的坑,我见过太多次因为没共地导致收不到字符的案例。
5.4 报警触发逻辑与防抖设计
报警逻辑不能只看一次EAR低于阈值就触发。表格里这组参数是我在模拟驾驶环境下调过的组合,兼顾响应速度和误报率。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| EAR闭眼阈值 | 0.20~0.22 | 根据摄像头安装距离微调 |
| 疲劳连续帧数 | 40~50 | 30fps下约1.3~1.7秒 |
| 报警冷却时间 | 2~3秒 | 触发后禁止重复报警 |
| 复位连续帧数 | 20 | 睁眼持续0.7秒后解除报警 |
加了冷却时间和复位条件之后,整个系统的行为就变成:驾驶员闭眼超过1.5秒,蜂鸣器响,持续3秒后即使仍在闭眼也不重复响,直到睁眼超过0.7秒才解除。这个逻辑比单纯判断alarm标志位要完善得多,答辩时也能讲清楚每个参数的设计依据。
6. 进阶与验证:用有标注视频回测,三个能加分的改进方向
6.1 帧级评估:FAR和FRR怎么算
跑通系统之后,下一步是用真实视频验证算法到底靠不靠谱。随车录一段5分钟的视频,然后手动标注每一帧是正常还是闭眼。标注工作很枯燥,但这是把毕业设计做成工程实践的必经一步。
# 帧级疲劳检测结果评估 # predicted和ground_truth都是每帧的0/1标签列表 tp = fp = fn = 0 for p, g in zip(predicted, ground_truth): if p == 1 and g == 1: tp += 1 elif p == 1 and g == 0: fp += 1 elif p == 0 and g == 1: fn += 1 far = fp / (fp + tp + 1e-6) # 误报率 frr = fn / (fn + tp + 1e-6) # 漏报率 print(f"误报率: {far:.2f}, 漏报率: {frr:.2f}")评估结果直接影响参数调优。误报率高就把EAR阈值调低或者连续帧数调高,漏报率高就反过来。这一步做完,你答辩时的数据是从真实视频里测出来的,比直接说“准确率95%”有说服力得多。
6.2 三个可扩展方向:头部姿态、打哈欠检测、红外夜视
疲劳检测只用眼睛还不够完整,头部姿态和打哈欠也能反映疲劳状态。Dlib的68点模型里,鼻子区域的关键点配合人脸框可以做简单的头部姿态估计,当低头角度超过30度持续几秒,结合闭眼信号一起触发报警,复杂度不高但系统完整度提升一大截。打哈欠检测思路类似,用嘴部关键点计算嘴的纵横比MAR,阈值设置成0.6以上并持续一定帧数。夜视场景则需要IR摄像头配合红外补光灯,OpenCV代码基本不用改,只是输入源从RGB变成红外图像。
6.3 性能优化:跳帧处理与ROI裁剪
如果你打算把系统跑到树莓派上,就需要考虑CPU占用。Dlib的关键点检测虽然不依赖GPU,但在树莓派4B上处理720P画面也就10到15帧每秒的样子。常见做法是每隔一帧做一次检测,比如跳帧参数设为2,检测频率降到15帧每秒,疲劳判定依然来得及,CPU占用能降一半。
另一个优化点是先检测人脸,把上一帧的人脸框扩大20%作为当前帧的ROI,只在ROI区域内做关键点检测。这样脸部占画面比例越大,节省的计算量越多。我一般用这两个手段组合,720P画面在树莓派上能稳定跑到20帧以上。
从那以后,我每拿到一个疲劳检测源码包,都会先录一段带标注的真实视频跑一遍评估流程,再谈装车测试。这套包下下来之后我也是这么处理的,先把环境装好、把阈值调对、把报警复位逻辑补上,再谈扩展功能。算法这东西,单帧跑通不是终点,连续视频里的可靠性才是真功夫。希望帮到你。
本文还有配套的精品资源,点击获取