简介:这是一套面向深度学习初学者与计算机视觉方向开发者的智能疲劳驾驶检测系统源码,以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 毫秒,如果只按「眼睛闭合」报警,等个红灯、看一眼后视镜都会触发,误报率高到没法用。工程上更稳的做法是引入时间维度:统计单位时间内的闭眼帧占比,也就是 PERCLOS(Percentage of Eyelid Closure),再配合眨眼频率、打哈欠频率、头部低垂角度一起判断。这就是为什么疲劳检测天然是个时序问题,单帧分类模型直接套上去效果往往很差。
常见做法是先用一个人脸关键点模型拿到眼睛和嘴巴的坐标,再算两个几何指标:EAR(Eye Aspect Ratio,眼睛纵横比)和 MAR(Mouth 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 和 CEW(Closed 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_size=24): 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], dtype=torch.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_classes=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 16, 3, padding=1), nn.BatchNorm2d(16), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), 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-5;batch 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, level='high') elif perclos > 0.4: trigger_alarm(frame, level='low') 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, T=4.0, alpha=0.7): # 软标签损失:学生模仿教师的输出分布 soft_loss = F.kl_div( F.log_softmax(student_logits / T, dim=1), F.softmax(teacher_logits / T, dim=1), reduction='batchmean' ) * (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 个百分点,而推理耗时几乎不变。验证蒸馏有没有效果,别只看整体准确率,重点对比闭眼和打哈欠这两个少数类的召回率,那才是疲劳检测的命门。
我自己做这类系统的习惯是:每改一次阈值或模型,都拿同一段包含白天、夜间、戴眼镜、打哈欠的测试视频跑一遍,把报警时间点和实际疲劳时间点对齐,看提前量和误报数。这个回归测试集比任何指标都实在。希望帮到你。
本文还有配套的精品资源,点击获取