☰
基于深度学习的驾驶员状态检测:从疲劳识别到多状态分类的工程化落地
2026/9/29 2:05:47 网站建设 项目流程

简介:这份资源面向计算机、人工智能相关专业的毕业设计与课程设计学生,聚焦基于深度学习的驾驶员状态检测课题,除疲劳驾驶外还可识别分心、饮酒、患病等多种状态,帮助读者快速搭建可复现的检测方案。压缩包共31个文件,约65.36MB,包含9个ipynb与9个html实验记录、4个py脚本、2份pdf与2份docx报告文档,以及gif演示动图和说明文件,覆盖数据可视化、迁移学习微调、瓶颈特征提取等完整流程。内容涉及VGG16、VGG19、ResNet50、InceptionV3、Xception等多种骨干网络的对比实验,并配有main-finetune、main-without-finetune等训练入口,便于理解微调与不微调的性能差异。已有38人学习,适合作为毕设参考、模型选型与实验复现的实践素材。

1. 驾驶员状态检测:从疲劳识别到多状态分类的工程化落地

跑长途的司机都有体会,真正危险的不是闭眼那一下,而是闭眼之前那几十秒的频繁眨眼、打哈欠、视线飘移。传统疲劳驾驶报警器只盯着一个「闭眼时长」阈值,误报率高得离谱,司机干脆把摄像头贴住。基于深度学习的驾驶员状态检测要解决的就是这件事:用一路普通 RGB 摄像头,把驾驶员的状态拆成正常、疲劳、分心、打电话、抽烟等多个类别,而不是只输出一个「疲劳/不疲劳」的二值判断。这个方向在毕设和课设里出现频率极高,原因很实在——数据集公开、模型结构清晰、单卡就能训、演示效果直观。但它也容易翻车:很多人拿一个分类网络套上去,准确率看着有 95%,一上车就崩。这篇笔记按「数据怎么标、模型怎么选、训练怎么调、部署怎么快」的顺序,把这条链路讲透,适合正在做毕设课设的学生,也适合想把状态识别接进车载设备的工程师。

2. 数据准备:驾驶员状态数据集怎么选、怎么标、怎么划分

2.1 状态类别定义决定模型上限

动手写代码之前,先把「状态」这件事定义清楚。这一步偷懒,后面调参调到怀疑人生也没用。常见的驾驶员状态检测会分成两大类任务:一类是二分类疲劳检测,标签只有疲劳/清醒;另一类是多状态分类,标签包括正常驾驶、疲劳(打哈欠、闭眼)、分心(低头、转头)、打电话、抽烟等。毕设里如果只做二分类,工作量偏薄,答辩容易被追问;做多状态分类,类别之间的边界又容易糊。

我一般建议按「可观测行为」来定类别,而不是按「心理状态」。比如「疲劳」这个标签,不要凭感觉标,而是拆成可量化的行为:连续闭眼超过 1.5 秒、打哈欠持续 2 秒以上、点头频率超过阈值。这样标注时不同人有统一标准,模型学到的特征也更稳定。类别数量控制在 4 到 6 类比较合适,太少体现不出「多状态」的价值,太多则每类样本不够,模型直接过拟合。

一个典型的类别设计如下表,这是我在多个课设里验证过比较稳的方案:

类别编号类别名称判定依据建议最少样本数
0正常驾驶视线前方,头部无明显偏移3000
1疲劳闭眼单次闭眼 ≥ 1.5s2000
2打哈欠嘴部张开持续 ≥ 2s2000
3分心低头头部俯仰角向下 > 20°2000
4打电话手部靠近耳部且头部侧偏1500
5抽烟手部靠近嘴部且有烟雾特征1500

样本数不是硬性指标,但每类低于 1000 张时,训练集和验证集的分布很容易偏,验证准确率会虚高。公开数据集里,驾驶员行为类数据通常按视频片段组织,抽帧时要注意同一段视频的帧不能同时出现在训练集和验证集,否则就是数据泄漏,准确率能虚高十几个点。

2.2 抽帧、去重与数据划分脚本

拿到视频数据后,第一步是抽帧。很多人直接按固定间隔抽,结果相邻帧几乎一样,训练集里全是冗余样本。我的做法是先用帧间差异做一次粗筛,把变化太小的帧丢掉,再按类别均衡抽样。下面这段脚本用 OpenCV 做抽帧和简单去重:

