DMS驾驶员监控系统实战:从瞳孔检测到CBAM分类的完整视觉推理链路
2026/9/11 14:26:44 网站建设 项目流程

简介:面向计算机类毕业设计或课程作业的完整项目,聚焦智能座舱中的驾驶员分心行为监测系统(DMS),融合人工智能与计算机视觉技术,实现对注意力分散行为的识别与预警,适合需要完成毕设、课程项目或学习检测算法实际落地的开发者。压缩包共39个文件,主要包含Python源码、预训练模型权重(pth)、XML与JSON配置、说明文档等,其中py/pyc文件覆盖视线估计、模型训练与实时预测逻辑,pth权重可直接用于推理,整体包体83.45MB,便于快速搭建运行环境。已有305人学习下载。通过源码可深入理解 gaze_tracking 视线跟踪模块、CBAM注意力机制及DMS整体流程,并可基于已有模型权重进行二次训练或迁移优化,对提升智能驾驶安全领域的AI实战能力具有明显价值。

1. 为什么一个0.978精度的DMS模型,落到智能座舱里仍然要先解决"跑不跑得动"的问题

model-70_0.978_fix.pth这个权重文件,验证集准确率0.978,初看相当亮眼,但它是固定的摄像头角度、固定光照、同一批驾驶姿态数据里测出来的,不是真实路测的统计口径。这套工程真正值钱的部分在于gaze_tracking模块:瞳孔检测、头部姿态解算、视线向量计算被完整串成了一条推理链,后端再挂一个带CBAM注意力机制的分类模型做分心状态判定。对做毕设和课程作业的人来说,它给出了"人脸关键点检测→视线估计→行为分类→报警输出"全链路,而智能座舱测试工程师也能拿它当纯视觉DMS基线方案,不依赖红外专用摄像头。整个工程是本地实时推理架构,MAIN.py是功能完整版,MAIN_fast.py是加速优化版,predict_cam.py可直接接USB摄像头,配合requirements.txt就能把环境拉起来。

2. 拆解gaze_tracking模块:瞳孔定位与头部姿态是分心判断的地基

2.1 文件清单决定了系统边界

先把这个工程的源码结构理清楚,因为一个压缩包里哪些东西被当作"核心交付物",本身就说明了项目的设计倾向。

文件/目录职责关键依赖
gaze_tracking/init.py模块入口,对外导出GazeTracking类
gaze_tracking/pupil.py单眼区域瞳孔中心定位OpenCV
gaze_tracking/eye.py眼睛几何特征,计算睁闭眼状态
gaze_tracking/gaze_tracking.py头部姿态解算与视线向量合成dlib、OpenCV
gaze_tracking/calibration.py屏幕注视点标定
model_cbam.py带CBAM注意力机制的分类模型PyTorch、torchvision
MAIN.py完整推理主循环,含标定与报警
MAIN_fast.py优化后的加速推理循环
predict_cam.py摄像头实时预测独立脚本
model-70_0.978_fix.pth训练完成的模型权重
class_indices.json类别索引到标签名的映射
requirements.txt运行环境依赖清单

这个文件分布很明显不是"训练脚本+测试脚本"的常规毕设结构,训练逻辑被压缩到只剩model_cbam.py一个文件,而推理侧拆出了两个主入口和一个独立预测脚本。也就是说,系统默认你已经有训练好的权重,剩余的问题全在"如何在一个新场景里稳定地跑起来"。这也是智能座舱测试里最容易被低估的环节——模型在离线数据集上精度做得再高,摄像头安装角度一变、驾驶员座椅高度一调,检测率会出现肉眼可见的回落。

2.2 瞳孔中心与眼睛几何:pupil.py和eye.py分工

gaze_tracking这套模块的底层依赖dlib的68点人脸关键点检测。pupil.py做的事情是:在关键点框出的眼睛区域里,通过二值化把最暗的虹膜区域分离出来,然后计算轮廓质心作为瞳孔中心。核心逻辑可以还原成如下形式。

import cv2 import numpy as np def detect_pupil(eye_region): # eye_region 是从人脸关键点裁剪出的单眼灰度图 gray = cv2.cvtColor(eye_region, cv2.COLOR_BGR2GRAY) # 瞳孔比虹膜更暗,用低阈值把最暗区域切出来 _, binary = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY_INV) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None # 取最大连通域的质心作为瞳孔近似中心 largest = max(contours, key=cv2.contourArea) M = cv2.moments(largest) if M["m00"] == 0: return None cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) return cx, cy

