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

文章详情

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

基于深度学习的疲劳驾驶检测系统:从摄像头到报警的工程链路

基于深度学习的疲劳驾驶检测系统:从摄像头到报警的工程链路 简介这是一套面向深度学习初学者与计算机视觉方向开发者的智能疲劳驾驶检测系统源码以Python为主要语言围绕驾驶员面部与眼部特征实现疲劳状态的实时识别适合课程设计、毕业设计或算法练手场景。压缩包共84个文件、约105.27MB其中34个py源码文件覆盖数据预处理、特征提取、模型训练与预测全流程另有pt与onnx模型文件、11个txt说明与日志、11个pyc字节码、7个jpg与4个webp、4个png图像样本及界面素材以及4个xml配置文件和gitignore等工程辅助文件目录按基础知识、深度学习、numpy基础、face_detect、疲劳检测、注册和识别、语音响应等模块划分结构清晰。已有106人学习下载。读者可据此获得一套可直接运行的完整检测方案理解从人脸检测到疲劳判定的实现链路并在此基础上做二次开发与功能扩展。1. 疲劳驾驶检测系统从摄像头到报警一条可复现的工程链路跑长途的司机都有体会连续开三四个小时眼皮开始发沉反应慢半拍这时候哪怕零点几秒的走神都可能出事。基于深度学习的智能疲劳驾驶检测系统要解决的就是在驾驶过程中实时判断驾驶员是否进入疲劳状态并及时给出预警。它的输入通常是一路车载摄像头画面输出是「清醒 / 轻度疲劳 / 重度疲劳」这类分级判断中间靠的是人脸检测、眼部与嘴部状态识别、头部姿态估计再叠加一个时序判断逻辑。这套东西适合谁做做嵌入式视觉的、做车载电子的、做毕业设计的、想拿一个完整 CV 落地项目练手的都能从这条链路里找到自己的位置。源码层面它一般包含数据预处理、模型定义、训练脚本、推理脚本和报警逻辑五块本文就按这条线把每一步拆开讲清楚让你拿到源码能跑通也能自己改参数。2. 疲劳判据怎么定EAR、MAR 与头部姿态的工程取舍2.1 为什么不能只靠「闭眼」一个特征很多人第一反应是闭眼就是疲劳。真做起来会发现正常眨眼一次大概 100 到 400 毫秒如果只按「眼睛闭合」报警等个红灯、看一眼后视镜都会触发误报率高到没法用。工程上更稳的做法是引入时间维度统计单位时间内的闭眼帧占比也就是 PERCLOSPercentage of Eyelid Closure再配合眨眼频率、打哈欠频率、头部低垂角度一起判断。这就是为什么疲劳检测天然是个时序问题单帧分类模型直接套上去效果往往很差。常见做法是先用一个人脸关键点模型拿到眼睛和嘴巴的坐标再算两个几何指标EAREye Aspect Ratio眼睛纵横比和 MARMouth Aspect Ratio嘴巴纵横比。EAR 在睁眼时大约 0.25 到 0.35闭眼时掉到 0.1 以下MAR 在正常闭嘴时接近 0打哈欠时能到 0.6 以上。这两个阈值不是拍脑袋定的要按摄像头分辨率、驾驶员坐姿、光照条件去标定。2.2 EAR 与 MAR 的计算与阈值标定下面这段是计算 EAR 的核心逻辑用的是 68 点人脸关键点里眼睛的 6 个点。左眼点索引是 36 到 41右眼是 42 到 47顺序是「左角、上左、上右、右角、下右、下左」。import numpy as np def eye_aspect_ratio(eye_points): # eye_points: 形状 (6, 2)顺序为 p1..p6 # 垂直距离上眼睑两点与下眼睑两点 v1 np.linalg.norm(eye_points[1] - eye_points[5]) v2 np.linalg.norm(eye_points[2] - eye_points[4]) # 水平距离左右眼角 h np.linalg.norm(eye_points[0] - eye_points[3]) # 加 1e-6 防止除零 ear (v1 v2) / (2.0 * h 1e-6) return ear def mouth_aspect_ratio(mouth_points): # 取上下唇内侧三点计算张口高度 v np.linalg.norm(mouth_points[2] - mouth_points[10]) h np.linalg.norm(mouth_points[0] - mouth_points[6]) return v / (h 1e-6)逻辑说明EAR 用两组垂直距离的平均值除以水平距离这样对头部轻微左右转动不敏感。参数上1e-6是数值稳定项别省。标定时我一般让测试者睁眼、闭眼、打哈欠各录 10 秒统计 EAR 和 MAR 的分布取闭眼分布的 90 分位作为闭眼阈值睁眼分布的 10 分位作为睁眼阈值中间留一段迟滞区间避免在阈值附近反复跳变。2.3 头部姿态作为辅助判据光看眼睛嘴巴还不够。有些疲劳状态是「眼睛睁着但头一点一点往下栽」这时候 EAR 正常但头部俯仰角pitch持续偏大。用 solvePnP 配合 3D 人脸模型可以估出头部姿态角pitch 超过 15 度且持续 2 秒以上就该计入疲劳评分。把 EAR、MAR、pitch 三个指标加权求和比任何单一指标都稳。权重我一般设 EAR 占 0.5、MAR 占 0.3、pitch 占 0.2具体按你的场景调。3. 模型选型与训练从 MRL Eye 数据集到可部署的轻量网络3.1 数据集准备与标签设计公开数据集里MRL Eye Dataset 和 CEWClosed Eyes in the Wild常被用来做眼睛状态分类前者约 8 万多张眼部图分睁眼闭眼两类。打哈欠数据相对少很多项目会自己补录。标签设计上我建议不要直接做「清醒 / 疲劳」二分类而是做「睁眼 / 闭眼」和「闭嘴 / 张嘴」两个独立二分类再在上层用规则融合。原因是疲劳是个时序累积概念单帧硬分类边界模糊拆成基础状态识别后模型更容易训也更好解释。数据预处理要注意三点一是灰度化减少光照通道干扰二是统一到 24x24 或 32x32眼部区域本身信息量不大太大反而过拟合三是做直方图均衡化应对隧道、夜间等低照度场景。下面是一个用 OpenCV 和 PyTorch 组织数据加载的骨架。import cv2 import torch from torch.utils.data import Dataset class EyeStateDataset(Dataset): def __init__(self, file_list, labels, img_size24): self.file_list file_list self.labels labels self.img_size img_size def __len__(self): return len(self.file_list) def __getitem__(self, idx): img cv2.imread(self.file_list[idx], cv2.IMREAD_GRAYSCALE) # 直方图均衡化缓解低照度影响 img cv2.equalizeHist(img) img cv2.resize(img, (self.img_size, self.img_size)) img img.astype(float32) / 255.0 img torch.from_numpy(img).unsqueeze(0) # 增加通道维 label torch.tensor(self.labels[idx], dtypetorch.long) return img, label参数说明img_size设 24 是速度和精度的平衡点实测 24 和 32 的准确率差距在 1% 以内但推理速度快近一倍。equalizeHist对夜间红外画面提升明显但如果你的摄像头本身带强补光可以关掉否则可能过曝。3.2 轻量 CNN 结构设计与训练参数眼部状态分类不需要 ResNet 这种大网络一个 4 层卷积加 2 层全连接的小网络就够了参数量控制在 10 万以内方便后续往嵌入式端移植。结构上每层卷积后接 BatchNorm 和 ReLU再池化最后全局平均池化接全连接。import torch.nn as nn class EyeNet(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 16, 3, padding1), nn.BatchNorm2d(16), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.classifier nn.Linear(64, num_classes) def forward(self, x): x self.features(x) x x.flatten(1) return self.classifier(x)训练参数上优化器用 Adam学习率 1e-3配合 CosineAnnealingLR 衰减到 1e-5batch size 设 64训练 30 个 epoch 基本收敛。损失函数用 CrossEntropyLoss如果睁眼闭眼样本不均衡闭眼样本通常偏少加一个 class weight。验证集准确率能到 97% 以上但别只看准确率重点看闭眼类的召回率漏检闭眼比误报睁眼后果严重得多。3.3 从单帧模型到时序疲劳评分单帧模型输出的是当前帧的眼睛状态真正判断疲劳要把连续帧串起来。我一般维护一个长度为 90 帧约 3 秒按 30fps 算的滑动窗口统计窗口内闭眼帧占比也就是前面说的 PERCLOS。当 PERCLOS 超过 0.4 且持续两个窗口判定为轻度疲劳超过 0.6 判定为重度疲劳触发报警。打哈欠则单独计数一分钟内超过 3 次也计入疲劳。这套规则逻辑简单但比端到端时序模型好调试出问题能定位到具体是哪一帧判错了。4. 推理部署与报警联动把模型塞进真实车载环境4.1 推理流水线的搭建推理阶段要串起人脸检测、关键点定位、状态分类、时序评分四步。人脸检测用轻量的 YuNet 或 MTCNN关键点用 PFLD 或 dlib 的 68 点模型状态分类用上面训好的 EyeNet。整条流水线要控制单帧耗时在 30 毫秒以内才能保证 30fps 实时。下面是一个推理主循环的骨架。import cv2 import numpy as np def inference_loop(cap, face_detector, landmark, eye_model, mouth_model): window [] # 滑动窗口存每帧疲劳标志 while True: ret, frame cap.read() if not ret: break faces face_detector.detect(frame) for face in faces: pts landmark.predict(frame, face) ear eye_aspect_ratio(pts[36:42]) mar mouth_aspect_ratio(pts[48:68]) # 闭眼标志EAR 低于阈值 eye_closed 1 if ear 0.18 else 0 window.append(eye_closed) if len(window) 90: window.pop(0) perclos sum(window) / len(window) if perclos 0.6: trigger_alarm(frame, levelhigh) elif perclos 0.4: trigger_alarm(frame, levellow) cv2.imshow(fatigue, frame) if cv2.waitKey(1) 0xFF 27: break逻辑说明window长度 90 对应 3 秒perclos是闭眼帧占比。trigger_alarm里做分级报警轻度只亮黄灯或语音提示重度才响蜂鸣器避免频繁打扰。参数上EAR 阈值 0.18 是经验值实际要按你的关键点模型标定不同模型点位定义有差异。4.2 报警策略与误报抑制报警最怕误报司机被吵几次就会把系统关掉。抑制误报有几个实用手段一是加冷却时间一次报警后 30 秒内不重复报二是区分场景检测到车辆静止结合 GPS 或车速信号时降低报警等级三是多指标确认只有 PERCLOS 和打哈欠同时超标才报重度。这些逻辑不复杂但能显著提升实际可用性。我见过不少项目模型指标很漂亮一上车就被误报拖垮问题都出在这一层。4.3 嵌入式端移植的注意点如果目标是往嵌入式平台比如带 NPU 的 SoC移植模型要提前做量化。EyeNet 这种小网络INT8 量化后精度掉不到 1%但速度能翻两三倍。移植时注意算子支持情况AdaptiveAvgPool2d 有些推理框架支持不好可以换成固定尺寸的 AvgPool2d。另外输入分辨率别在运行时动态改固定 24x24 或 32x32避免反复重分配内存。5. 避坑与排查那些让系统上不了车的细节5.1 夜间红外画面下 EAR 整体偏移现象白天标定的 EAR 阈值到夜间红外补光下全部失效睁眼 EAR 只有 0.15系统一直报闭眼。原因红外成像下瞳孔和眼睑对比度变化关键点定位偏移导致 EAR 整体下移。解决分光照条件标定两套阈值或者用自适应阈值取最近 300 帧 EAR 的中位数作为基准动态调整闭眼判定线。5.2 戴眼镜反光导致关键点抖动现象戴眼镜的测试者镜片反光让眼睛关键点在帧间跳变EAR 曲线毛刺严重误报增多。原因反光区域被误识别为瞳孔或眼睑边缘。解决对 EAR 序列做中值滤波窗口取 5 帧能压掉大部分毛刺同时在预处理阶段加一个反光检测反光面积过大时跳过该帧不纳入统计。5.3 头部大幅转动时人脸丢失现象司机扭头看后视镜人脸检测直接丢失系统报「未检测到驾驶员」体验很差。原因正脸检测器对侧脸鲁棒性不足。解决换用支持多角度的检测器或者在人脸丢失时保持上一个疲劳评分不变给一个 2 秒的宽限期超过再报丢失。别一丢脸就重置窗口否则每次扭头回来都要重新累积报警会延迟。5.4 滑动窗口长度设错导致响应迟钝现象疲劳已经很明显了系统还要等好几秒才报警。原因窗口设太长比如 150 帧5 秒累积慢。解决窗口长度按场景调高速长途场景 60 到 90 帧比较合适城市短途可以更短。另外可以加一个快速通道连续 15 帧闭眼直接触发轻度报警不等窗口满。5.5 训练集与实车分布不一致现象模型在测试集上 98%实车上频繁误判。原因训练集多是正面、光照均匀的图实车有侧脸、逆光、夜间红外分布差异大。解决拿实车数据做一轮微调哪怕只有几百张效果提升也很明显。这一步没有捷径血泪经验就是实验室指标再高不上车跑一圈都不算数。6. 进阶技巧用知识蒸馏把大模型压进车载芯片前面 EyeNet 已经够小但如果你想让准确率再上一个台阶可以先训一个大的教师模型比如 ResNet18 改输入通道再用知识蒸馏把能力迁移到小模型上。做法是让小模型同时拟合真实标签和教师模型的软标签损失函数里加一个温度系数 T 和权重 alpha。import torch.nn.functional as F def distillation_loss(student_logits, teacher_logits, labels, T4.0, alpha0.7): # 软标签损失学生模仿教师的输出分布 soft_loss F.kl_div( F.log_softmax(student_logits / T, dim1), F.softmax(teacher_logits / T, dim1), reductionbatchmean ) * (T * T) # 硬标签损失真实分类 hard_loss F.cross_entropy(student_logits, labels) return alpha * soft_loss (1 - alpha) * hard_loss参数说明温度 T 设 4 是常见起点T 越大软标签越平滑学生能学到类间关系alpha 控制软硬损失比例0.7 偏重蒸馏。实测这套下来小模型在闭眼类召回率上能提 2 到 3 个百分点而推理耗时几乎不变。验证蒸馏有没有效果别只看整体准确率重点对比闭眼和打哈欠这两个少数类的召回率那才是疲劳检测的命门。我自己做这类系统的习惯是每改一次阈值或模型都拿同一段包含白天、夜间、戴眼镜、打哈欠的测试视频跑一遍把报警时间点和实际疲劳时间点对齐看提前量和误报数。这个回归测试集比任何指标都实在。希望帮到你。本文还有配套的精品资源点击获取
返回列表