import cv2 import os import numpy as np def extract_frames(video_path, out_dir, diff_threshold=8.0, max_frames=800): """ 从视频中抽帧,基于帧间差异过滤冗余帧 diff_threshold: 平均像素差阈值,低于该值认为画面变化不大,丢弃 max_frames: 单视频最多保留帧数,防止长视频样本过多 """ os.makedirs(out_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) prev_gray = None saved = 0 idx = 0 while cap.isOpened() and saved < max_frames: ret, frame = cap.read() if not ret: break idx += 1 # 每 5 帧处理一次,降低计算量 if idx % 5 != 0: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (160, 120)) if prev_gray is not None: diff = np.mean(np.abs(gray.astype(np.float32) - prev_gray.astype(np.float32))) if diff < diff_threshold: continue prev_gray = gray # 统一缩放到 224x224,方便后续训练 frame = cv2.resize(frame, (224, 224)) cv2.imwrite(os.path.join(out_dir, f"{saved:05d}.jpg"), frame) saved += 1 cap.release() return saved

这段代码里diff_threshold是关键参数。设得太低,几乎每帧都保留,冗余严重;设得太高,动作变化快的帧会被误删,比如打哈欠的峰值帧。我一般从 8.0 开始试,抽完看样本量,如果单视频超过 500 帧就往上调。max_frames用来防止某个长视频主导整个数据集。

抽完帧之后,按 7:2:1 划分训练、验证、测试集,并且要保证同一视频的帧只进一个集合。划分脚本用哈希或者视频 ID 做分组,不要用随机划分,否则数据泄漏会让你的验证曲线好看得不真实。

提示:如果用的是公开数据集,先确认它的划分方式。有些数据集已经给了训练/验证列表,直接沿用,不要自己重新随机分,否则和别人论文里的结果没法比。

3. 模型选型:CNN 分类、时序建模还是关键点方案

3.1 三条技术路线的取舍

驾驶员状态检测的模型方案大致分三派。第一派是纯 CNN 分类,把单帧图像送进 ResNet、MobileNet 这类骨干网络,直接输出状态类别。优点是简单、训练快、部署容易;缺点是完全丢失时序信息,单帧很难区分「正常闭眼眨眼」和「疲劳闭眼」,因为两者在单帧上几乎一样。

第二派是 CNN + 时序模型,比如 CNN 提特征后接 LSTM 或 GRU,或者用 3D 卷积、SlowFast 这类视频理解结构。它能利用连续帧的时序关系,对疲劳这种渐进状态判断更准。代价是训练数据要按片段组织,显存占用大,部署时延迟也高。

第三派是关键点方案,先用面部关键点检测(如 Dlib、MediaPipe、PFLD)拿到眼睛、嘴巴、头部的几何特征,再用这些特征做分类或规则判断。它的可解释性强,眼睛开合度、嘴巴张开度都能直接算,但关键点检测本身在遮挡、暗光下会失效,误差会传导到最终判断。

毕设和课设里,我一般推荐「CNN + 轻量时序」的折中方案:用 MobileNetV3 或 ResNet18 做单帧特征提取,后面接一个 2 到 3 层的 GRU,输入是连续 16 帧。这样既有 CNN 的成熟预训练权重可用,又有时序建模能力,单卡 8G 显存就能跑。

3.2 一个可复现的 CNN + GRU 模型定义

下面这个模型定义用 PyTorch 写,骨干网络换成 MobileNetV3-Small,后面接 GRU 做时序聚合。输入形状是(batch, seq_len, 3, 224, 224),输出是类别数。

import torch import torch.nn as nn from torchvision.models import mobilenet_v3_small, MobileNet_V3_Small_Weights class DriverStateModel(nn.Module): def __init__(self, num_classes=6, seq_len=16, hidden_dim=128): super().__init__() # 加载 ImageNet 预训练权重,加速收敛 backbone = mobilenet_v3_small(weights=MobileNet_V3_Small_Weights.IMAGENET1K_V1) # 去掉原始分类头,只保留特征提取部分 self.feature = backbone.features self.pool = nn.AdaptiveAvgPool2d((1, 1)) # MobileNetV3-Small 特征维度是 576 self.gru = nn.GRU(input_size=576, hidden_size=hidden_dim, num_layers=2, batch_first=True, dropout=0.3) self.fc = nn.Linear(hidden_dim, num_classes) self.seq_len = seq_len def forward(self, x): # x: (B, T, 3, 224, 224) b, t, c, h, w = x.shape x = x.view(b * t, c, h, w) feat = self.feature(x) # (B*T, 576, 7, 7) feat = self.pool(feat) # (B*T, 576, 1, 1) feat = feat.view(b, t, -1) # (B, T, 576) out, _ = self.gru(feat) # (B, T, hidden_dim) # 取最后一个时间步的输出做分类 out = self.fc(out[:, -1, :]) return out

