拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从MTCNN到PERCLOS:疲劳驾驶检测技术链路实战解析

简介面向智能交通、自动驾驶及计算机视觉领域的研究者与工程师这份PDF文档系统介绍了一种基于深度学习的疲劳驾驶检测方法。文档首先概述疲劳驾驶的现实危害对比主观法与客观法的适用场景随后重点阐述基于MTCNN的人脸检测、FEL眼睛精定位、OC-Net眼状态判定以及结合PERCLOS算法与眨眼频率的疲劳判定流程实验表明该方法识别准确率达97.18%且具备实时性与鲁棒性。资源文件为1个PDF大小约2.69MB除核心方法、网络结构与实验对比外还附有参考文献同时包含清晰的算法流程与实验数据适合用于快速复现实验或作为论文写作的支撑资料。目前已有325人学习下载适合作为学术论文阅读、算法方案设计或毕业设计参考能够帮助读者建立完整的基于深度学习的疲劳检测技术框架。1. 从论文到可落地的疲劳驾驶检测这条技术链路值得按图索骥疲劳驾驶导致的交通事故占比超过 42%这个数字在真实路测里只高不低。市面上多数检测方案要么依赖脑电、心电等穿戴设备要么靠方向盘转角、车道偏移等间接信号前者成本高、佩戴别扭后者误报率受车型和驾驶习惯影响极大。桂林电子科技大学这篇 2020 年的论文给出了一条更实用的技术路径MTCNN 做实时人脸检测FEL 网络做眼睛精定位OC-Net 判断眼睛开闭状态最后用 PERCLOS 算法与眨眼频率联合判定疲劳程度。整套方法在论文实验中的疲劳状态检测准确率达到 97.18%并且满足实时性约束。如果你正在做疲劳驾驶检测、驾驶员监控系统或者边缘端视觉识别相关的项目这篇论文的模块拆分和级联思路可以直接拿来当设计蓝本。它的价值不在于某个单点模型有多新而在于把「人脸检测—眼睛定位—状态分类—时序判疲劳」这条链路完整打通了每一段都有可参考的工程参数。2. 网络结构选型为什么是 MTCNN、FEL 与 OC-Net 的组合2.1 MTCNN 做人脸检测的工程优势MTCNNMulti-task Cascaded Convolutional Networks是 2016 年提出的联合人脸检测与对齐模型它由 P-Net、R-Net、O-Net 三个级联网络组成。三级级联的核心逻辑是先粗后精P-Net 通过全卷积网络快速生成候选窗和边界框回归向量并用 NMS 合并重叠区域这一步追求召回率R-Net 对候选窗做精细筛选进一步排除非人脸区域O-Net 输出最终人脸框的同时还能定位双眼、鼻子、嘴角两侧共 5 个人脸特征点。这套设计在疲劳驾驶检测场景里非常合适。驾驶员处于固定座姿脸框尺度变化不大但光照、眼镜反光、头部偏转带来的干扰很常见。MTCNN 的三级级联让计算量集中在 P-Net 的浅层特征上只有候选区域才会进入 R-Net 和 O-Net整体推理速度比单阶段大模型快得多。论文中 O-Net 还提供了眼睛初始位置这为后续的 FEL 精定位奠定了基础。# MTCNN 推理逻辑示意三级级联的输入输出关系 import cv2 from mtcnn import MTCNN detector MTCNN() frame cv2.imread(driver_frame.jpg) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 返回人脸框、置信度和 5 个关键点坐标 results detector.detect_faces(rgb) for res in results: x, y, w, h res[box] conf res[confidence] keypoints res[keypoints] # left_eye, right_eye, nose, mouth_left, mouth_right print(fface box: ({x}, {y}, {w}, {h}), confidence: {conf:.3f}) print(fright eye: {keypoints[right_eye]}, left eye: {keypoints[left_eye]})参数说明detect_faces返回每个人脸的边框box、置信度confidence和关键点字典keypoints。实际工程中会过滤掉置信度低于 0.9 的框因为疲劳检测要求眼睛区域输入足够清晰误检带来的人脸框会直接污染后续眼睛状态分类。P-Net 的滑动窗口步长和 NMS 阈值会影响召回率与误检率论文场景里建议把 NMS 阈值设在 0.7 左右既能合并重叠框又不至于把相近的人脸合并掉。2.2 FEL 网络解决遮挡与姿势变化下的眼睛精定位MTCNN 输出的眼睛关键点只是粗定位。真实行车环境中驾驶员低头、转头、手臂遮挡、墨镜反射都会让粗定位的眼睛框偏移如果直接把这块区域送进分类网络眼睛内容可能残缺甚至错位。这篇论文的解决方案是设计一个 FELFine Eyes Location网络思路是引入多任务学习把眼睛定位作为主任务把遮挡判断和头部姿势判断作为辅助任务。多任务学习的核心公式这里展开一下。传统多任务损失是各个任务损失直接求和但 FEL 的做法是给辅助任务加权重系数 λ并且用 early stop 机制控制辅助任务的训练时机L(w) 0.5 * sum(||y_r - f(x_i; w_r)||^2) - sum( sum( λ_a * y_a * log(p(y_a | x_i; w_a)) ) ) sum(||w||^2)第一项是主任务眼睛坐标回归的均方误差第二项是辅助任务遮挡分类、头部姿势分类的交叉熵损失第三项是正则化项。关键点在于辅助任务不是全程都参与训练而是当它的验证集误差不再下降时提前终止。论文里 early stop 的判定基于训练误差变化趋势和泛化误差的比较超过阈值 ε 就结束该辅助任务的学习。这样设计的原因很直接遮挡分类和姿势分类在训练初期能帮助网络学到鲁棒的眼睛特征表示但训练到后期继续优化辅助任务反而可能干扰主任务的回归精度。我实际复现这类多任务结构时一般把 λ 初始设为 0.1然后按验证集的主任务误差调整。如果你用 PyTorch 实现可以在每个 epoch 结束后同时评估主任务和辅助任务的 loss辅助任务连续若干 epoch比如 5 个没有下降就将其梯度清零。FEL 的输入是 40×40 像素的灰度人脸图像网络包含 4 个卷积层、3 个池化层和 1 个全连接层激活函数用的是绝对切线函数。从表 1 的对比数据看FEL 模型参数量约为 10.18 万级联 CNN 约为 99.12 万前者的推理耗时只有后者的约六分之一。参数量少一个数量级、计算量更低这意味着在嵌入式设备和车载 Jetson 平台上跑实时检测是完全可行的。2.3 OC-NetLeNet-5 变体做眼睛状态识别眼睛开闭状态分类属于典型的二分类图像任务不需要特别深的网络。OC-Net 是 LeNet-5 的变体论文用它来判定眼睛区域图像是睁开还是闭合。LeNet-5 的经典结构是卷积—池化—卷积—池化—全连接识别 MNIST 手写数字能达到 99.2% 的准确率对 40×40 或者稍大尺寸的眼睛图像完全够用。选择 LeNet-5 变体而非 ResNet 这类深层网络背后是数据量和实时性的权衡。眼睛图像经过 FEL 裁剪后内容高度归一化眼睛位于图像中心背景占比小类内差异集中在虹膜区域、眼睑张合程度和睫毛形态上。深层网络在这种小尺寸、结构简单的图像上不仅收益有限还会显著增加推理延迟。疲劳驾驶检测的实时性要求通常在 20—30 FPS 以上单帧眼睛分类耗时必须控制在毫秒级。# OC-Net 网络结构定义PyTorch 风格 import torch.nn as nn class OCNET(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 6, kernel_size5, padding2), # 输入 40x40 灰度图 nn.ReLU(inplaceTrue), nn.AvgPool2d(kernel_size2, stride2), # 20x20 nn.Conv2d(6, 16, kernel_size5), # 16x16 nn.ReLU(inplaceTrue), nn.AvgPool2d(kernel_size2, stride2) # 8x8 ) self.classifier nn.Sequential( nn.Linear(16 * 8 * 8, 120), nn.ReLU(inplaceTrue), nn.Linear(120, 84), nn.ReLU(inplaceTrue), nn.Linear(84, 2) # 0闭合, 1睁开 ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)代码里注意三点。第一输入是单通道灰度图论文的 FEL 输入也是灰度图整个链路里不需要彩色信息灰度图还能减少计算量和内存占用。第二池化层使用平均池化而非最大池化因为眼睛状态分类需要保留整体的灰度分布特征最大池化会过度强调局部纹理在低分辨率眼睛图像上反而容易引入噪声。第三最后的全连接层输出 2 个节点对应闭合和睁开两个类别训练时用交叉熵损失函数。从表 2 的结果看OC-Net 在 ZJU 和 DDR 数据集的睁眼/闭眼准确率都在 96.87% 以上说明这个轻量级结构对眼睛状态识别的区分度足够。3. 疲劳判定的两个核心指标PERCLOS 与眨眼频率如何配合3.1 PERCLOS 计算的工程定义PERCLOSPercentage of Eyelid Closure over the Pupil over Time是衡量单位时间内眼睛闭合时间占比的指标计算公式很简单P n / N × 100%其中 n 是闭眼状态的帧数N 是统计窗口内的总帧数。论文引用的文献里给出了两个关键阈值R2 0.075 和 R1 0.150。工程上通常配合视频帧率来解读这两个数值。假设摄像头帧率是 30 FPS统计窗口为 60 秒即 1800 帧P 值超过 7.5% 意味着累计闭眼时间超过 4.5 秒超过 15% 则意味着累计闭眼时间超过 9 秒。需要强调的是PERCLOS 本身是一个时间段内的积分指标单帧闭眼并不代表疲劳只有眼睛闭合在时间维度上持续累积到一定比例才进入疲劳判定。这就是为什么必须配合时序窗口来计算而不是对每一帧做独立判断。3.2 眨眼频率的补充作用仅靠 PERCLOS 会有两个盲区。第一种情况是驾驶员频繁短眨眼单次闭眼时间短PERCLOS 值可能不超标但眨眼频率异常本身就反映出神经系统疲劳。第二种情况是固定阈值无法适配个体差异有人天生眨眼频率高有人习惯性眯眼。论文的做法是把眨眼频率阈值设定为 t1 10与 PERCLOS 联合判定。具体逻辑是条件判定结果P R1即 P 0.150直接判定疲劳R2 P R1 且眨眼频率 tbf t1判定疲劳P R2 且眨眼频率正常判定清醒这个「主指标过阈值直接判主指标临界时用辅助指标兜底」的思路在工程上非常实用。直接判定条件保证了对严重疲劳的快速响应联合判定条件则提高了临界状态的检出率减少了漏报。# PERCLOS 眨眼频率联合判定逻辑 def judge_fatigue(eye_states, fps30.0, window_sec60, thr_r10.150, thr_r20.075, blink_thr10): eye_states: 最近窗口内每帧的眼睛状态1 表示睁开0 表示闭合 返回: (是否疲劳, PERCLOS值, 眨眼频率) total len(eye_states) if total 0: return False, 0.0, 0.0 closed_frames total - sum(eye_states) perclos closed_frames / total # 统计眨眼次数睁-闭-睁 视为一次完整眨眼 blink_count 0 prev_open True in_blink False for s in eye_states: if prev_open and s 0: in_blink True elif in_blink and s 1: blink_count 1 in_blink False prev_open bool(s) # 折合成每分钟眨眼频率 tbf blink_count * 60.0 / window_sec if perclos thr_r1: return True, perclos, tbf if thr_r2 perclos thr_r1 and tbf blink_thr: return True, perclos, tbf return False, perclos, tbf判定逻辑的参数说明窗口长度 window_sec 决定了疲劳检测的响应时间窗口越短报警越灵敏但瞬时闭眼比如看后视镜、擦汗更容易误报窗口越长统计越稳但发现疲劳可能不够及时。我一般会在 30—60 秒之间做可配置项。眨眼频率统计要特别注意眨眼边界的定义必须用「睁眼—闭眼—睁眼」的完整状态切换算一次否则容易把连续闭眼的多帧误计为多次眨眼。上面代码里的in_blink标志就是为了避免这种情况。实际部署时眼睛状态数组用环形缓冲区维护只保留最近窗口的数据避免内存无限增长。# 环形缓冲区示例只保留最近 60 秒的状态 # 假设帧率 30FPS缓冲区容量 30 * 60 1800 BUFFER_SIZE 1800 eye_state_buffer [0] * BUFFER_SIZE4. 用 ANE 与验证集评估模型性能的几个关键动作4.1 FEL 网络的多任务消融验证论文对 FEL 网络做了三组变体实验FELpose加头部姿势辅助任务、FELglasses加遮挡辅助任务、FELall同时加两个辅助任务。评估指标是平均误差即估计眼睛位置与真实位置的距离、并除以眼间距离做归一化。对比结果里有几个值得提取的信息第一FELall 的精度显著优于单一 FEL说明遮挡和姿势两个辅助任务的联合监督能带来互补信息。第二FEL 与级联 CNN 对比时不仅在精度上更优参数量还从约 99 万降到约 10 万推理耗时从约 20ms 降到约 1ms。这个对比给我们的启发是多任务学习中的辅助任务不要盲目堆叠要通过消融实验逐个验证增益每加一个辅助任务都要看主任务误差是否真的下降同时记录参数量和计算量的增幅。如果你要复现这个消融实验关键动作是固定训练集和验证集划分确保每个变体使用相同的数据增强策略。AFLW 数据集的原始标注包含人脸框和局部特征点但论文提到需要手动修改标注来生成遮挡和姿势的辅助任务标签这在实际操作中成本较高。一个变通做法是用现成的人脸姿态估计模型如 HopeNet生成粗标签再用小规模人工修正来保证质量。4.2 OC-Net 的准确率与泛化能力评估OC-Net 的训练使用了 ZJU 和 DDR 两个数据集。ZJU 数据集是自然光环境下的扩展眼睛图像DDR 数据集来自实际驾驶环境。从表 2 的数据看ZJU 和 DDR 的睁眼准确率分别为 97.80% 和 98.35%闭眼准确率分别为 96.87% 和 97.88%两个数据集的准确率差距很小说明模型在实际驾驶场景中具备初步的泛化能力。表 3 给出了 OC-Net 与手工特征方法的对比核心指标有两个——AVE 和 AUC。Gabor、LBP、HOG 三种特征配合 SVM 的 AVE平均验证误差都在 91—94% 之间AUC 在 96—98% 之间OC-Net 的 AVE 达到 97.51%AUC 达到 99.60%。这里要注意指标的差异AVE 是分类准确率的直接度量AUC 衡量的是模型在不同阈值下的综合分类能力。OC-Net 在两个指标上都领先验证了深度特征相比手工特征在眼睛状态分类上的优势。模型收敛观察值得单独说一句。论文里提到迭代 10^4 次后使用 model.ckpt 文件进行测试。训练过程中准确率曲线呈先快速上升、后平缓爬升的趋势。实际复现时我会额外监控训练集和验证集准确率的差距如果训练集继续上升但验证集停滞说明开始过拟合应当提前保存当前最优权重而不是等待训练结束。TensorFlow 的训练代码如下# TensorFlow 2.x 训练 OC-Net 并保存 checkpoint import tensorflow as tf model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), losstf.keras.losses.SparseCategoricalCrossentropy(from_logitsTrue), metrics[accuracy] ) callbacks [ # 每 1000 步保存一次权重 tf.keras.callbacks.ModelCheckpoint( model.ckpt, save_weights_onlyTrue, save_freq1000 ), # 验证集准确率连续 5 轮不提升则提前停止 tf.keras.callbacks.EarlyStopping( monitorval_accuracy, patience5, restore_best_weightsTrue ) ] model.fit( train_dataset, validation_dataval_dataset, epochs20, callbackscallbacks )参数说明Adam 优化器的学习率设置在 1e-4 附近比较稳妥过大的学习率会让 loss 震荡过小则收敛速度太慢在疲劳检测这种二分类任务上 1e-4 是个安全的起点。save_freq1000表示每训练 1000 步保存一次权重方便回溯中间状态。EarlyStopping的patience5允许验证集准确率在最多 5 个 epoch 内不提升才停止避免因局部波动提前终止。4.3 端到端疲劳检测的验证方法论文最后把 3 个测试视频的人工统计疲劳次数与算法检测次数做了对比平均准确率为 97.18%。这种验证方式的核心在于人工标注的疲劳时刻要与算法输出的疲劳时刻对齐——不是只看最终次数相等还要看每次判定的时间点是否接近。# 端到端评估比较算法输出与人工标注的时间重叠度 def evaluate_detection(detected_epochs, ground_truth_epochs, tolerance_sec10): detected_epochs: 算法判定为疲劳的时间点列表秒 ground_truth_epochs: 人工标注的疲劳时间点列表秒 tolerance_sec: 时间容差 matched 0 used [False] * len(ground_truth_epochs) for det in detected_epochs: for i, gt in enumerate(ground_truth_epochs): if not used[i] and abs(det - gt) tolerance_sec: matched 1 used[i] True break precision matched / len(detected_epochs) if detected_epochs else 0 recall matched / len(ground_truth_epochs) if ground_truth_epochs else 0 accuracy matched / max(len(detected_epochs), len(ground_truth_epochs)) return precision, recall, accuracy时间容差 tolerance_sec 的设定取决于应用场景。论文表 4 的准确率计算方式是按视频整体疲劳次数对比这属于宽松判定如果做严格评估我会把容差缩到 5 秒以内要求算法在人工标注的疲劳时刻前后 5 秒内给出报警。这时准确率通常会比论文报告的 97.18% 低一些属于正常现象因为帧级对齐比次数对齐严格得多。5. 复现时最容易踩的三个坑与对应解法5.1 眼睛图像裁剪过大会引入背景噪声MTCNN 输出的眼睛关键点坐标直接用来裁剪眼睛区域时需要控制裁剪框的尺度。如果裁剪框设置得过大会把眉毛、额头甚至部分脸颊包含进来这些背景纹理对 OC-Net 是噪声如果裁剪过小可能会截掉部分眼睑导致信息不完整。问题表现对应解法裁剪过大眼睛分类准确率下降以眼间距离为基准裁剪框宽度设为 1.2 倍眼间距、高度设为 0.8 倍眼间距裁剪过小边缘眼睛信息缺失保证裁剪框中心与眼睛关键点精确对齐灰度归一化不统一同一驾驶员不同光照下识别结果波动每个裁剪区域独立做归一化使用全局均值和方差灰度归一化这一步容易被忽略。不同光照条件下的眼睛图像灰度分布差异很大如果按全图统计归一化裁剪区域内的对比度可能被压缩。我通常先裁剪、后归一化让每个眼睛图像自己算均值和方差这样能最大程度保留眼睛区域的局部对比度。5.2 PERCLOS 的阈值需要按场景做标定论文引用的 R20.075 和 R10.150 来自公开文献但实际部署时不能直接照搬。不同摄像头帧率下同样 60 秒窗口内闭眼帧数的分布不同不同驾驶员的眼睛闭合幅度、眯眼习惯也存在差异。如果采集到对应场景的少量标注数据可以用网格搜索的方式做阈值标定# 用网格搜索微调 PERCLOS 阈值 best_thr (0.075, 0.150) best_f1 0 for r2 in [0.05, 0.06, 0.075, 0.09]: for r1 in [0.12, 0.15, 0.18]: # 在验证集上计算检测结果的 F1 分数 f1 calculate_f1(r2, r1, val_dataset) if f1 best_f1: best_f1 f1 best_thr (r2, r1)网格范围不需要太大在公开文献给出的基准值附近做小范围微调即可。疲劳状态类样本通常远少于清醒状态评估时建议用 F1 分数而非单纯准确率推荐机制里闭眼样本被漏报的代价远高于误报。5.3 在边缘设备部署需要同时考虑推理时间和帧间稳定性基于深度学习的疲劳驾驶检测从文中的实验环境迁移到真实车载环境时面临的问题往往不在网络层而在链路稳定性。MTCNN 偶尔会出现连续几帧检测不到人脸低头看手机、转头看后视镜这时眼睛状态序列出现空洞PERCLOS 计算就会失真。# 帧丢失时的处理策略用最近有效状态填充 # 1. 记录每帧的人脸检测结果和眼睛状态 # 2. 若连续丢帧超过 0.5 秒暂停 PERCLOS 窗口推进 # 3. 恢复检测后重新从缓冲区尾部继续累计我一般会在缓冲区里同时存人脸检测标志位。如果连续超过 15 帧0.5 秒没有人脸就认为驾驶员处于低头或遮挡状态当前时间窗口内不累计闭眼帧避免把丢失帧当作稳定帧处理。此外MTCNN 输出的人脸框如果出现抖动会导致裁剪出的眼睛区域内容跳动这种噪声会干扰 OC-Net 的分类稳定性。一个简单的平滑做法是使用指数移动平均处理眼睛关键点坐标相邻帧的关键点位置不会因为单帧检测波动而剧烈跳变。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门