这段代码的关键在阈值60这个值,它是经验值。我实测过驾驶舱偏暗环境下这个阈值基本稳定,但迎光行驶时瞳孔区域会被高光打穿,二值化直接失败,这时需要换成cv2.adaptiveThreshold做自适应分割。这类"固定阈值"实现进不了量产,但对毕设演示完全够用——它把一个视线估计问题降维成了图像分割加质心计算。eye.py则在此基础上提取眼睑几何特征,典型的是眼睛长宽比EAR,用来判断睁眼闭眼状态。分心检测里有一个信号值得关注:眼睛被判定为闭合,后端的分类模型却输出"正常驾驶",这种矛盾通常意味着人脸框发生了漂移。

2.3 头部姿态与视线向量:从solvePnP到注意力方向

gaze_tracking.py比pupil.py更深一层。它做的事可以概括为两步:第一,用dlib输出的人脸关键点和标准3D人脸模型做solvePnP求解,得到头部旋转向量;第二,把瞳孔中心相对眼睛中心的像素偏移叠加到旋转向量上,合成最终的视线向量。这个向量的水平分量和垂直分量一旦持续超出某个角度范围,系统就认为驾驶员没有在看前方道路。

实际使用里有几个容易忽略的细节。分心检测真正依赖的不是"看哪里"的绝对坐标,而是"头部姿态加视线偏移"的组合是否持续偏离前方。当座椅位置调整后,头部姿态的基准值会整体变化,所以calibration.py才存在——它引导用户依次注视屏幕的四个角和中心点,在视线向量和屏幕坐标之间建立映射关系。演示时标定环节的意义很大,因为答辩评委通常会上手试坐,标定顺不顺直接影响第一印象。我做二次开发时会把标定点从5个扩展到9个,边界区域的映射误差能够显著降低。

3. CBAM注意力机制与0.978权重:分心分类模型在通道和空间维度上关注什么

3.1 model_cbam.py的结构:通道注意力与空间注意力串联

model_cbam.py定义的分心分类模型,核心在于CBAM模块。CBAM由通道注意力模块和空间注意力模块串行组成:先让模型知道"看什么特征",再告诉模型"去哪里看"。在分心行为识别场景里,背景座椅、车窗外的景物很容易干扰分类结果,CBAM的作用就是让特征图聚焦到人脸、手部和方向盘区域的响应上。它的标准实现如下。

import torch import torch.nn as nn class CBAMBlock(nn.Module): def __init__(self, channel, reduction=16): super().__init__() # 通道注意力:全局平均池化和最大池化并行提取描述符 self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.mlp = nn.Sequential( nn.Conv2d(channel, channel // reduction, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(channel // reduction, channel, 1, bias=False) ) self.sigmoid_channel = nn.Sigmoid() # 空间注意力:沿通道维度聚合后用一个7x7卷积生成空间掩码 self.conv_spatial = nn.Conv2d(2, 1, kernel_size=7, padding=3, bias=False) self.sigmoid_spatial = nn.Sigmoid() def forward(self, x): # 通道注意力:两种池化结果共享同一个MLP,相加后做sigmoid avg_out = self.mlp(self.avg_pool(x)) max_out = self.mlp(self.max_pool(x)) channel_att = self.sigmoid_channel(avg_out + max_out) x = x * channel_att # 空间注意力:通道维平均和最大聚合后拼接,过7x7卷积 avg_spatial = torch.mean(x, dim=1, keepdim=True) max_spatial, _ = torch.max(x, dim=1, keepdim=True) spatial_att = self.sigmoid_spatial(self.conv_spatial(torch.cat([avg_spatial, max_spatial], dim=1))) return x * spatial_att

这段代码里有三个细节值得展开。reduction取16意味着通道数先压缩16倍再过两个卷积层,这个倍数控制的是MLP的参数量;空间注意力部分不用MLP而用7x7卷积,是因为空间注意力需要保留位置信息,全连接会破坏空间结构;两种池化并行是因为平均池化保留全局背景响应,最大池化突出响应最强的局部区域,两者互补。在分心分类这个任务上,通道注意力会倾向于放大"手部接近面部"这类判别性特征的权重,空间注意力则把高响应区域约束在图像中部的人脸及周围区域。

3.2 class_indices.json与标签体系:四类分心行为定义