几个参数需要说明。seq_len=16表示每次输入 16 帧,对应大约 0.5 秒的视频(按 30fps 抽帧后每 5 帧取一帧)。这个长度能覆盖一次眨眼或一次哈欠的起始阶段,再长显存吃不消,再短时序信息不足。hidden_dim=128是 GRU 隐藏层维度,太小欠拟合,太大过拟合,128 在多数课设数据集上比较稳。dropout=0.3放在 GRU 层间,防止时序部分过拟合。

训练时损失函数用交叉熵,但要注意类别不均衡。正常驾驶的样本通常远多于疲劳、抽烟这些类别,直接训练会让模型偏向多数类。解决办法是在损失函数里加类别权重,权重按类别样本数的倒数计算:

# 假设 train_labels 是训练集所有标签的列表 class_counts = np.bincount(train_labels, minlength=num_classes) class_weights = 1.0 / (class_counts + 1e-6) class_weights = class_weights / class_weights.sum() * num_classes criterion = nn.CrossEntropyLoss(weight=torch.tensor(class_weights, dtype=torch.float32).cuda())

这样少数类样本的损失会被放大,模型不敢忽略它们。学习率用 1e-3 配 Adam,训练 30 到 50 个 epoch,观察验证集 F1 而不是只看准确率。如果验证 F1 在 10 个 epoch 后不涨,把学习率降到 1e-4 再跑。

3.3 关键点方案作为补充验证

如果你的数据集里遮挡样本多,纯 CNN 方案在打电话、抽烟这些手部遮挡面部的类别上会吃力。这时候可以并行跑一个关键点方案做对比:用 MediaPipe Face Mesh 提取 468 个面部关键点,取眼睛、嘴巴、眉毛、头部姿态相关的点,计算 EAR(眼睛纵横比)、MAR(嘴巴纵横比)、头部俯仰角,再用一个浅层 MLP 或 XGBoost 做分类。这个方案训练极快,而且能告诉你模型到底在依赖哪些几何特征,答辩时讲可解释性很有用。

4. 训练与调参:让多状态分类真正收敛的实操细节

4.1 数据增强要针对驾驶场景做

通用图像增强(随机裁剪、翻转、颜色抖动)在驾驶员状态检测里不能无脑用。水平翻转会把「向左转头」变成「向右转头」,如果类别定义里区分了左右分心,翻转就制造了错误标签。颜色抖动幅度太大,暗光下的疲劳特征会被抹掉。我的做法是:只保留小幅度亮度对比度调整、随机小角度旋转(±10°)、随机遮挡(模拟手部或方向盘遮挡),不做水平翻转。

from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomApply([ transforms.ColorJitter(brightness=0.2, contrast=0.2) ], p=0.5), transforms.RandomRotation(10), transforms.RandomErasing(p=0.2, scale=(0.02, 0.1)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

RandomErasing的scale控制在 0.02 到 0.1 之间,模拟小面积遮挡,太大就把整个面部盖住了,模型学不到东西。归一化参数用 ImageNet 的均值方差,因为骨干网络是在 ImageNet 上预训练的,保持一致能让预训练权重发挥最大作用。

4.2 学习率调度与早停策略

训练多状态分类,最怕的是验证集准确率震荡。我一般用余弦退火加 warmup:前 3 个 epoch 学习率从 1e-5 线性升到 1e-3,之后按余弦曲线降到 1e-5。这样前期不会因为学习率太大把预训练权重冲垮,后期又能精细收敛。

from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2, eta_min=1e-5)

T_0=10表示第一个周期 10 个 epoch,T_mult=2表示之后每个周期翻倍。weight_decay=1e-4是 L2 正则,防止全连接层过拟合。早停看验证集 F1,连续 8 个 epoch 不提升就停,同时保存 F1 最高的那个 checkpoint,不要用最后一个 epoch 的权重。

4.3 混淆矩阵告诉你哪里在翻车

训练完不要只看一个准确率数字,打印混淆矩阵。多状态分类里最常见的翻车是「疲劳闭眼」和「打哈欠」互相混淆,因为两者都伴随面部肌肉变化,单帧特征接近。如果混淆矩阵显示这两类互相错分超过 20%,说明时序建模没起作用,或者seq_len太短。解决办法是把seq_len从 16 加到 24 或 32,让模型看到更长的动作过程。

另一个常见问题是「正常驾驶」被大量误判为其他类,这通常是正常类样本太多导致的。除了损失加权,还可以在采样时对多数类做欠采样,让每个 batch 里各类别比例接近 1:1。

