ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习的驾驶员状态检测:从疲劳识别到多状态分类的工程化落地

基于深度学习的驾驶员状态检测:从疲劳识别到多状态分类的工程化落地 简介这份资源面向计算机、人工智能相关专业的毕业设计与课程设计学生聚焦基于深度学习的驾驶员状态检测课题除疲劳驾驶外还可识别分心、饮酒、患病等多种状态帮助读者快速搭建可复现的检测方案。压缩包共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正常驾驶视线前方头部无明显偏移30001疲劳闭眼单次闭眼 ≥ 1.5s20002打哈欠嘴部张开持续 ≥ 2s20003分心低头头部俯仰角向下 20°20004打电话手部靠近耳部且头部侧偏15005抽烟手部靠近嘴部且有烟雾特征1500样本数不是硬性指标但每类低于 1000 张时训练集和验证集的分布很容易偏验证准确率会虚高。公开数据集里驾驶员行为类数据通常按视频片段组织抽帧时要注意同一段视频的帧不能同时出现在训练集和验证集否则就是数据泄漏准确率能虚高十几个点。2.2 抽帧、去重与数据划分脚本拿到视频数据后第一步是抽帧。很多人直接按固定间隔抽结果相邻帧几乎一样训练集里全是冗余样本。我的做法是先用帧间差异做一次粗筛把变化太小的帧丢掉再按类别均衡抽样。下面这段脚本用 OpenCV 做抽帧和简单去重import cv2 import os import numpy as np def extract_frames(video_path, out_dir, diff_threshold8.0, max_frames800): 从视频中抽帧基于帧间差异过滤冗余帧 diff_threshold: 平均像素差阈值低于该值认为画面变化不大丢弃 max_frames: 单视频最多保留帧数防止长视频样本过多 os.makedirs(out_dir, exist_okTrue) 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_classes6, seq_len16, hidden_dim128): super().__init__() # 加载 ImageNet 预训练权重加速收敛 backbone mobilenet_v3_small(weightsMobileNet_V3_Small_Weights.IMAGENET1K_V1) # 去掉原始分类头只保留特征提取部分 self.feature backbone.features self.pool nn.AdaptiveAvgPool2d((1, 1)) # MobileNetV3-Small 特征维度是 576 self.gru nn.GRU(input_size576, hidden_sizehidden_dim, num_layers2, batch_firstTrue, dropout0.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_len16表示每次输入 16 帧对应大约 0.5 秒的视频按 30fps 抽帧后每 5 帧取一帧。这个长度能覆盖一次眨眼或一次哈欠的起始阶段再长显存吃不消再短时序信息不足。hidden_dim128是 GRU 隐藏层维度太小欠拟合太大过拟合128 在多数课设数据集上比较稳。dropout0.3放在 GRU 层间防止时序部分过拟合。训练时损失函数用交叉熵但要注意类别不均衡。正常驾驶的样本通常远多于疲劳、抽烟这些类别直接训练会让模型偏向多数类。解决办法是在损失函数里加类别权重权重按类别样本数的倒数计算# 假设 train_labels 是训练集所有标签的列表 class_counts np.bincount(train_labels, minlengthnum_classes) class_weights 1.0 / (class_counts 1e-6) class_weights class_weights / class_weights.sum() * num_classes criterion nn.CrossEntropyLoss(weighttorch.tensor(class_weights, dtypetorch.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(brightness0.2, contrast0.2) ], p0.5), transforms.RandomRotation(10), transforms.RandomErasing(p0.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(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingWarmRestarts(optimizer, T_010, T_mult2, eta_min1e-5)T_010表示第一个周期 10 个 epochT_mult2表示之后每个周期翻倍。weight_decay1e-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输入降到 160x160GRU 隐藏维度从 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_version11 )dynamic_axes把 batch 和 seq 都设成动态这样部署时可以灵活调整输入长度。opset_version11对 GRU 支持比较稳再高有些推理引擎不兼容。第二用滑动窗口做在线推理。实际部署时不会等 16 帧凑齐再判断而是维护一个帧队列每来一帧就滑出最老的一帧凑够 16 帧就推理一次。这样输出是连续的不会一跳一跳。第三加一个后处理平滑。模型单帧输出会有抖动用最近 5 次推理结果做投票或加权平均状态切换会自然很多。比如连续 3 次判为疲劳才触发报警避免误报。第四验证时不要只用自己拍的视频。找几个不同光照、不同角度、不同人的片段做测试集哪怕每个只有几十秒。我见过太多模型在训练集同分布的视频上完美换个人就崩。如果条件允许用公开数据集的测试集跑一遍和论文里的数字对一下心里有底。最后说一个我自己的习惯每次训完模型先不急着调参而是把误判的样本单独导出来看一遍。十次里有八次看完就知道问题出在数据而不是模型。这个习惯帮我省了无数个通宵调参的夜晚。希望帮到你。本文还有配套的精品资源点击获取
返回列表