简介:这是一份基于深度学习实现的司机危险驾驶行为识别告警系统完整项目,评审得分98分,适合正在准备毕业设计、课程设计或期末大作业的计算机相关专业学生,也适合希望积累实战经验的学习者。项目围绕驾驶场景中的分神、打电话、吸烟、打哈欠等危险行为构建检测流程,通过SSD目标检测模型与MobileNet等网络实现人脸及行为识别,并通过GUI界面展示告警结果。资源包共64个文件,压缩包约212.52MB,涵盖12个Python源码文件、13个编译缓存文件,同时包含预训练权重(pth、h5、hdf5)与模型配置文件,内置7个测试视频和一批jpg测试图片,可帮助快速复现并验证效果。代码附带详细注释与文档说明,目录结构清晰,按数据记录、模型权重、界面逻辑等模块划分,便于理解与二次开发。目前已有274人学习使用,适合作为完整项目参考。
1. 先说清楚这个项目在做什么,解决什么问题
拿到“基于深度学习的司机危险驾驶行为识别告警系统”这个标题,第一反应不是模型选什么,而是它到底解决谁的什么问题。真实场景是:货运车队、网约车平台、运输公司都想知道司机在开车时有没有打电话、看手机、喝水、抽烟、打瞌睡,光靠行车记录仪拍路面没用,得有一路摄像头对着司机,实时判断行为并告警。这就是这个项目的核心——用深度学习模型对驾驶舱内画面做行为分类,命中危险行为就弹窗告警。做成 Python 工程套上 GUI,意味着这套东西不是只停留在一段推理代码,而是能双击运行、接上摄像头就能用的完整工具,适合课题、毕设,也适合小规模车队做原型验证。开源的源码包里通常已经有训练好的权重、GUI 界面和注释,你要做的是把它吃透、调好、跑在自己的数据上。
2. 用 PyTorch 微调 ResNet18 识别危险驾驶行为:先让模型“看得懂”
“识别告警系统”里最难的一块就是识别。告警逻辑再完善,模型认不出“开车看手机”和“正常看前方”的区别,后面全是空话。所以第一件事是把模型定下来、训练出来,并且把训练到部署的全链路参数讲清楚。这里我用最常见的做法:基于公开数据集微调一个轻量级 CNN 分类模型,而不是自己从零搭网络。
2.1 行为类别怎么定:别只盯着“打电话”
打开这个方向的开源代码,最常看到的类别划分来自那个经典的驾驶员分心数据集(Kaggle 上的 State Farm Distracted Driver Detection 竞赛),通常分成“正常驾驶”“右手发消息”“左手发消息”“右手打电话”“左手打电话”“操作收音机”“喝水”“伸手拿后座物品”“化妆”“和乘客说话”这 10 类。为什么我说别只盯着打电话?因为实际落地你会发现,真实车队的告警需求是分层的——最危险的是长时间低头看手机,其次是打电话、喝水,然后是伸手拿后座东西。类别定多了,模型学不过来,告警也不好定优先级;类别定太粗,比如只写“危险”和“安全”,司机和安全管理又觉得不直观。
所以推荐的做法是先定 5 到 6 个类别:正常驾驶、手持打电话、看手机/发消息、喝水吃东西、操作中控面板、转身拿东西。这个划分既覆盖了法规里明令禁止的行为,也兼顾了车队最常关注的场景。框架代码里类名建议直接写在配置文件的 CLASS_NAMES 里,别散落在多个文件里,后续改类别只需要动一处。
训练数据的分法也要注意——同一个司机在训练集和验证集里同时出现会导致验证指标虚高。按司机 ID 分组划分而不是按帧划分,这是这个方向最容易忽视的一个细节。很多参考工程都是 shuffle 后按比例分割,这样同一司机的画面会同时落在两个集合里,模型记住的是这个人而不是这个行为。
2.2 数据集准备:公开数据集为主、自采为辅的落地做法
常见做法是先把公开数据集作为预训练基础,再用自己摄像头录 1 到 2 小时驾驶舱视频做微调。视频要覆盖不同光线:白天逆光、傍晚、地下车库。录的时候让司机真的做动作、而不是摆拍,摆拍出来的动作和真实驾驶习惯差很多,模型上线会翻车。
先给出训练数据的目录组织方式,这也是这类源码项目里最常见的数据组织形态:
data/ train/ normal/ phone_hand/ phone_text/ drink_eat/ operate_panel/ turn_back/ val/ normal/ phone_hand/ ...训练前先做一步预处理:把图片统一缩放到 224x224,并在训练时做随机增强。下面这段是数据加载部分最常见的写法,我一般直接用一个带增强的 ImageFolder 来读:
from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.3), # 模拟左右手不同习惯 transforms.RandomRotation(degrees=5), # 轻微旋转,提升鲁棒性 transforms.ColorJitter(brightness=0.2, contrast=0.2), # 适应光线变化 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_data = datasets.ImageFolder("data/train", transform=train_transform) val_data = datasets.ImageFolder("data/val", transform=val_transform) train_loader = DataLoader(train_data, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_data, batch_size=32, shuffle=False, num_workers=4)这段代码的逻辑:ImageFolder 按目录名自动生成类别标签,目录名就是类名,省去手写标签映射。增强里我只开了水平翻转、小角度旋转和亮度对比度抖动,没开 RandomResizedCrop,原因是驾驶舱画面里司机的人脸和手部位置相对固定,过强的裁剪会让模型学到错误的部位信息。
几个参数值得注意:
- batch_size=32 在普通消费级显卡(如 6 到 8G 显存)上配合 ResNet18 刚好合适,显存小就降到 16。
- RandomHorizontalFlip(p=0.3) 之所以不设成 0.5,是因为有些动作左右手不对称,比如左手持手机、右手操作挂挡,翻转太频繁会让模型混淆方向。
- num_workers=4 是读取图片的线程数,Windows 下建议设为 0,否则容易报 DataLoader worker 错误。
2.3 训练参数:六组关键参数与调法
模型结构上,我一般选 ResNet18 而不是 ResNet50 或更深的网络。理由有三个:驾驶舱行为识别输入是单摄像头画面,不要求 ImageNet 那种千类粒度,ResNet18 最后一层特征已经够用;延迟敏感,在 CPU 上 ResNet18 跑一帧在 100ms 左右,ResNet50 要接近 300ms;源码工程多半还要再接 GUI 和告警线程,模型推理不能独占太多 CPU。
下面是一段可以直接跑的训练主循环,用预训练权重微调,最后替换全连接层输出类别数:
import torch import torch.nn as nn import torch.optim as optim from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_classes = len(train_data.classes) model.fc = nn.Linear(model.fc.in_features, num_classes) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) for epoch in range(15): model.train() total_loss, correct, total = 0.0, 0, 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) _, preds = torch.max(outputs, 1) correct += torch.sum(preds == labels).item() total += labels.size(0) print(f"Epoch {epoch+1}: loss = {total_loss/total:.4f}, acc = {correct/total:.4f}") scheduler.step() torch.save(model.state_dict(), "model/driver_behavior_resnet18.pth")几个关键参数说清楚:
- lr=1e-4 对微调任务来说是把双刃剑,小了收敛慢,大了会把预训练特征破坏掉。如果发现 loss 前几轮不降,可以改成 1e-3 跑两轮再调回来。
- StepLR 每 5 个 epoch 把学习率减半,15 个 epoch 总共衰减两次,够用且好调。
- 全连接层输出改为 num_classes=6,这一步漏了的话,验证时会报维度不匹配。
这批代码跑完之后,val 集准确率能做到 94% 以上就算达标。如果只有 92% 也别急着堆数据,先看是不是类别不平衡——操作中控、转身拿东西这类样本天然少,最简单的做法是对少样本类别在 DataLoader 里用 WeightedRandomSampler 做加权采样,让模型在每轮迭代里看到少量类别的次数更多。
3. PyQt5 GUI 主框架:从摄像头到告警弹窗的完整链路
模型训练好之后,接下来就是用 GUI 把这套东西做成一个能交给别人用的工具。这里 GUI 框架我用 PyQt5,选它的原因是:OpenCV 的 highgui 只能出裸窗口,给不了按钮、下拉框、状态栏;Tkinter 虽然 Python 自带,但摄像头画面实时刷新需要频繁 update,做列表和历史记录也很别扭。PyQt5 的 QThread 机制天然适合摄像头取流、模型推理、界面刷新这三件事并行。
3.1 三线程架构:摄像头取流、推理、告警互不阻塞
常见做坏了的源码工程长什么样?把摄像头读帧、模型推理、UI 更新全写在一个 while 循环里,结果就是摄像头 30 帧的源被模型拖到 5 帧,界面一卡一卡,告警弹窗直接把程序卡死,这就是标准的“黑匣子翻车现场”。
我一般这样拆三个线程:
- 取流线程:只负责 cap.read(),把最新一帧放进队列,不碰模型。
- 推理线程:从队列取帧,缩放后输入模型,得到类别和置信度,通过信号发给主界面。
- 主线程:只做 UI 刷新和告警判断。
每个环节互不阻塞,摄像头不会因为推理慢而丢帧堆积。下面是取流线程的写法:
import cv2 from PyQt5.QtCore import QThread, pyqtSignal class CaptureThread(QThread): frame_ready = pyqtSignal(object) def __init__(self, source=0, parent=None): super().__init__(parent) self.source = source self.running = True def run(self): cap = cv2.VideoCapture(self.source) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while self.running and cap.isOpened(): ret, frame = cap.read() if not ret: continue self.frame_ready.emit(frame) cv2.waitKey(10) cap.release()说几个细节:采集分辨率故意压到 640x480 而不是 1080p,因为行为识别不需要看清司机脸上的毛孔,1080p 每一帧在推理端都要缩放,白费 CPU。waitKey(10) 是控制取流节奏的,理论上 30fps 的源每帧间隔 33ms,设 10ms 足够。如果不用 waitKey,某些摄像头驱动会热得厉害。
推理线程稍微复杂一点,因为要处理模型加载、预处理和推理三件事。加载模型放构造函数里做,不要在 run 里反复加载:
import torch import torch.nn.functional as F from torchvision import transforms import numpy as np class InferenceThread(QThread): result_ready = pyqtSignal(str, float) # 类别名, 置信度 def __init__(self, model_path, class_names, parent=None): super().__init__(parent) self.model = self._load_model(model_path) self.class_names = class_names self.running = True self.queue = queue.Queue(maxsize=2) def _load_model(self, model_path): from torchvision import models import torch.nn as nn model = models.resnet18() model.fc = nn.Linear(model.fc.in_features, len(self.class_names)) model.load_state_dict(torch.load(model_path, map_location="cpu")) model.eval() return model def run(self): while self.running: try: frame = self.queue.get(timeout=0.5) except queue.Empty: continue if frame is None: continue img = cv2.resize(frame, (224, 224)) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor = torch.from_numpy(img_rgb).permute(2, 0, 1).float().div(255.0) tensor = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])(tensor) with torch.no_grad(): output = self.model(tensor.unsqueeze(0)) prob = F.softmax(output, dim=1) conf, idx = torch.max(prob, 1) self.result_ready.emit(self.class_names[idx.item()], conf.item())这个线程有两个容易踩的坑。第一个是模型 load_state_dict 时,如果之前训练保存的是带 module 前缀的权重(用了 DataParallel 训练),这里会报 key 不匹配,解决方法是保存前先 model.module.state_dict(),或者加载时做一个 key 过滤。第二个是预处理顺序,要先 BGR 转 RGB 再做 Normalize,很多人先归一化再转通道,结果整个画面颜色反了,识别结果直接崩。
3.2 告警逻辑:连续 N 帧命中才触发,别让一次性误报吵到司机
告警是这套系统里最容易被低估的模块。很多人拿到源码直接把“模型输出危险类别 + 置信度 > 0.7”当成告警条件,结果是什么?司机手里拿着矿泉水瓶正常喝水被识别成 drink,告警响了;阳光在司机脸上形成阴影,模型抽风给了个 phone_text,又响了。告警太敏感,司机直接把系统关掉,整条链路白做。
我的做法是加一个“连续命中计数”逻辑:连续 5 帧以上命中同一危险类别,才触发告警。5 帧按 10fps 的推理速度就是 0.5 秒,足够过滤掉单帧误报,又不会漏掉真实危险行为。下面这一段是告警判断的参考实现:
class AlertMonitor: def __init__(self, threshold=0.7, hit_count=5): self.threshold = threshold self.hit_count = hit_count self.current_hits = {} self.alerted = set() def update(self, class_name, confidence): if confidence < self.threshold: self.current_hits = {} return None self.current_hits[class_name] = self.current_hits.get(class_name, 0) + 1 if (self.current_hits[class_name] >= self.hit_count and class_name not in self.alerted): self.alerted.add(class_name) return class_name return None def reset(self, class_name): self.alerted.discard(class_name)这个 AlertMonitor 的精髓在于:当前命中计数器是全局字典而非单类别一个数,因为模型在连续帧里偶尔会在两个危险类之间横跳,加个字典能容纳轻微抖动。告警过一次的类别记录在 alerted 里,防止同一行为连续触发 10 次弹窗,直到 reset 才会重新告警。reset 的时机一般是检测到安全类别时调用——司机把手机放下,系统就解除警报,下次再拿手机才能再响。
参数上,threshold=0.7 和 hit_count=5 是我在真实视频上验证过比较稳妥的组合。阈值太高容易漏报(比如司机转头时侧脸图片模型天然低置信度),太低全是误报。如果部署场景是边开车边用,建议把 hit_count 提到 8,宁可晚 0.3 秒告警,也要把误报压下来。
3.3 界面布局与运行参数:把关键开关放出来
GUI 界面不追求好看,但要让使用者一眼知道当前状态。参考这个方向常见源码工程的布局,我一般放五个区域:实时视频预览区、当前行为标签(大字显示“正常驾驶/危险行为”)、置信度条、告警历史列表、启动/停止按钮和摄像头源下拉框。
关键参数别写死在代码里,放 GUI 上让用户调:
from PyQt5.QtWidgets import (QMainWindow, QLabel, QPushButton, QComboBox, QListWidget, QSlider) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.capture_thread = None self.inference_thread = None self.camera_source = QComboBox() self.camera_source.addItems(["摄像头 0", "摄像头 1", "视频文件"]) self.frame_interval = QSlider() self.frame_interval.setRange(1, 20) # 每 N 帧推理一次 self.alert_list = QListWidget()frame_interval 这个滑块很有用——实际部署时,如果 CPU 性能不够,可以每 3 帧甚至每 5 帧做一次推理,画面预览依然是全帧率,只是行为识别的判断频率降下来。这是牺牲实时性换流畅度的常用手段。
4. 从单帧识别到实时告警:推理速度与准确率的三个必调点
模型在图片上准确率高,不代表在实时视频流里表现好。这一章把从“离线验证”到“实时告警”之间最关键的三个调整点写清楚,这些都是这个方向源码工程里最常被忽略的部分。实时性不够,再准的模型也只是一堆离线数字。
4.1 摄像头参数设置:曝光、白平衡和帧率
摄像头对着司机,最大的敌人是逆光——车前挡玻璃透进来的阳光会让司机脸部过曝,模型基本认不出行为。常见做法是关掉自动曝光,手动固定一个偏暗的曝光值,同时把白平衡固定成固定模式:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 15) cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.0) # 关闭自动曝光 cap.set(cv2.CAP_PROP_EXPOSURE, -4.0) # 具体值视摄像头而定抓三个关键点:FPS 如果源只支持 30fps 而取流线程设了 15fps,有些驱动会自动跳回 30,要在循环里实际读一次 ret 来判断;曝光值在 Windows 下是相对值,不同摄像头厂商的取值范围不一样,建议先跑一段打印 cap.get(cv2.CAP_PROP_EXPOSURE) 看看范围再调;USB 摄像头长时间运行会过热掉帧,程序界面上最好加一个显示“最近 1 分钟平均推理帧率”的标签,数据异常时用户能马上知道是摄像头问题。
4.2 推理速度优化:三个手段按顺序上
实时系统里,推理速度决定了系统的可用性。优化手段我一般按性价比排序:
第一,把模型切到 ONNX Runtime。PyTorch CPU 推理有大量框架开销,ONNX Runtime 在 x86 CPU 上能把单帧延迟从 120ms 压到 60 到 70ms。导出步骤很简单:
import torch from torchvision import models import torch.nn as nn model = models.resnet18() model.fc = nn.Linear(model.fc.in_features, 6) model.load_state_dict(torch.load("model/driver_behavior_resnet18.pth", map_location="cpu")) model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy, "model/driver_behavior_resnet18.onnx", input_names=["input"], output_names=["output"], opset_version=11, dynamic_axes={"input": {0: "batch"}})导出的 ONNX 用 onnxruntime 推理,原来 torch.no_grad() 那段代码换成 ort_session.run 就会快一大截。注意 onnx 导出时如果模型是 GPU 上训练的,要先 map_location 到 cpu,否则导出的 onnx 里会带 CUDA 算子,在纯 CPU 部署环境直接报错。
第二,输入分辨率从 224 降到 160。行为识别这种粗粒度分类任务,160x160 和 224x224 的准确率差距通常不超过 1%,但推理时间能再省三分之一。训练时用 224 的模型,部署时用 160 的输入,置信度会整体偏低一点,阈值可以相应调低 0.03 到 0.05。这个技巧在源码工程里通常写在配置文件的 input_size 里,改一行就能生效。
第三,如果还慢,就把主干网络换成 MobileNetV3-Small。ResNet18 在 CPU 上已经很快,但 MobileNet 可以做到 20ms 以内。做法是把前面 2.3 节里的 models.resnet18 替换成 torchvision.models.mobilenet_v3_small,分类头改成线性层,其余代码不用动。代价是准确率会掉 1 到 2 个百分点,但换来了在低端工控机上流畅运行的能力。
4.3 从误报中做增量迭代:告警历史是免费数据集
这是这类项目迭代最快的一个手段。每一条告警记录都把触发前后各 10 帧的图片保存到本地目录,按“类别_日期_序号”命名。跑一周,你会积累几百张真实场景下的危险行为样本,这些样本的价值远大于公开数据集——因为它们和你的摄像头视角、光线条件、司机习惯完全一致。
保存告警帧的代码可以挂在告警触发点:
def on_alert(self, class_name, frame): import os, time folder = f"alert_samples/{class_name}" os.makedirs(folder, exist_ok=True) filename = f"{class_name}_{int(time.time())}.jpg" cv2.imwrite(os.path.join(folder, filename), frame)这些样本积累到一定数量(每个类别 200 张以上),就把它并进训练集重新微调一轮。两三轮之后,系统在你自己的场景里的表现会明显超过从公开数据集训练出来的模型。这就是这套系统“越用越准”的关键路径,也是源码工程项目里最值得保留的设计习惯。
5. 避坑指南:这些坑在部署时最容易翻车
这类项目的源码可能写得很漂亮,但拿到真环境里跑,问题几乎都出在环境、硬件和真实场景的差异上。以下五条是我积累的踩坑记录,按现象、原因、解决三段式展开,新手可以直接当排查手册用。
5.1 现象:PyTorch 装完,程序一运行就提示 DLL load failed
原因:Windows 下安装 PyTorch 时,如果用的是不带 CUDA 的 pypi 源,装出来的包默认依赖的 Visual C++ 运行库版本不对,或者 Python 3.8 配了最新版 torch,版本不匹配。这类“黑匣子”问题最耗时间。
解决:先卸载重装 CPU 版:
pip uninstall torch torchvision pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu如果还报错,大概率是 Python 环境问题,建议直接用 anaconda 创建干净环境:conda create -n driver python=3.9,再装依赖。项目源码里如果有 requirements.txt,先看版本,别盲目装最新。这一步解决了,后面大部分运行问题都不会再出现。
5.2 现象:摄像头预览正常,但推理结果全是同一个类别
原因:多数是预处理和数据流方向的 bug。最常见的两个:一是前文提到过的 BGR/RGB 顺序搞反,OpenCV 读出来是 BGR,直接转 Tensor 后模型看到的是通道错乱图;二是取流线程和推理线程共用一个队列,但队列里存的是同一个 frame 对象的引用,推理还没跑完下一帧就覆盖了数据。
解决:一是预处理里显式加 cv2.cvtColor(frame, cv2.COLOR_BGR2RGB);二是队列用 queue.Queue(maxsize=2),放帧时 frame.copy(),消费完再清掉,避免覆盖。这两个改完,问题基本能消除。
5.3 现象:白天基本正常,傍晚和夜间误报剧增
原因:模型训练数据多是白天光照环境,夜间红外或低照度下的画面分布完全不同。另一个因素是仪表盘灯光、路灯频闪造成的画面闪烁,会让模型输出置信度在类别之间跳变。
解决:一是夜间场景单独建一个阈值体系,置信度阈值从 0.7 提高到 0.8,hit_count 从 5 提到 10,用更严格的条件换更低的误报;二是积累夜间样本做二次微调,方法就是 4.3 节那套。如果摄像头支持红外模式但默认没开,检查驱动设置,别在代码里硬切。真实夜间跑下来,误报率能压到和白昼接近的水平。
5.4 现象:GUI 点击“停止”按钮后程序不再响应
原因:这是 PyQt5 多线程最经典的坑。停止按钮的槽函数里把线程的 running 标志位设为 False,但线程还阻塞在 cap.read() 或 queue.get() 上,没有机会退出循环,于是线程没有回收,界面就假死了。
解决:在 run 循环里把阻塞操作改成带超时的读取:
import queue try: frame = self.frame_queue.get(timeout=0.5) except queue.Empty: continue同时在线程的 run() 末尾加 self.finished.emit(),主界面在 finished 信号槽里做 thread.wait()。这样点停止后 0.5 秒内线程必定退出,不会卡死。这是 PyQt5 多线程停止操作的后悔药,写 GUI 线程时先把这条记住。
5.5 现象:同一套代码,换一台电脑性能差一半
原因:大多数和硬件相关。CPU 推理时,PyTorch 在没有检测到 AVX/AVX2 指令集的旧 CPU 上会自动退回慢速内核;另一个是摄像头驱动,不同品牌摄像头在同分辨率下的 USB 带宽占用策略不同。
解决:检查 CPU 是否支持 AVX2;检查推理线程是不是被 PyQt 主线程的定时器抢占——把推理线程的优先级调高,QThread.setPriority(QThread.HighPriority)。如果换了品牌摄像头性能骤降,打开任务管理器看谁在占用 CPU,十有八九是摄像头驱动在后台做软件编码。这一步排查完,性能差异通常能缩到 20% 以内。
6. 验收与进阶:用一段真人视频检验这套告警系统是否值得投入
6.1 两个验收指标:检出率和告警延迟
判定这套系统能不能投入,我只看两个数据。一是“危险事件检出率”:让司机在镜头前做 10 次看手机、8 次喝水,系统至少检出 8 次以上,才算合格;二是“告警延迟”,从司机拿起手机到系统弹窗,不超过 2 秒。这两个指标一卡,很多训练精度 90% 以上但告警逻辑写得粗糙的源码工程立刻现原形。检出不达标是模型问题,延迟超标是工程问题,两个维度分别优化,别混在一起调。
6.2 一个可复现的验收脚本
用一段自己录的视频做离线验收,比对着摄像头反复试要稳定得多。录 10 分钟驾驶舱真人视频,标注出每段危险行为的起止时间,再写个脚本跑推理,核对告警时间点。这个脚本本身不复杂,核心逻辑是:读视频逐帧推理,命中危险行为时记录时间戳,与标注的时间窗口做比对,落在窗口内就算一次检出。标注文件的格式可以用简单的 JSON,每段标注记录 start_time、end_time、class_name 三个字段。
这个验收脚本的价值在于把它沉淀到项目里,每次改完模型或告警参数都能跑一遍对比。不加这个脚本,谁都能说自己“识别准确率高”;配上可复现的验收流程,数字就骗不了人了。
6.3 下一步值得投入的三个方向
如果这套系统跑通了,再往上走有三个方向性价比最高。第一个是加上疲劳检测——闭眼和打哈欠的时序模式,用 OpenCV 的人脸关键点 68 点就能做一个可用版本,不用换模型架构,只需在现有 GUI 里加一个“疲劳状态”标签。第二个是把识别结果接入车队管理后台,告警记录上传、生成日报,这是从“工具”变成“产品”的关键一步。第三个是在树莓派或 Jetson Nano 这类小主机上跑通,用 ONNX Runtime 加 MobileNet 替换,把整套系统做成一个盒子——司机上车插电就工作,不需要一个台式机放在副驾。
这套系统值不值得投入,我的态度是:如果只是为了演示,那别投入太多时间在 GUI 上,训练好模型、写好告警逻辑就够了;如果能收集到足够多的真实驾驶舱视频,这个方向是可以持续迭代的——因为每一次误报都会变成训练数据,每一次漏报都能定位到具体是感知还是判断环节的问题。我自己做完这套东西最大的教训是:别在模型结构上反复折腾,先把手头数据用透,把告警链路调到不惹人烦,大多数问题就解决了。希望帮到你。
本文还有配套的精品资源,点击获取