5. 避坑与排查:驾驶员状态检测里最容易踩的五个坑

5.1 验证准确率 99%,一上车就废

现象:训练时验证集准确率冲到 99%,但用手机拍一段自己开车的视频测试,几乎全错。 原因:数据泄漏。同一段视频的相邻帧被随机分到了训练集和验证集,模型记住了背景和人物,而不是状态特征。 解决:按视频 ID 分组划分数据集,确保同一视频的所有帧只出现在一个集合里。划分后重新训练,准确率通常会掉 5 到 10 个点,但那个数字才是真实的。

5.2 模型把「戴眼镜」学成了「疲劳」

现象:混淆矩阵里,戴眼镜的受试者被大量判为疲劳闭眼。 原因:训练集里疲劳样本恰好多数戴眼镜,模型学到了眼镜框这个无关特征。 解决:检查每个类别的受试者分布,确保戴眼镜/不戴眼镜的样本在各类别里都有。如果做不到,就在数据增强里加随机遮挡眼镜区域,强迫模型看眼睛本身而不是镜框。

5.3 推理速度只有 5 FPS,达不到实时

现象:模型在服务器上跑得好好的,部署到边缘设备或普通笔记本上,帧率掉到个位数。 原因:输入分辨率 224x224 加 GRU 时序,单帧推理就要几十毫秒,再乘上时序长度,延迟爆炸。 解决:把骨干网络换成 MobileNetV3-Small 或 ShuffleNetV2,输入降到 160x160,GRU 隐藏维度从 128 降到 64。如果还慢,改成每 3 帧推理一次,中间帧用缓存结果,实际体验不会差太多。

5.4 暗光环境下关键点全丢

现象:白天测试正常,晚上车内灯光暗,MediaPipe 关键点检测直接失效,EAR 算出来全是噪声。 原因:关键点模型在低照度下鲁棒性差,面部纹理看不清。 解决:在预处理阶段加一个轻量暗光增强,比如 Gamma 校正或 CLAHE,把亮度拉回来再送关键点检测。如果还是不行,就退回纯 CNN 方案,因为 CNN 对暗光的容忍度比关键点高。

5.5 类别不均衡导致少数类 F1 极低

现象:整体准确率 90%,但抽烟类的 F1 只有 0.3。 原因:抽烟样本只有几百张,模型直接学会了「永远不预测抽烟」也能拿高准确率。 解决:损失函数加类别权重,同时用重采样让每个 batch 里少数类占比不低于 20%。如果样本实在不够,用 MixUp 或 CutMix 做数据增强,把少数类样本和正常样本混合,扩充决策边界。

6. 部署与进阶:把模型塞进车载设备的几个实用技巧

模型训完之后,真正落地还有一段路。毕设答辩通常要求现场演示,课设可能要求跑在树莓派或 Jetson Nano 上。这里说几个我踩过坑之后总结的技巧。

第一,导出 ONNX 再转 TensorRT 或 NCNN。PyTorch 模型直接部署在边缘设备上依赖太重,导出 ONNX 后用 TensorRT 能提速 2 到 3 倍。导出时注意把 GRU 的batch_first和动态轴设置对,否则时序维度会报错。

torch.onnx.export( model, dummy_input, "driver_state.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch", 1: "seq"}, "output": {0: "batch"}}, opset_version=11 )

dynamic_axes把 batch 和 seq 都设成动态,这样部署时可以灵活调整输入长度。opset_version=11对 GRU 支持比较稳,再高有些推理引擎不兼容。

第二,用滑动窗口做在线推理。实际部署时不会等 16 帧凑齐再判断,而是维护一个帧队列,每来一帧就滑出最老的一帧,凑够 16 帧就推理一次。这样输出是连续的,不会一跳一跳。

第三,加一个后处理平滑。模型单帧输出会有抖动,用最近 5 次推理结果做投票或加权平均,状态切换会自然很多。比如连续 3 次判为疲劳才触发报警,避免误报。

第四,验证时不要只用自己拍的视频。找几个不同光照、不同角度、不同人的片段做测试集,哪怕每个只有几十秒。我见过太多模型在训练集同分布的视频上完美,换个人就崩。如果条件允许,用公开数据集的测试集跑一遍,和论文里的数字对一下,心里有底。

最后说一个我自己的习惯:每次训完模型,先不急着调参,而是把误判的样本单独导出来看一遍。十次里有八次,看完就知道问题出在数据而不是模型。这个习惯帮我省了无数个通宵调参的夜晚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询