class_indices.json是模型输出索引到类别名的映射文件。这个工程的分心行为类别划分符合目前智能座舱测试中最常见的数据集组织方式:一个正常驾驶类,加上若干种典型分心行为。推理侧解码逻辑如下。

import json import torch import torch.nn.functional as F with open("class_indices.json", "r", encoding="utf-8") as f: idx_to_class = {int(k): v for k, v in json.load(f).items()} def predict_single_frame(model, tensor_image): model.eval() with torch.no_grad(): logits = model(tensor_image) # 形状 [1, num_classes] probs = F.softmax(logits, dim=1) score, idx = torch.max(probs, dim=1) return idx_to_class[idx.item()], score.item()

注意第一行的int(k)转换。JSON字典的键在序列化后是字符串,不转int会导致索引错位这种隐蔽bug。softmax输出的概率和等于1,torch.max同时返回最大值和对应索引,score就是这个帧的置信度——后面的报警阈值决策就是拿这个值和预设阈值比较。每个类别在训练前都要保证视频样本里出现的场景足够多样,否则模型会学到"这个座椅纹理属于正常驾驶"这种伪特征。

3.3 0.978这个精度数字的真实含义

model-70_0.978_fix.pth这个文件名本身包含三层信息:70很可能是训练轮数epoch,0.978是验证集准确率,fix后缀表示这是修正过某类错误后的版本。但要从这个数字判断模型能不能用于智能座舱测试,还缺一个角色:混淆矩阵。分心检测中存在两类错误,安全行为被报成分心行为的假阳性会打扰驾驶员,分心行为漏报的假阴性则是安全底线。0.978的总体准确率无法回答这两类错误各自占比多少,必须自己录制测试视频做统计。我测试这类分心模型时有一个固定做法:每一类危险行为准备80到100个片段,正常驾驶准备200个片段,跑完后单独计算每一类的召回率和正常类的误报率,这个评估口径与智能座舱测试规范里对DMS误报率的要求是对齐的。

4. 实时推理链路与关键参数:从摄像头帧到报警输出的完整过程

4.1 主循环调度:关键点检测与分类推理异步执行

MAIN.py和predict_cam.py共享同一套推理逻辑,区别在于入口组织和输出形式。主循环的骨架可以抽象成下面的伪代码结构,它反映了分心检测推理链路的真实调度方式。

import cv2 import torch from gaze_tracking import GazeTracking gaze = GazeTracking() model = torch.load("model-70_0.978_fix.pth", map_location="cpu") model.eval() cap = cv2.VideoCapture(0) # 默认摄像头,智能座舱里通常是A柱或仪表盘上方 frame_interval = 2 # 每隔2帧做一次人脸关键点检测 frame_count = 0 vote_buffer = [] # 保存最近N帧的分类结果 while True: ret, frame = cap.read() if not ret: break frame = cv2.resize(frame, (640, 360)) # 统一输入分辨率,降低后续计算量 frame_count += 1 if frame_count % frame_interval == 0: gaze.refresh(frame) # dlib关键点检测 + 视线向量更新 face = gaze.face_region() # 按关键点裁剪人脸区域 if face is None: vote_buffer.append("unknown") continue category, confidence = predict_single_frame(model, preprocess(face)) vote_buffer.append(category if confidence > 0.75 else "unknown") # 滑动窗口多数投票,窗口内危险类占多数才触发报警 if len(vote_buffer) > 10: vote_buffer.pop(0) dangerous_ratio = vote_buffer.count("phone") / len(vote_buffer) # 示例统计 if dangerous_ratio > 0.6: trigger_alarm()

这段逻辑里gaze.refresh和模型推理是串行执行的关系,因此每一帧的耗时是两者的和。dlib的68点检测是关键路径上的瓶颈,而frame_interval控制了它的执行频率。滑动窗口投票的作用是抑制单帧抖动,比如图像噪声导致一帧误判为phone,单个异常值不会触发报警,连续多帧都判定为phone才真正报警。0.75置信度阈值和0.6危险占比这两个值解释一下:0.75是模型单帧输出的取舍点,置信度低时宁可选unknown,避免把不确定帧当作证据;0.6则控制投票窗口内的触发比例,窗口长度10帧,意味着至少6帧判定为危险类才报警。

4.2 推理链路的关键参数与调参顺序

