基于深度学习的疲劳驾驶检测系统源码:从关键点到时序判定全流程
简介这是一套面向深度学习初学者与计算机视觉方向开发者的智能疲劳驾驶检测系统源码以Python为主要语言围绕驾驶员面部特征、眼动与行为信息实现疲劳状态的实时识别可用于课程设计、毕业项目或算法练手。资源包共84个文件、约105.27MB其中34个py源码文件覆盖数据预处理、特征提取、模型训练与预测全流程另有pt与onnx模型文件、11个txt说明与日志、11个pyc字节码、7张jpg与4张webp图像样本、4个xml配置及png图标资源目录中可见深度学习基础、numpy基础、人脸检测、注册识别、语音响应、疲劳检测等模块结构清晰便于按功能拆解学习。目前已有106人学习下载适合希望快速跑通检测流程、理解模型落地思路并在此基础上做二次开发的读者参考。1. 从摄像头到告警这套疲劳驾驶检测源码到底能跑出什么高速上连续开三小时眼皮开始打架方向盘轻微画龙——这是每个跑长途的人都遇到过的场景。基于深度学习的智能疲劳驾驶检测系统做的就是把这套“人眼判断”换成模型推理摄像头采集驾驶员面部算法判断闭眼、打哈欠、低头等疲劳特征达到阈值就触发告警。这套源码包把数据预处理、模型训练、推理部署和界面展示串成了一条完整链路技术栈是 Python 深度学习框架适合做课程设计、毕业设计也适合想跑通一个端到端视觉项目的开发者。它解决的不是“从零研究算法”而是“有一套能改、能跑、能讲清楚原理的工程骨架”。拿到手先别急着训练先搞清楚它每一层在干什么后面调参和排错才不会抓瞎。2. 疲劳检测的技术链路从人脸关键点到疲劳判定2.1 为什么是“关键点 分类”而不是端到端很多人第一反应是直接把驾驶员面部图片丢进 CNN输出疲劳/清醒二分类不就完了理论上可行但实际工程里几乎没人这么干原因有三个。第一端到端分类需要海量标注数据而疲劳状态是渐变的标注边界模糊同一张半睁眼的图不同标注员可能给出不同标签。第二端到端模型是黑匣子误报了你不知道是眼睛判错了还是嘴巴判错了调试成本极高。第三疲劳判定的核心指标——闭眼时长、眨眼频率、打哈欠频率——都是时序量单帧分类根本表达不了。所以这套源码走的是“人脸检测 → 关键点定位 → 疲劳特征提取 → 时序判定”的分层路线。常见做法是用人脸检测器框出面部区域再用关键点模型定位眼睛、嘴巴的坐标点然后计算眼睛纵横比和嘴巴纵横比最后用滑动窗口统计一段时间内的闭眼帧占比和哈欠次数。这样做的好处是每一层都可解释、可单独替换人脸检测换更快的模型关键点换精度更高的模型判定逻辑改阈值就行不用重新训练整个网络。2.2 眼睛纵横比与嘴巴纵横比的计算逻辑眼睛纵横比是疲劳检测里最核心的几何特征。原理很直白人眼睁开时上下眼睑的距离较大闭眼时这个距离趋近于零。用关键点坐标算出一个比值就能量化“睁眼程度”。import numpy as np def eye_aspect_ratio(eye_points): 计算眼睛纵横比 (EAR) eye_points: 6个关键点坐标, 顺序为 [左角, 上左, 上右, 右角, 下右, 下左] # 垂直距离: 上下眼睑两组点的欧氏距离 vertical_1 np.linalg.norm(eye_points[1] - eye_points[5]) vertical_2 np.linalg.norm(eye_points[2] - eye_points[4]) # 水平距离: 左右眼角距离 horizontal np.linalg.norm(eye_points[0] - eye_points[3]) # EAR 平均垂直距离 / 水平距离 ear (vertical_1 vertical_2) / (2.0 * horizontal) return ear def mouth_aspect_ratio(mouth_points): 计算嘴巴纵横比 (MAR) mouth_points: 8个关键点, 顺序为 [左角, 上唇左, 上唇中, 上唇右, 右角, 下唇右, 下唇中, 下唇左] # 三组垂直距离取平均, 比单组更稳定 vertical_1 np.linalg.norm(mouth_points[1] - mouth_points[7]) vertical_2 np.linalg.norm(mouth_points[2] - mouth_points[6]) vertical_3 np.linalg.norm(mouth_points[3] - mouth_points[5]) horizontal np.linalg.norm(mouth_points[0] - mouth_points[4]) mar (vertical_1 vertical_2 vertical_3) / (3.0 * horizontal) return mar这段代码的逻辑是EAR 用两组垂直距离取平均再除以水平距离目的是消除头部轻微旋转带来的误差。如果只用一组垂直距离头一歪数值就跳变。MAR 用了三组垂直距离因为嘴巴张开时形状变化比眼睛复杂多点平均更稳。参数方面EAR 的阈值通常在 0.2 到 0.25 之间低于这个值判定为闭眼。但这个阈值跟关键点模型的精度强相关——不同模型定位的眼睑位置有偏差阈值必须重新标定。MAR 的阈值一般在 0.5 到 0.7 之间超过判定为张嘴。实际用的时候我一般会先跑一段正常状态的视频统计 EAR 和 MAR 的均值和标准差再取均值减去两倍标准差作为闭眼阈值这样比拍脑袋定一个数靠谱得多。2.3 时序判定为什么不能单帧报警单帧 EAR 低于阈值就报警结果就是每次正常眨眼都会触发告警——正常人每分钟眨眼 15 到 20 次这系统就没法用了。所以必须引入时序逻辑连续 N 帧 EAR 低于阈值才判定为“闭眼事件”闭眼事件持续超过 T 秒才触发疲劳告警。from collections import deque class FatigueDetector: def __init__(self, ear_thresh0.22, mar_thresh0.60, consec_frames3, fatigue_seconds2.0, fps30): self.ear_thresh ear_thresh self.mar_thresh mar_thresh self.consec_frames consec_frames self.fatigue_frames int(fatigue_seconds * fps) self.eye_counter 0 # 连续闭眼帧计数 self.yawn_counter 0 # 哈欠计数 self.ear_history deque(maxlenfps * 60) # 保留60秒历史 def update(self, ear, mar): 每帧调用一次, 返回当前疲劳状态 self.ear_history.append(ear) status {drowsy: False, yawning: False, perclos: 0.0} # 闭眼判定: 连续多帧低于阈值才算一次闭眼 if ear self.ear_thresh: self.eye_counter 1 else: # 闭眼结束, 如果持续够长则记录 if self.eye_counter self.fatigue_frames: status[drowsy] True self.eye_counter 0 # 哈欠判定: MAR 超阈值且持续一定帧数 if mar self.mar_thresh: self.yawn_counter 1 else: if self.yawn_counter self.fatigue_frames: status[yawning] True self.yawn_counter 0 # PERCLOS: 最近一段时间闭眼帧占比 if len(self.ear_history) 0: closed sum(1 for e in self.ear_history if e self.ear_thresh) status[perclos] closed / len(self.ear_history) return status这段代码里几个参数需要重点理解。consec_frames控制的是“连续多少帧低于阈值才算一次有效闭眼”设太小会把眨眼误判为闭眼设太大则真正闭眼时反应迟钝一般 2 到 4 帧比较合适。fatigue_seconds是闭眼持续多久算疲劳正常眨眼闭眼时间约 0.1 到 0.4 秒疲劳性闭眼通常超过 1.5 秒所以设 2 秒左右能有效区分。perclos是学术界常用的疲劳指标指单位时间内闭眼帧占总帧数的比例超过 0.4 通常认为进入疲劳状态。这三个参数不是孤立的调其中一个往往要连带调另外两个后面避坑章节会细说。3. 把源码跑起来环境配置与训练推理全流程3.1 环境依赖与目录结构确认拿到源码包第一件事不是急着python train.py而是先看清楚目录结构和依赖版本。这类项目常见的目录组织是data/放数据集models/放网络定义utils/放工具函数train.py和inference.py是入口脚本requirements.txt锁依赖。先确认 Python 版本——深度学习项目对版本敏感3.8 到 3.10 之间最稳3.11 以上有些老版本框架会出兼容问题。# 创建独立环境, 避免污染系统 Python conda create -n fatigue python3.9 -y conda activate fatigue # 安装依赖, 先看 requirements 里锁的版本 pip install -r requirements.txt # 如果 requirements 没锁版本, 手动指定核心依赖 pip install torch1.13.1 torchvision0.14.1 pip install opencv-python4.7.0.72 pip install numpy1.24.3这里有个血泪经验opencv-python和numpy的版本必须匹配否则 import cv2 时可能报numpy.core.multiarray failed to import。如果 requirements.txt 里没写死版本建议按上面这样手动锁。另外如果要用 GPU 训练torch 的安装命令要去官网查对应 CUDA 版本的别直接pip install torch那样装的是 CPU 版训练时会发现 GPU 用不上。3.2 数据准备与标注格式转换疲劳检测的数据集通常有两种来源公开数据集和自采数据。公开数据集常见的是包含驾驶员面部视频的序列数据标注形式可能是视频级标签疲劳/清醒或帧级标签。自采数据一般用手机或摄像头录一段然后用标注工具标出眼睛和嘴巴的关键点。如果数据集是视频格式需要先抽帧再标注。抽帧的帧率不用太高15 到 30 帧每秒足够太高了相邻帧几乎一样浪费存储和训练时间。import cv2 import os def extract_frames(video_path, output_dir, fps_target15): 从视频抽帧, 按目标帧率保存为图片 os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) src_fps cap.get(cv2.CAP_PROP_FPS) # 计算抽帧间隔: 源帧率 / 目标帧率 interval max(1, int(src_fps / fps_target)) frame_idx 0 saved 0 while True: ret, frame cap.read() if not ret: break if frame_idx % interval 0: # 文件名带帧号, 方便后续对齐标注 filename os.path.join(output_dir, fframe_{frame_idx:06d}.jpg) cv2.imwrite(filename, frame) saved 1 frame_idx 1 cap.release() print(f源帧率 {src_fps:.1f}, 抽帧间隔 {interval}, 共保存 {saved} 张)抽帧后如果要做关键点标注常见工具是 labelme 或 dlib 自带的标注工具。标注格式各家不同训练前需要统一转成模型能读的格式。这里的关键是关键点顺序必须和模型定义一致眼睛 6 点、嘴巴 8 点是常见约定但不同模型可能用 4 点或 68 点转换时顺序错了模型就学废了。3.3 模型训练与关键超参设置训练脚本的核心是数据加载、前向传播、损失计算、反向传播这四步。疲劳检测的关键点回归任务通常用均方误差作为损失函数优化器选 Adam 或 SGD。下面是一个训练循环的骨架import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0.0 for batch_idx, (images, keypoints) in enumerate(dataloader): images images.to(device) keypoints keypoints.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, keypoints) loss.backward() # 梯度裁剪, 防止关键点回归时梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() return total_loss / len(dataloader) # 超参设置 config { batch_size: 32, # 显存不够就降到16或8 lr: 1e-3, # 关键点回归常用1e-3, 太大不收敛 epochs: 100, weight_decay: 1e-4, # L2正则, 防止过拟合 lr_step: 30, # 每30轮降一次学习率 lr_gamma: 0.1 # 降为原来的0.1 }参数说明batch_size受显存限制8G 显存跑 224x224 输入大概能到 32再大就 OOM。lr是关键点回归的命门设 1e-2 会震荡不收敛设 1e-5 收敛太慢1e-3 是常用起点。weight_decay对关键点任务很重要因为面部关键点位置高度相关不加正则容易过拟合到训练集的特定人脸。lr_step和lr_gamma控制学习率衰减训练后期用小学习率精调能明显提升关键点精度。训练过程中要盯两个指标训练损失和验证损失。如果训练损失降但验证损失不降甚至上升说明过拟合了该加数据增强或加大 weight_decay。如果两个都不降检查学习率是不是太大或者数据标注是不是有问题。3.4 推理部署与实时性优化训练完模型要部署到实际场景推理速度是关键。如果只是离线分析视频慢一点无所谓如果要实时告警必须保证每帧处理时间小于帧间隔。30 帧每秒意味着每帧只有 33 毫秒人脸检测加关键点回归加逻辑判定都要在这个时间内完成。import torch import cv2 import time def inference_realtime(model, camera_id0, devicecuda): 实时推理主循环 model.eval() model.to(device) cap cv2.VideoCapture(camera_id) detector FatigueDetector() # 预热, 第一次推理通常较慢 dummy torch.randn(1, 3, 224, 224).to(device) with torch.no_grad(): model(dummy) while True: ret, frame cap.read() if not ret: break t0 time.time() # 预处理: 缩放、归一化、转tensor img cv2.resize(frame, (224, 224)) img img[:, :, ::-1].copy() # BGR转RGB tensor torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 tensor tensor.unsqueeze(0).to(device) with torch.no_grad(): keypoints model(tensor) # 后处理: 关键点转坐标, 算EAR/MAR kp keypoints.cpu().numpy().reshape(-1, 2) ear eye_aspect_ratio(kp[0:6]) mar mouth_aspect_ratio(kp[6:14]) status detector.update(ear, mar) # 可视化 if status[drowsy]: cv2.putText(frame, DROWSY!, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 0, 255), 3) cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break # 打印耗时, 超过33ms说明实时性不够 elapsed (time.time() - t0) * 1000 if elapsed 33: print(f帧耗时 {elapsed:.1f}ms, 实时性不足) cap.release() cv2.destroyAllWindows()实时性优化有几个方向模型量化把 float32 转 int8速度能提升一倍左右但精度会掉一点输入分辨率从 224 降到 112速度提升明显但小脸检测会变差用 TensorRT 或 ONNX Runtime 加速推理比原生 PyTorch 快不少。我一般先跑原生版本看耗时如果超过 33 毫秒再逐项优化不要一上来就上 TensorRT调试成本太高。4. 避坑与排查那些让模型“看起来能跑但实际没用”的问题4.1 现象训练损失正常下降但推理时关键点乱跳原因训练集和推理时的预处理不一致。最常见的是训练时用了归一化除以 255推理时忘了除或者训练时是 RGB 输入推理时 OpenCV 读进来是 BGR 没转。模型学的是训练时的数据分布推理时分布变了输出自然乱。解决把预处理逻辑抽成一个独立函数训练和推理都调同一个函数。别在两处各写一遍迟早会不一致。4.2 现象白天正常晚上或逆光时频繁误报原因训练数据几乎全是正常光照模型没见过暗光或强逆光的人脸关键点定位漂移EAR 算出来偏低系统以为你在闭眼。解决训练时加亮度扰动和对比度扰动做数据增强让模型见过各种光照。如果已经训练完了不想重训推理前加一步直方图均衡化能缓解但不能根治。根本办法还是补暗光数据。4.3 现象戴眼镜的人检测不准摘了眼镜就正常原因镜片反光会遮挡眼睛关键点尤其是红外摄像头配普通眼镜时反光更严重。另外镜框可能被误检为眼睑。解决训练数据里加入戴眼镜的样本这是最有效的。如果数据不好找推理时可以先做眼镜检测对戴眼镜的帧放宽 EAR 阈值或者用红外摄像头减少反光。常见做法是收集几百张戴眼镜的图微调一下关键点模型效果立竿见影。4.4 现象PERCLOS 一直偏高明明人很清醒原因EAR 阈值设得太高正常睁眼时 EAR 偶尔也会低于阈值导致闭眼帧统计偏多。或者帧率不稳定deque 长度对应的实际时间不对。解决先跑一段清醒状态的视频统计 EAR 的分布把阈值设在均值减去两到三倍标准差的位置。同时确认实际帧率如果摄像头标称 30 帧但实际只有 15 帧fatigue_frames的计算要按实际帧率来否则时间窗口就错了。4.5 现象模型在验证集上精度很高实际用起来还是误报多原因验证集和实际场景的分布差异太大。验证集可能是从同一批视频里随机划分的人脸角度、光照、背景都相似而实际场景千变万化。解决划分验证集时按视频或按人划分别按帧随机划分。同一个人同一段视频的帧高度相似随机划分会导致验证集“泄漏”训练集信息精度虚高。按人划分才能反映模型对陌生人的泛化能力。5. 进阶技巧用滑动窗口平滑和自适应阈值把误报压下去前面讲的判定逻辑用的是固定阈值实际用起来会发现两个问题一是不同人的 EAR 基线不一样有人天生眼睛小睁眼时 EAR 就接近阈值二是摄像头角度变化会导致 EAR 整体偏移。这两个问题叠加固定阈值要么误报多要么漏报多。我后来改用的方案是自适应阈值加滑动窗口平滑。思路是用最近一段时间的 EAR 统计量动态调整阈值同时用中值滤波平滑 EAR 序列去掉单帧跳变。from collections import deque import numpy as np class AdaptiveFatigueDetector: def __init__(self, window_seconds30, fps30, k2.0): self.fps fps self.k k # 标准差倍数 # 用较长的窗口估计个人基线 self.ear_baseline deque(maxlenwindow_seconds * fps) # 用较短窗口做平滑 self.ear_smooth deque(maxlen5) self.eye_counter 0 self.fatigue_frames int(2.0 * fps) def update(self, ear_raw): # 中值滤波平滑 self.ear_smooth.append(ear_raw) ear float(np.median(self.ear_smooth)) # 更新基线(只在清醒状态下更新) if ear 0.25: # 明显睁眼时才纳入基线 self.ear_baseline.append(ear) # 自适应阈值: 基线均值减去k倍标准差 if len(self.ear_baseline) self.fps * 5: baseline np.mean(self.ear_baseline) std np.std(self.ear_baseline) threshold max(0.15, baseline - self.k * std) else: threshold 0.22 # 基线不足时用默认值 # 闭眼判定 if ear threshold: self.eye_counter 1 else: self.eye_counter 0 drowsy self.eye_counter self.fatigue_frames return {ear: ear, threshold: threshold, drowsy: drowsy}这段代码的关键改动有三个。第一用中值滤波代替原始 EAR单帧跳变被压掉误报明显减少。第二只在 EAR 明显高于阈值时才更新基线避免把闭眼帧混进基线统计里否则基线会越拉越低。第三阈值用基线均值减去 k 倍标准差k 取 2 左右这样阈值随个人特征自适应眼睛小的人阈值自动降低不会因为天生眼小就误报。实测下来自适应阈值比固定阈值在误报率上能降一半左右代价是启动阶段需要几十秒采集基线这段时间用默认阈值兜底。另外 k 值不要设太大k 大于 3 时阈值太低真正闭眼时可能触发不了漏报就上来了。还有一个细节滑动窗口的长度要跟实际帧率匹配。如果摄像头实际帧率是 15 而不是 30window_seconds * fps算出来的窗口长度就偏大基线更新变慢。我一般会在初始化时用几帧测一下实际帧率动态调整窗口长度而不是写死 30。从那以后我每次拿到这类视觉项目都强制先跑一段真实场景的视频看误报和漏报而不是只看验证集精度。验证集上的数字再好看实际场景里误报一次就够让人想把系统关了。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →