多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于dlib和EAR的疲劳驾驶检测系统设计与实现

基于dlib和EAR的疲劳驾驶检测系统设计与实现 简介一份PDF版技术文献围绕基于计算机视觉的司机驾驶疲劳检测系统展开适合计算机视觉、图像处理方向的学生与开发者作为参考文献与专业指导。内容涵盖人脸特征点检测、人眼定位、基于EAR值的疲劳识别算法以及完整系统实现与结果分析便于读者复现实验并理解疲劳驾驶检测的关键流程。资源包仅1个PDF文件大小2.66MB轻量易下载。目前已有165人浏览学习。文中结合OpenCV与dlib工具包对比了Haarcascades在人眼小且闭合时定位不准的问题并给出基于dlib的68点特征点方案还介绍了通过眼睛纵横比判断闭眼状态、根据闭眼时长与频率判定疲劳状态的算法思路。实验部分显示系统成功率约90%并分析了现实环境中光线、侧面等因素对检测的影响。对需要快速掌握该方向基础方法或撰写相关课题报告的研究者这份PDF能提供较完整的理论框架和实现思路。1. 从眼皮闭合到疲劳报警一个上手快的计算机视觉项目在高速上跑夜车眼皮打架的那一两秒恍惚往往就是事故的起点疲劳驾驶检测也因此成了计算机视觉里一个很接地气的落地课题——不做复杂动作识别只盯住驾驶员的眼睛状态。这篇基于计算机视觉的司机驾驶疲劳检测系统论文技术路线非常直观先定位人脸再提取人眼特征点用特征点的几何比例算出眼睛开合程度连续多帧闭眼就触发报警。它的选型也够实在放弃了 opencv 的 haarcascades改用 dlib 的 68 点人脸特征点模型再配合 EAR 值做疲劳判断。对准备做计算机视觉课程设计、大作业起步或者想复现一个疲劳检测项目的人来说这份资源把算法选型、参数设定和系统流程都讲清楚了照着搭一套实时检测 demo 完全可行。2. 人眼定位方案选型为什么绕开 haarcascades 直接上 dlib2.1 haarcascades 在闭眼场景下翻车的原因opencv 自带的 haarcascades 是一套基于 Haar-like 特征的级联分类器官方包里给了 haarcascade_frontalface_default.xml 和 haarcascade_eye.xml 等现成模型。它的优势是轻量、CPU 上跑得快正脸、光线均匀、眼睛自然睁开时表现尚可。论文里也承认这一点光泽比较好、眼睛睁开幅度比较大的时候opencv 自带功能确实能识别人脸和人眼。但到了疲劳检测的真实场景这套模型马上露馅。论文原文写得很直白在人眼比较小且闭合比较大时人眼定位并不准确有时候甚至连人脸都识别不出来。从原理上说Haar 级联分类器靠的是局部矩形特征的组合投票眼睛睁开时瞳孔、虹膜和眼白的灰度对比强烈特征响应明显一旦眼皮闭合眼区变成一整块平滑纹理可区分特征大幅衰减分类器自然就丢框了。再加上疲劳状态下头部姿态往往偏低侧脸和低头角度一上来正脸检测器也容易失手。我自己的复现体验是haarcascades 在戴眼镜、逆光、夜间这三个条件下掉点尤其严重识别框会在眼周跳动甚至直接消失。这不是参数调优能救的是特征表达方式的天花板。2.2 dlib 的 68 点特征点模型与眼部关键点分布dlib 的人脸检测与关键点定位是另一个技术路线。检测器用的是 HOG方向梯度直方图 线性 SVM先滑窗扫出人脸框关键点定位则通过训练好的回归模型在框内输出 68 个语义一致的坐标点。论文选取其中 36-47 号点作为眼部特征点使用这里要稍微细化一下标准 68 点模型的具体划分。68 点模型把脸部解剖结构拆成下颌、眉毛、鼻梁、眼睛、嘴巴几个区域其中 36-41 是右眼轮廓的六个点42-47 是左眼轮廓的六个点48-67 则覆盖嘴部。论文里写的36-47 为左右眼的特征点在区间上是对的但实际取点时必须精确到单眼六点否则后面算 EAR 时点的顺序会乱。每个点都对应图像上的一个像素坐标坐标已经做过归一化处理和输入图像的分辨率无关所以同样的代码在不同摄像头分辨率下不需要改参数。这就是 dlib 相对 haarcascades 的核心优势它输出的不是眼睛在哪的粗粒度矩形框而是眼睛轮廓的精确坐标序列。闭眼时上下眼睑的坐标点会贴在一起但这种几何变化是连续的、可度量的不会像 Haar 特征那样直接丢掉目标——疲劳检测要的恰恰是这种连续量化能力。与 68 点模型对应的还有 5 点模型和 194 点模型后者精度高但模型体积和推理耗时都更大。疲劳检测这种实时场景68 点模型在精度和速度之间是相对平衡的选择我之前在普通 i5 笔记本上跑CPU 单帧处理大约只要 20 到 30 毫秒配合 30fps 的 USB 摄像头完全没有压力。论文采用的就是这套模型对树莓派这类嵌入式设备也足够友好。2.3 环境准备与模型文件加载正式写代码之前先把环境搭好。需要的东西很常规dlib、opencv-python、numpy外加一个关键模型文件 shape_predictor_68_face_landmarks.dat。这个模型文件是 dlib 官方训练好的关键点检测器下载后放到项目目录即可。dlib 的安装在不同系统上体验差异很大。Windows 下直接 pip install dlib 经常要现场编译耗时长还容易翻车建议先找对应 Python 版本的预编译 wheel或者用 conda install -c conda-forge dlib 走 conda 源Linux 和 macOS 下一般需要先装好 cmake 和编译工具链再 pip 安装。装完后做个最小验证python -c import dlib; print(dlib.__version__)能看到版本号就说明安装成功。接着加载检测器和模型顺便验证模型能跑通import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 用一张正脸照片验证检测链路 import cv2 img cv2.imread(test_face.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) print(f检测到 {len(faces)} 张人脸) shape predictor(gray, faces[0]) print(f特征点数量: {shape.num_parts})参数说明detector(gray, 1) 里的数字是人脸框检测的 upsampling 次数1 表示放大一次再检测小脸命中率更高代价是耗时增加shape.num_parts 返回关键点个数正常应该是 68。第一次跑的时候建议把每个点的坐标打印出来画在图上和模型区间对照一遍这一步能帮你把后面所有的索引错乱问题消灭在源头。3. EAR 值的计算逻辑用六个点量化眼睛开合程度3.1 EAR 的几何定义与公式拆解眼部疲劳识别的核心是 EAREye Aspect Ratio眼睛纵横比。它的思路非常朴素睁眼时上下眼睑距离大闭眼时距离小用几个特征点之间的欧氏距离做比值就能把开合程度变成一个数字。以右眼为例取 36-41 六个点其中 36 是外眼角39 是内眼角37、38 在上眼睑40、41 在下眼睑。EAR 的计算方式是先求上眼睑两个点到下眼睑对应点的垂直距离再求内外眼角的水平距离最后做除法EAR (|P37 - P41| |P38 - P40|) / (2 * |P36 - P39|)分子里两个距离取平均是为了抵消上下眼睑弧度不对称带来的偏差分母取内外眼角距离起到归一化作用。这五个距离都是像素值但注意它们做的是比值运算因此摄像头离人脸远一点近一点所有距离等比例缩放EAR 值基本不变——这就是论文里强调的对摄像头和眼睛距离不敏感的来源。从数学上看这个比值为什么能抵抗距离变化假设摄像头从距离 d 移到 2d整个人脸在画面上等比例缩小一半五个距离都变成原来的一半分子分母同乘 0.5比值不变。这就是 EAR 作为相对指标的核心优势它把绝对尺度去掉留下的是纯粹的几何比例。这也是为什么 EAR 方案不需要标定摄像头焦距也不需要知道司机离摄像头多远的原因。3.2 睁眼、闭眼状态下的典型数值变化正常睁眼状态下人的眼睛长宽比大体保持在一个稳定区间我实际跑下来多数人分布在 0.25 到 0.35 之间。这个数值个体差异不小有的人天生眼型细长基线就偏低。眨眼发生时上眼睑快速下压两个垂直距离迅速缩小EAR 值会在一个眨眼周期里从 0.3 附近掉到接近 0再迅速弹回。关键现象在于接近 0而不是等于 0。因为眼皮完全闭合时上下特征点并非精确重合实际值通常在 0.05 到 0.1 之间浮动。所以闭眼阈值的设定要在睁眼基线和闭眼峰值之间取一个中间值既不能太靠近睁眼基线导致误判也不能太靠近 0 导致真正闭眼时漏判。论文里提到用 EAR1 和 EAR2 分别表示左右眼的张开程度因为正常情况下两只眼的动作是同步的所以最终取两者的平均值作为整体开合度。这个平均操作还有额外好处单侧眼被遮挡或者特征点跳动时平均值不至于瞬间崩坏。如果只跟踪一只眼睛一旦司机侧脸挡住另一侧系统就会完全失效。初期调试的时候我建议在代码里把左右眼的 EAR 值直接 print 出来对着视频观察数值变化。正常睁眼时两个 EAR 应该比较接近如果其中一个明显偏低说明该侧特征点定位有问题眨眼瞬间数值快速下探再回升这个波形越陡峭说明特征点跟踪越稳定。3.3 闭眼阈值与连续帧阈值的设定规则两个阈值最让人头大闭眼阈值EAR_THRESH和疲劳阈值FRAME_THRESH。主流方案里 EAR_THRESH 取 0.2 或 0.25 起步我用 0.22 作为初始值跑大部分摄像头都还算稳当。但如果测试目标是个体差异较大的多人场景固定阈值就可能出问题更稳妥的做法是先让系统采集前几十帧的正常睁眼状态计算个人的 EAR 基线再按基线的 70% 左右设定闭眼阈值。FRAME_THRESH 的设定要结合摄像头帧率。这个阈值表达的是连续多少帧 EAR 低于闭眼阈值就报警本质上是把闭眼持续时间转换成帧数。假设摄像头 30fps人眼一次正常眨眼大约 0.1 到 0.2 秒只占 3 到 6 帧而疲劳闭眼的持续时间往往超过 0.5 秒对应 15 帧以上。把 FRAME_THRESH 设为 15就能天然过滤掉正常眨眼并捕捉到疲劳闭眼。换算逻辑很简单FRAME_THRESH fps × 期望闭眼秒数。比如 30fps 下想检测 0.5 秒以上的持续闭眼阈值就设 15摄像头换成 15fps同样的判定标准应减到 8 帧左右。参数调整有个优先级顺序需要遵守先验证检测链路是否正确再调 EAR_THRESH最后才动 FRAME_THRESH。很多人一上来就调 EAR_THRESH发现效果不佳又去改 FRAME_THRESH两个参数互相拉扯越调越乱。4. 疲劳驾驶检测系统实现从视频帧到报警输出的完整链路4.1 整体流程拆解论文里的六步走论文把系统流程整理得很清晰我在复现时基本按它的顺序走并补了几个工程细节。第一步把视频帧转成灰度图灰度化能减少颜色通道的冗余计算同时 dlib 的检测器本身也是基于灰度特征训练的彩色图送进去反而多余第二步加载 dlib 的人脸检测器和特征点检测器第三步在灰度帧上检测人脸并生成 68 个特征点第四步提取眼部区域的 12 个点分别计算左右眼的 EAR 并取平均第五步把 EAR 和闭眼阈值比较小于阈值计数器加一否则清零第六步当计数器超过疲劳阈值时触发警告。这段流程里有两个容易被忽略的工程细节。一是灰度化之后不要做额外的图像缩放检测器的分辨率适应能力有限过小的输入图会让小脸直接检测不到过大的图又拖慢帧率二是检测器每帧都全程跑会很耗时我一般会先把人脸框检测出来再用检测到的人脸区域去跑关键点定位这个顺序省下来的计算量相当可观。4.2 核心代码人脸检测、特征点提取与 EAR 计算下面是系统的主循环骨架包含了视频读取、灰度化、人脸检测、特征点提取和眼睛坐标抽取这一段是整条链路的起点import cv2 import dlib import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 右眼索引 36-41左眼索引 42-47 RIGHT_EYE list(range(36, 42)) LEFT_EYE list(range(42, 48)) cap cv2.VideoCapture(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) # 把 68 个点转成 (68, 2) 的坐标矩阵后续取眼睛区间的数值 coords np.array([[p.x, p.y] for p in shape.parts()]) right_eye coords[RIGHT_EYE] left_eye coords[LEFT_EYE] # 可视化用画出眼睛轮廓方便确认取点是否正确 cv2.polylines(frame, [right_eye], True, (0, 255, 0), 1) cv2.polylines(frame, [left_eye], True, (0, 255, 0), 1) cv2.imshow(Eye Tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()参数说明detector(gray, 0) 的第二个参数我改成 0 了意思是原图直接检测不再 upsampling保证实时性优先。shape.parts() 返回 68 个 point 对象转成 numpy 矩阵后按行索引取眼点区间最方便。cv2.polylines 只是调试用的可视化正式跑疲劳检测时可以根据需要关掉。再来看 EAR 的计算函数这段是核心中的核心def eye_aspect_ratio(eye): # 参数 eye 是六个点组成的 (6, 2) 坐标数组顺序按模型固定 # 上下眼睑的纵向距离点 1 到点 5点 2 到点 4 vertical_1 np.linalg.norm(eye[1] - eye[5]) vertical_2 np.linalg.norm(eye[2] - eye[4]) # 内外眼角之间的横向距离点 0 到点 3 horizontal np.linalg.norm(eye[0] - eye[3]) # 两个纵向距离取平均后归一化 return (vertical_1 vertical_2) / (2.0 * horizontal)逻辑说明函数接收的 eye 是一个 (6, 2) 的数组六个点的排列顺序必须与 dlib 输出一致右眼传 coords[36:42]左眼传 coords[42:48]。np.linalg.norm 计算的是两点之间的欧氏距离本质上是坐标差的模长。vertical_1 对应公式里的 37-41 点距离vertical_2 对应 38-40 点距离horizontal 是内外眼角距离。返回值即为这只眼睛的 EAR数值范围正常在 0 到 0.4 之间。4.3 疲劳判定计数器逻辑与报警触发拿到左右眼各自的 EAR 之后按论文的思路取平均然后进入计数器判定环节EAR_THRESH 0.22 # 闭眼阈值小于该值视为闭眼 FRAME_THRESH 15 # 连续闭眼帧数阈值超过则判定疲劳 frame_counter 0 # 连续闭眼帧计数器 # 循环视频帧每帧先算 ear再进入以下判定逻辑 # ear (eye_aspect_ratio(right_eye) eye_aspect_ratio(left_eye)) / 2.0 if ear EAR_THRESH: frame_counter 1 if frame_counter FRAME_THRESH: cv2.putText(frame, FATIGUE DETECTED!, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 2) else: frame_counter 0逻辑说明frame_counter 记录连续闭眼的帧数一旦 EAR 恢复正常就立刻清零这样做的好处是中间有零星几帧误检不会累积误报。报警之后计数器不用重置只要眼睛一直闭着报警会持续显示等到睁开眼睛EAR 回到阈值以上计数器清零报警自动解除。这里有个设计细节值得注意报警发生在 frame_counter FRAME_THRESH 的同一帧也就是说从闭眼开始到报警触发系统会有约 0.5 秒的观察期。这 0.5 秒不是延迟而是有意为之的滤波窗口它把正常的生理性眨眼和病理性闭眼区分开。如果你觉得报警太灵敏或太迟钝优先调整 FRAME_THRESH而不是去动 EAR_THRESH。5. 避坑排查眼动检测常见问题的现象原因与处理5.1 特征点索引取错导致左右眼数据混淆现象算出的人眼 EAR 数值忽高忽低眼睛明明睁着却显示闭眼或者反过来闭眼状态下 EAR 不下降。原因68 点模型里右眼是索引 36-41左眼是 42-47这个顺序是按观察者视角定义的。相当多的人在复现时会按屏幕左边就是左眼的习惯把区间对调导致两只眼睛的坐标串位EAR 计算完全失真。解决第一次跑通时把所有 68 个点 draw 在图片上逐一核对下标。标准 dlib 模型中36 号点是脸部右侧的右眼外眼角不是屏幕视觉上的左边。写好代码后先用单张图片而不是视频流验证确认左右眼轮廓分别正确闭合。5.2 逆光、暗光下人脸检测直接丢框现象室内正常光线下系统一切正常一到车窗侧光、夜间照明或者太阳直射背光的场景detector 返回空列表代码直接跳过人脸区域疲劳检测全程瘫痪。原因dlib 的人脸检测基于 HOG 特征它描述的是局部梯度方向分布。强逆光会让人脸整体欠曝皮肤纹理消失梯度信息被噪声淹没夜间暗光下信号本身太弱HOG 特征无从谈起。这属于检测器的工作原理限制不是代码 bug。解决在送进检测器之前先做直方图均衡化提升暗部细节或者用 gamma 校正压低过曝区域。如果再不行退一步用检测框锁定策略连续多帧检测不到人脸时沿用上一帧的人脸框位置继续往下游跑让系统不至于瞬间失效。5.3 戴眼镜场景下 EAR 曲线出现周期性尖峰现象驾驶员佩戴镜框或深色镜片EAR 值在稳定区间内周期性向下跳变偶尔还触发误报警摘下眼镜后一切正常。原因镜框边缘、镜片反光在 HOG 特征下会形成虚假的边缘响应导致关键点定位在镜框和真实眼睑之间跳动。解决先做图像预处理用高斯模糊轻度抑制高频噪点再送检测器其次把 EAR_THRESH 略微调低牺牲一点灵敏度换取稳定性。如果镜框反光太严重可以考虑把检测区域裁剪到眉骨以下再算 EAR减少镜框对关键点的拉扯。5.4 固定阈值在多人使用场景下频繁误报现象一个人测试时表现很好换一个人坐上去就频繁报警或者全程无报警。原因人与人之间的眼部形态存在明显差异睁眼基线 EAR 有人 0.28有人只有 0.2。固定阈值设高了对低基线人群就是灾难设低了又对高基线人群不敏感。这是 EAR 方案最典型的调参玄学。解决加一段开机自校准流程系统启动后先检测到人脸取前 50 帧正常睁眼状态下的 EAR 均值按均值的 70%-80% 动态生成每个人的闭眼阈值。这样每个用户上车时只需正常睁眼面向摄像头一两秒参数就自动就位。5.5 帧率波动导致计数窗口失真现象报警时机飘忽不定有时刚闭眼 0.3 秒就报有时闭眼快 1 秒都没反应。原因FRAME_THRESH 是按固定帧率假设的整数帧数但摄像头在实际运行中帧率并不恒定系统负载高、USB 带宽挤压都会掉帧。帧率变低时同一时间跨度只有更少的帧进入计数器报警延迟变大。解决把计数逻辑从连续帧数改成持续时间窗口。记录闭眼起始的视频时间戳当 (当前时间戳 - 起始时间戳) 超过设定秒数时再报警。时间戳窗口和帧率解耦无论 30fps 还是 15fps判定标准保持一致。6. 收尾用回放视频验证判定逻辑把阈值调校从玄学变成数据活系统主流程跑通之后我发现最大的问题不是算法本身而是不知道阈值到底定得对不对——眼睛睁开多少算正常、闭眼多久算疲劳直接靠肉眼对着视频窗口看很难判断系统迟报早报。后来我把调试方式改成录制回放 EAR 曲线可视化两步走才真正把参数调校从玄学变成了可量化的数据活。具体做法分三步。第一步录一段模拟驾驶场景的视频包含三种状态的片段正常驾驶约 30 秒、频繁眨眼 20 秒、持续闭眼 15 秒尽量在真实的光线条件和头部姿态下录制第二步写一个离线的回放脚本对视频逐帧计算 EAR并把每帧的时间戳、EAR 值、判定状态写进日志文件第三步把日志里的 EAR 序列画成曲线图对照视频内容检查阈值切得是否合理。import cv2 import dlib import numpy as np cap cv2.VideoCapture(sim_drive.mp4) fps cap.get(cv2.CAP_PROP_FPS) frame_idx 0 while True: ret, frame cap.read() if not ret: break # 计算 ear 的过程和实时检测一致此处省略 t frame_idx / fps print(f{t:.2f}s EAR{ear:.3f} state{state}) frame_idx 1我在这个阶段发现过几次真实的问题。曲线图上能清楚看到戴眼镜时的 EAR 尖峰大约每 20 帧出现一次这让我确定是反光干扰而不是真实眨眼换人测试时基线从 0.28 掉到 0.21也让我意识到固定阈值撑不住多人场景才下决心加开机自校准。从那以后我每次调整完检测逻辑都会强制走一遍录制回放看完整段曲线确认关键帧全部命中再上实时摄像头验证一次整个过程控制在十分钟以内。这套工作流不复杂但对参数调校和边界确认的帮助非常直接希望帮到你。本文还有配套的精品资源点击获取
返回列表