把上一节出现过的参数汇总成一张表,其中调参方向是实际测试中摸索出来的。

参数默认值作用调整方向
frame_interval2每隔N帧执行一次dlib关键点检测调大降低CPU占用,但快速转头时会跟丢人脸
推理分辨率640x360送入分类模型的图像尺寸降到320x180提速,但小目标特征会丢失
置信度阈值0.75低于阈值的帧标记为unknown调高减少误报,调低降低漏报率
投票窗口长度10参与多数投票的帧数越长越稳定,但报警响应延迟增加
危险类占比0.6窗口内危险类触发报警的比例调低更灵敏,适合对漏报零容忍的场景

调参有一个最常见的错误顺序:一上来就调置信度阈值。但置信度阈值的作用范围只在分类模型输出之后,前端人脸检测如果已经丢失目标,后面的阈值再合理也无济于事。我推荐的顺序是:先固定摄像头位置和角度,跑一遍标定;然后用一段30秒的连续驾驶视频测试,统计人脸丢失帧占比;丢失率高于5%,先降低推理分辨率或调大frame_interval;最后再根据剩余的误报情况调整0.75和0.6这两个值。还有一个端到端指标比单帧准确率更关键——从分心动作发生到报警触发的时间差。行业里这个值通常要求小于800毫秒,如果超时,优先缩减投票窗口而不是优化模型。

4.3 predict_cam.py与MAIN.py的工程分工

predict_cam.py做的事情与MAIN.py主循环基本一致,但它是独立脚本,不依赖完整工程的状态管理。我在验证一个模型权重是否好用时更倾向于直接跑predict_cam.py,因为它的输出更精简,只有类别标签和置信度,方便在终端里肉眼确认结果。MAIN.py则集成了标定流程、状态叠加显示和报警输出,面向的是完整演示场景。两个入口分开维护符合实际工程里"验证脚本"和"业务主程序"的分离原则,改报警逻辑时不需要担心弄坏模型验证链路。

5. 用MAIN_fast.py做推理加速:跳帧、跟踪器与降分辨率的具体收益

MAIN_fast.py相对MAIN.py的四个优化手段,逐一拆开看收益来源。第一,dlib关键点检测从每帧执行改成隔帧执行,中间帧用OpenCV的跟踪器接手人脸框,因为跟踪器的计算量远小于重新检测;第二,模型输入分辨率从640x360降到320x180,推理耗时大约能降一半;第三,人脸框区域使用上一帧的位置做局部裁剪,避免整帧缩放;第四,模型权重加载后转成半精度浮点。这四个手段里收益最大的是第一项,dlib正脸检测单帧需要30到80毫秒,跟踪器一轮只需要2到5毫秒,代价是快速转头时人脸框可能偏移,所以跟踪器判定丢失时要立即回退到dlib重新检测。

帧率测量和延迟统计用time.perf_counter打点,这是验证优化效果的第一步。

import time start = time.perf_counter() result = process_frame(frame) # 这里放MAIN_fast.py的处理函数 elapsed = time.perf_counter() - start fps = 1.0 / elapsed print(f"frame latency: {elapsed * 1000:.1f} ms, fps: {fps:.1f}")

process_frame包含检测、推理、后处理全流程,elapsed是完整处理一帧的耗时。fps要在无人脸、有人脸、快速转头三种场景下分别记录,因为MAIN_fast.py的优化手段在不同场景下的表现差异很大。

端到端验证用智能座舱测试里常见的动作序列回放法。录一段3分钟驾驶视频,包含正常驾驶、手持手机、喝水、转头看侧窗和操作中控五个动作,每个动作持续8到12秒,动作之间留5秒正常驾驶间隔。录制时环境光保持稳定,跑完回放后统计每个动作段内系统输出正确类别帧数的占比,得到检出度和误报数。这个流程可以放在云资源上批量执行,也可以本地循环播放。与对着摄像头实拍相比,录制回放的优势在于条件可控,每次跑结果可复现。

比较有性价比的一个验证技巧是:把投票窗口长度从10改为15或20,观察误报数和报警延迟的变化。如果误报数几乎不变,说明误报来源是前端人脸检测丢帧或跟踪器漂移,而非分类模型本身;此时应回头调整frame_interval和跟踪器的丢失回退策略,而不是继续拉长投票窗口。这个判断方法能把调参方向引到真正的瓶颈环节,比反复调置信度阈值高效得多。

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

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

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

立即咨询