ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python与dlib的驾驶员疲劳检测:关键点、EAR与PERCLOS实战

基于Python与dlib的驾驶员疲劳检测:关键点、EAR与PERCLOS实战 简介基于Python的驾驶员面部特征疲劳检测系统是一款融合OpenCV、Dlib与单片机技术的毕业设计项目源码包面向计算机视觉、嵌入式开发及智能交通方向的初学者和毕业设计学生。项目通过摄像头实时采集驾驶员面部图像利用HOG特征检测定位眼睛、鼻子、嘴巴等关键点并根据眼睑闭合程度和头部姿态判断疲劳状态触发声光报警。资源包共45个文件体积约135.75MB包含10个Python主程序、20个XML配置文件如Dlib模型参数、4个IDEA工程配置、2个MP3报警提示音及2个DAT数据文件代码结构清晰便于二次开发。目前已有61人学习下载。读者可从中掌握Python图像处理、Dlib关键点检测、串行通信UART/SPI以及单片机协同控制的完整流程同时参考工程源码理解实时系统设计中响应时间与可靠性权衡是毕业设计实战与技能提升的实用素材。1. 基于Python的驾驶员面部特征的疲劳检测系统源码.zip这东西能干什么「基于Python的驾驶员面部特征的疲劳检测系统源码.zip」这类压缩包在免费python源码大全里非常常见。解压之后里面通常是一套能直接接摄像头的Python工程摄像头对准驾驶员的脸程序先用68点关键点定位眼睛和嘴巴再按闭眼时长、打哈欠幅度和闭眼帧占比判断是否需要报警。它解决的是长途货运、网约车场景里的疲劳预警驾驶员不用穿戴任何设备也不需要接管方向盘系统只输出「该提醒一下了」这个信号。适合做毕业设计的学生、给车队做方案验证的工程师也适合想评估视觉疲劳检测性价比的产品负责人。先说一个反直觉的结论这类系统检测的从来不是疲劳本身而是眼皮闭合和打哈欠这些外部表现后续所有调参其实都在跟这两个特征打交道。2. 从人脸到疲劳指标关键点、EAR/MAR与判定公式的拆解2.1 dlib 68点关键点检测为什么绕不开它常见做法里检测链路是「人脸检测 → 关键点定位 → 特征计算 → 时序判定」。OpenCV自带的Haar级联和深度学习人脸检测只给一个矩形框框内部有没有眼睛、嘴在哪里它不负责。要计算闭眼和打哈欠必须拿到眼睛轮廓和嘴部轮廓的具体坐标所以绝大多数基于面部特征的方案会用dlib的shape_predictor_68_face_landmarks.dat模型。这个模型输出68个点索引0到67其中左眼是36到41右眼是42到47嘴部外圈集中在48到59。我把取点函数写成独立函数方便后面直接复用import numpy as np def get_landmarks(gray, rect, predictor): shape predictor(gray, rect) pts np.zeros((68, 2), dtypefloat) for i in range(68): pts[i] (shape.part(i).x, shape.part(i).y) return ptspredictor接收灰度图和一个人脸矩形调用一次返回全部68点。这里有个容易翻车的细节shape.part(i).x取出来是int如果直接参与EAR计算除法结果的精度会差先转成float数组处理起来干净得多。另外dlib的rect是dlib.rectangle不能直接当OpenCV的tuple来切片否则后面画框时会报类型错误。2.2 EAR和MAR两个结构相同的公式三种经典误判EAREye Aspect Ratio衡量的是眼睛睁开程度公式是垂直距离的均值除以水平距离。左眼和右眼各算一次再取平均就是当前帧的EARdef eye_aspect_ratio(eye_points): # 传入6个点左眼或右眼的dlib关键点坐标 p1, p2, p3, p4, p5, p6 eye_points.astype(float) v1 np.linalg.norm(p2 - p6) v2 np.linalg.norm(p3 - p5) h np.linalg.norm(p1 - p4) if h 1e-6: return 0.0 return (v1 v2) / (2.0 * h)睁眼时EAR一般在0.25到0.35闭眼时会掉到0.05到0.15。这个比值对图像分辨率不敏感所以摄像头距离变化不会直接摧毁阈值但不同人眼型差异大有人天生眼睛细长睁眼EAR可能只有0.22这也是后面要做标定的根本原因。MARMouth Aspect Ratio的结构和EAR几乎一样只是把点组换成了嘴部外圈轮廓def mouth_aspect_ratio(mouth_points): # mouth_points 是 pts[48:60] 的切片按dlib官方68点顺序取 p1 mouth_points[0] # 48 左侧嘴角区域 p4 mouth_points[6] # 54 右侧嘴角区域 p2 mouth_points[3] # 51 上唇 p3 mouth_points[4] # 52 上唇内侧 p5 mouth_points[8] # 56 下唇内侧 p6 mouth_points[9] # 57 下唇 v1 np.linalg.norm(p2 - p6) v2 np.linalg.norm(p3 - p5) h np.linalg.norm(p1 - p4) return (v1 v2) / (2.0 * h)嘴部索引在不同源码里经常不一致跑通第一步应该是打印几个嘴部点的坐标确认它们确实落在嘴角和上下唇边缘而不是直接信网上抄的编号。嘴巴闭合时MAR大约0.1到0.2打哈欠时能到0.4以上。口罩场景会让这组点完全失真后面第四章会讲怎么关掉这一路信号。2.3 从单帧指标到疲劳判定连续帧和PERCLOS单帧的EAR低没有意义因为正常眨眼也有闭眼帧。业界最常见的做法是两套规则叠加第一套是连续帧判定EAR低于阈值且持续超过一定帧数才报疲劳第二套是PERCLOS统计一个滑动窗口内闭眼帧的占比。from collections import deque def perclos(closed_flags, window): # closed_flags 是每帧产生的布尔值True表示该帧闭眼 if len(closed_flags) window: return 0.0 recent closed_flags[-window:] return sum(recent) / len(recent)这里有几个容易忽略的边界closed_flags建议用deque(maxlenwindow)否则跑几十分钟内存就涨下去滑动窗口未填满时直接返回0避免刚启动时把司机正常眨眼误判成疲劳。PERCLOS窗口长度一般取5秒阈值常设在0.4含义是5秒里有40%以上时间眼皮处于闭合状态就认为驾驶员进入危险状态。2.4 三种信号怎么组合才不误报实际源码里最常见的是「EAR连续帧阈值 PERCLOS MAR打哈欠」三路并联任一路超限就输出疲劳。但并联会放大误报尤其MAR在说话、唱歌、咀嚼时都会波动。我一般会把判定优先级调成PERCLOS优先因为它本身已经带时间窗口抗单帧噪声最强EAR连续帧作为辅助专门抓那种「眼睛半闭但还没完全闭合」的疲劳状态MAR只做加分项不单独触发最终报警而是当MAR偏高时把PERCLOS的阈值从0.4放宽到0.35让系统更敏感。这个组合思路会贯穿后面所有参数调整。3. 跑通源码的最小环境安装顺序、模型文件与主循环3.1 环境准备python安装和dlib依赖的先后顺序先处理环境。如果你还没装Python按官方python安装教程装64位版本推荐3.8或3.10不建议一上来就装最新版因为很多从源码集锦里打包下来的项目依赖列表还没有适配新Python。装完Python后用虚拟环境装依赖是底线操作python -m venv .venv source .venv/bin/activate # Windows下执行 .venv\Scripts\activate pip install opencv-python dlib numpy在Windows上装dlib容易卡在编译环节它的前置条件是cmake和VS Build Tools。用pycharm配置python环境时记得让项目指向这个虚拟环境而不是全局解释器。很多下载源码的人都栽在同一个地方dlib在全局环境里装了好几次换台机器或者换项目后直接ModuleNotFoundError因为全局环境里装了太多互相冲突的包版本。如果目标设备是嵌入式Linux编译dlib会更折腾常见替代是OpenCV DNN配合一个独立的关键点ONNX模型但本文标题这类源码默认就是dlib所以先按dlib这条线走通后续再谈替换。3.2 模型文件放哪里整个项目最容易卡住的黑匣子dlib的人脸检测器和关键点模型是分开的检测器内置于dlib库关键点模型却是一个独立的dat文件。源码里通常只写了怎么调用并没有自带这个文件。注意模型文件不会跟着pip安装进来必须手动放到项目目录。先去源码的README里找下载说明没有说明就把文件名固定为shape_predictor_68_face_landmarks.dat放到models目录下。项目目录我一般这样组织项目根目录main.pymodels/shape_predictor_68_face_landmarks.datrequirements.txt代码里不要写死绝对路径用相对路径定位from pathlib import Path model_file Path(__file__).parent / models / shape_predictor_68_face_landmarks.dat if not model_file.exists(): raise FileNotFoundError(f模型文件不存在: {model_file})Path(__file__).parent取的是当前源码文件所在目录不管项目被拷贝到哪台机器都能找到模型。提前做存在性检查比等dlib内部报错然后傻眼要直观得多。3.3 摄像头实时检测的最小主循环把第2章的函数和模型加载拼起来就是一个能跑的实时检测程序import cv2 import dlib import numpy as np from collections import deque from pathlib import Path model_file Path(__file__).parent / models / shape_predictor_68_face_landmarks.dat detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(str(model_file)) LEFT_EYE [36, 37, 38, 39, 40, 41] RIGHT_EYE [42, 43, 44, 45, 46, 47] def eye_aspect_ratio(eye_points): p1, p2, p3, p4, p5, p6 eye_points.astype(float) v1 np.linalg.norm(p2 - p6) v2 np.linalg.norm(p3 - p5) h np.linalg.norm(p1 - p4) return (v1 v2) / (2.0 * h) cap cv2.VideoCapture(0) if not cap.isOpened(): raise SystemExit(摄像头打不开检查权限或被其他程序占用) closed_flags deque(maxlen150) while True: ok, frame cap.read() if not ok: break frame cv2.resize(frame, (640, 480)) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) rects detector(gray, 0) for rect in rects: pts np.zeros((68, 2), dtypefloat) shape predictor(gray, rect) for i in range(68): pts[i] (shape.part(i).x, shape.part(i).y) left_ear eye_aspect_ratio(pts[LEFT_EYE]) right_ear eye_aspect_ratio(pts[RIGHT_EYE]) ear (left_ear right_ear) / 2.0 closed_flags.append(ear 0.22) if len(closed_flags) 30: pclos sum(closed_flags) / len(closed_flags) if pclos 0.4: cv2.putText(frame, FATIGUE, (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) cv2.rectangle(frame, (rect.left(), rect.top()), (rect.right(), rect.bottom()), (0, 255, 0), 2) cv2.imshow(driver fatigue monitor, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()detector(gray, 0)的第二个参数是上采样次数0表示在原始分辨率上检测1会让检测更齐全但CPU负载暴涨驾驶座这种单人场景用0即可。cv2.resize(frame, (640, 480))是保证帧率的关键1080p直接喂给dlib普通CPU会卡到没法看。闭眼判断写在检测循环内部每帧把ear 0.22这个布尔值追加到deque长度超过30帧后才有perclos输出前30帧不参与判断。3.4 报警方式从界面提示到声音的接入点界面红色提示只适合开发时验证逻辑司机不会一直盯着屏幕。真正要接报警最简单的方式是非阻塞声音提示import winsound def alarm(): winsound.Beep(880, 300) winsound.Beep(660, 300)winsound只在Windows上存在Linux和macOS要用pygame或者直接调系统的音频播放命令。报警函数不要放在检测主循环的同一线程里连续调用否则每次报警都会把摄像头帧率拖垮常见做法是检测到疲劳后置一个标志位由独立线程消费这个标志位去播放声音。如果源码里是直接在循环里sleep报警那就要注意这个sleep会连累下一帧的读取时间。4. 驾驶场景参数调优分辨率、灯光、眼镜和口罩怎么设4.1 别一上来就跑1080p输入尺度和检测频率很多第一次跑疲劳检测源码的人习惯把摄像头分辨率调到最高结果打开程序发现画面像幻灯片。dlib的人脸检测在CPU上的耗时增长很陡720p和1080p的体验完全不在一个量级。驾驶场景不需要把车窗外的细节拍清楚只需保证人脸区域在150像素以上。所以第一件事就是把输入缩到640x480检测频率从「每帧检测」降到「每2到3帧检测一次」。降频的意思是人脸检测和关键点计算每隔几帧做一次中间帧直接沿用最后一次得到的关键点坐标。EAR在连续几帧之间变化极小不会因为降频漏掉闭眼状态但CPU占用能降一半。这个优化对工控机、树莓派这类设备尤其重要因为疲劳检测系统往往还要同时跑GPS、4G通信和视频录制。4.2 关键点抖动对EAR做平滑而不是对报警做平滑dlib的关键点稳定但逐帧看还是会有1到2像素的抖动体现在EAR上就是0.02到0.05的小幅波动。如果阈值正好卡在波动区间程序会像抽风一样频繁进入报警又退出报警。对EAR本身做指数移动平均比在报警状态上做延时要干净alpha 0.3 smoothed_ear alpha * ear (1 - alpha) * smoothed_earalpha越大越跟手越小越平滑。0.2到0.4算是一个安全区间。不要为了追求平滑把alpha调到0.05以下因为闭眼动作本身只有零点几秒过度平滑会把快速闭眼抹成一条缓降曲线反而让连续帧判定失效。4.3 光照变化夜间红外和背光怎么处理dlib的正脸检测器对灰度图的对比度敏感夜间红外摄像头输出的虽然是灰度图但眼睛周围容易因为红外反光出现关键点漂移。最有效的做法不是加图像增强而是开启摄像头自带的红外补光没有红外补光时就要求驾驶室保留一定的基础照明。很多源码里会加直方图均衡化来增强对比度这个操作在夜间背景容易把噪点放大导致人脸框时有时无。处理丢帧的方法是保存上一次有效的人脸框位置下一帧如果没有检测到人脸就在上一帧位置附近扩大搜索范围重新检测而不是直接报「人脸丢失」。把这一层逻辑加进去夜间场景的稳定性会明显提升。4.4 戴眼镜与戴口罩两个容易被低估的变量戴眼镜时镜片反光会让眼裂区域的特征被高亮关键点会向反光边缘偏移结果是EAR整体偏大闭眼时EAR可能跌不透阈值造成漏报。最直接的排查方式是把EAR实时打印出来让司机闭眼看闭眼瞬间EAR到底掉到多少。如果闭眼EAR还停在0.22以上说明固定阈值在这副眼镜下无效要么换检测参考点要么在闭眼分布和睁眼分布的中间取阈值。口罩场景更直接嘴部关键点会被布料推离真实位置MAR完全失真。戴口罩时应该关闭MAR这条信号线只用EAR和PERCLOS。最麻烦的组合是眼镜加口罩这时只剩眼睛一组信号系统的容错空间很小阈值必须严格按这个司机的历史数据来标定不能沿用另一个人的参数。4.5 参数表推荐初始值与标定方法给出这套系统里最常用的初始参数按30fps摄像头设定参数推荐初始值调参依据ear_thresh0.22标定后的睁眼均值减2倍标准差close_frames_thresh15帧30fps下约0.5秒闭眼perclos_window150帧5秒滑动窗口perclos_thresh0.4窗口内闭眼占比超过40%报警mar_thresh0.4打哈欠时MAR的通用分界up_sample0640x480输入时不用上采样min_face_width120像素小于该宽度不送关键点避免误检我一般会在项目里做一个20秒标定流程司机坐在驾驶位正常睁眼看前方10秒统计EAR均值和标准差然后用ear_thresh mean - 2 * std作为该司机的阈值。闭眼2秒记录闭眼期的EAR最低值确认它和睁眼均值有清晰间隔。提示标定时让司机保持实际驾驶坐姿不要凑近摄像头。凑近会让眼裂占比变大标定出的阈值偏紧真正驾驶时误报会明显增加。5. 避坑源码能跑但不稳定的系统性排查5.1 现象摄像头画面很卡fps掉到5以下卡顿的原因一般是三个输入没有缩放过无穷上采样次数被设成1或2以及OpenCV用同步读帧的方式读取高分辨率摄像头。解决方式是先把采集分辨率用cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)压下来再在循环里用cv2.resize做二次确认。如果摄像头支持MJPG编码可以额外申请一下cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*MJPG)) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)MJPG输出的是压缩帧带宽占用低摄像头内部解码压力小帧率经常比默认YUV格式高一倍。改完之后如果还卡就把检测频率降到每3帧一次画面流畅度会立刻改观。5.2 现象车里有第二个人系统频繁乱报警原因是所有检测到的人脸都送进了疲劳判定逻辑副驾聊天、后座乘客动一下都会触发。解决方法是只保留一个目标驾驶座上的人。在多人脸场景里最简单的筛选是取面积最大的人脸框因为驾驶员离摄像头最近面部占据的像素面积最大rects detector(gray, 0) if rects: rect max(rects, keylambda r: r.width() * r.height())更稳的做法是做一个「最近帧匹配」记录上一帧的人脸框位置这一帧在上一帧周边一定范围内找最近的框找不到才换成最大框。这样即使副驾偶尔比司机离摄像头更近系统也不会跳目标。5.3 现象正常眨眼被误判成疲劳报警正常眨眼持续0.1到0.4秒30fps下大约是3到12帧疲劳状态下的闭眼通常超过0.8秒。很多源码默认写的是ear thresh且连续3帧就报警这等于把每一次眨眼都当成疲劳。正确做法是把连续帧阈值提到15帧以上这已经能过滤掉绝大多数正常眨眼。如果阈值提高了还误报就去查PERCLOS窗口。滑动窗口太短比如只有30帧一次闭眼就会把PERCLOS顶到0.3以上再加上0.4的判定线眨眼和疲劳的边界就被抹掉了。窗口保持150帧左右再配合连续帧阈值眨眼这种短时信号基本进不了报警状态。5.4 现象戴眼镜的司机闭眼检测不到漏报严重镜片反光造成EAR偏大闭眼时EAR跌不破阈值导致漏报。盲目把阈值调高又会把睁眼状态误报成闭眼。解决思路是先标定后调参让司机闭眼几秒打印出闭眼期EAR的实际分布如果闭眼和睁眼两个分布仍有明显间隔就把阈值取在两个分布交界处如果两个分布已经重叠说明这副眼镜反光太强关键点本身不可信只能改用另一只眼的数据。具体到代码里左眼右眼可以分别算EAR并打印不要只看平均值。很多场景下反光只影响一侧眼睛另一侧仍然可靠。这时可以让系统只用可信的那只眼做判定代价是阈值要重新标定一次。5.5 现象打开程序就报错模型文件找不到或依赖缺失这个报错基本不是逻辑问题而是路径和依赖。源码作者往往把模型文件放在自己的绝对路径下直接分享压缩包时忘了包含models目录接收者又习惯把源码解压后单独拿着main.py跑于是dlib.shape_predictor(C:/Users/xxx/models/...)直接抛异常。我的习惯是在入口处做路径检查并打印出实际路径一眼就能看出来是不是找错了地方。from pathlib import Path model_file Path(__file__).parent / models / shape_predictor_68_face_landmarks.dat if not model_file.exists(): raise FileNotFoundError(f模型文件不存在: {model_file.resolve()})另一个隐蔽问题是中文路径。项目根目录如果包含中文文件夹名称部分旧版dlib在读取模型文件时会解析失败表现是文件名明明对但一直报错。项目目录统一用英文字母命名从根上避免这个玄学问题。6. 进阶把检测结果落盘并用离线视频重放验证阈值6.1 先录制一段带时间戳的驾驶视频再调参不建议直接开着系统上车实测参数调不好时车内报警声会让人完全没法判断对错。正确流程是先用摄像头录制一段10分钟左右的驾驶视频覆盖正常驾驶、闭眼、打哈欠、看手机、侧脸说话这些片段然后把检测程序改成离线模式把每一帧的EAR、MAR、PERCLOS和判定结果写入CSV再和视频时间轴对齐。import csv with open(tuning_log.csv, w, newline) as f: writer csv.writer(f) writer.writerow([timestamp_ms, frame, ear, mar, perclos, state]) frame_idx 0 while cap.isOpened(): ok, frame cap.read() if not ok: break ts_ms cap.get(cv2.CAP_PROP_POS_MSEC) ear, mar, pclos, state process_frame(frame) writer.writerow([ts_ms, frame_idx, round(ear, 3), round(mar, 3), round(pclos, 3), state]) frame_idx 1这里时间戳必须用cap.get(cv2.CAP_PROP_POS_MSEC)不能用系统当前时间。系统时间在视频文件里没有意义回放时也没法和画面帧对齐视频内时间戳是按帧位置计算的逐帧对照时才不会错位。6.2 回放时看误报点分布而不是只看画面是否报警CSV落盘后用一个简单的回放脚本把视频按帧播放同时读取CSV里对应帧的state把判定为fatigue的帧画上红色边框。逐帧看过一遍误报的规律就会浮出水面正常睁眼但state是fatigue多数是阈值太贴近睁眼均值侧脸时误报说明关键点在非正脸角度下漂移刚眨完眼就报警问题多半出在PERCLOS窗口太短或者连续帧阈值太低。我第一次把这套系统装到实车上时直接用了网上现成的固定阈值结果夜路一路狂报关掉报警后才发现每一条误报都能在CSV里找到对应帧。后来把每个司机都做一遍标定用离线重放确认阈值边界系统才算真正能交付。如果你也准备往这个方向投入建议先别急着优化模型把标定和回放这条链路搭起来你会发现多数所谓检测不稳定根源都在参数没有跟随真实场景。希望帮到你。本文还有配套的精品资源点击获取
返回列表