ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于YOLOv5与DeepSORT的驾驶员分心预警系统实战

基于YOLOv5与DeepSORT的驾驶员分心预警系统实战 简介这份资源是面向高校学生与深度学习入门者的驾驶员分心驾驶行为预警系统完整项目基于YOLOv5与Deepsort实现目标检测与多目标跟踪可识别驾驶过程中的分心动作并触发预警适合用作毕业设计、期末大作业或课程设计的高分参考方案。压缩包共59个文件约118.04MB以20个Python源码文件为核心配合18个YAML模型配置、13个编译缓存文件另含docx说明手册、演示视频、界面文件、权重文件与关键点数据等覆盖从模型推理到界面交互的完整链路。目前已有194人学习关注。项目代码附带详细注释新手也能读懂下载后简单部署即可运行文档手册与演示视频可帮助快速理解整体流程模块划分清晰便于二次修改与功能扩展对需要完成同类课题或想掌握检测加跟踪组合方案的学习者具有较高参考价值。1. 驾驶员分心预警系统从 YOLOv5 检测到 DeepSORT 追踪的完整落地链路高速上跑长途最怕的不是自己犯困而是旁边车道那辆忽左忽右的车——司机大概率在低头看手机。分心驾驶行为的预警系统要解决的正是这个问题用摄像头实时判断驾驶员是否在打电话、抽烟、喝水、低头调中控一旦持续超过阈值就触发告警。这套方案的技术骨架是 YOLOv5 做目标检测、DeepSORT 做多目标追踪再叠加一个行为判定状态机Python 源代码层面完全可复现。它适合两类人一类是想把 YOLOv5 训练自己的数据集跑通、但缺一个完整业务闭环练手的算法工程师另一类是做车载 DMS驾驶员监控系统产品、需要快速搭一个可演示原型的技术负责人。整套链路不依赖特殊硬件一台带独显的机器或树莓派 4B/5 加加速棒就能跑起来重点在于检测类别怎么定、追踪 ID 怎么稳住、告警逻辑怎么防抖。2. 系统架构拆解YOLOv5 负责看什么DeepSORT 负责记什么2.1 为什么是「检测 追踪」而不是纯分类很多人第一反应是分心驾驶不就是个分类问题吗把驾驶员手部、脸部区域裁出来丢进 CNN 分类器输出「正常/打电话/抽烟」不就行了。这个思路在小数据集上能跑出很高的准确率但一上车就翻车。原因是分类器只看单帧它不知道这个「打电话」的动作持续了多久——司机抬手挠个痒单帧看和举手机几乎一样分类器直接误报。检测加追踪的组合解决的是时序问题。YOLOv5 每帧输出驾驶员身体关键区域手、脸、手机、烟、水杯的边界框DeepSORT 给每个框分配一个稳定的 track ID然后状态机根据同一个 ID 在连续帧里的位置关系判断行为。比如「手机框和手部框的 IoU 连续 15 帧大于 0.3且手机框中心点在脸部框下方 200 像素内」才判定为打电话。这个逻辑用纯分类器做不了因为分类器没有跨帧的 ID 概念。从工程角度看这个拆分还有个好处检测模型可以独立迭代。你换一版 YOLOv5 权重只要输出类别不变追踪和判定逻辑一行不用改。反过来你想调告警灵敏度也只动状态机参数不用重新训练。2.2 YOLOv5 的类别设计与标注边界类别设计直接决定系统上限。我一般会定 6 类person、phone、cigarette、cup、hand、face。注意hand和face单独标出来不要指望person框能覆盖手部细节——YOLOv5 的person框通常框住整个人手部区域在框内占比很小检测头感受野不够手部关键动作根本分不出来。标注时有几个血泪经验。第一phone和cigarette这类小目标标注框要贴紧物体边缘不要留太多背景否则训练时正样本 IoU 上不去。第二遮挡情况必须标。司机手部被方向盘挡住一半、手机被手挡住只露一个角这些都要作为正样本标出来不然模型学到的是「完整手机」的特征一遮挡就漏检。第三hand类不要标拳头只标张开或握持状态的手因为分心行为里手部要么在操作手机要么在拿烟拳头状态对判定没贡献标了反而增加类间混淆。数据量上每个类至少 1500 个实例phone和cigarette这种小目标建议 3000 以上。如果自己采数据困难常见做法是用公开 DMS 数据集做预训练再用自己场景的数据微调。标注格式用 YOLO 的 txt每行class_id x_center y_center width height归一化到 0-1。2.3 DeepSORT 在车载场景的参数适配DeepSORT 默认参数是给行人追踪调的直接搬到车载场景会出问题。核心改动在三个地方。第一max_dist从默认 0.2 调到 0.35。车载摄像头视角下同一只手在相邻帧的移动距离比行人步幅小但检测框抖动大放宽匹配阈值能减少 ID switch。第二max_iou_distance从 0.7 降到 0.5因为手部和手机框经常重叠IoU 计算容易混淆收紧阈值让匹配更依赖外观特征。第三n_init从 3 降到 2车载场景目标出现快、消失也快等 3 帧才确认轨迹会导致告警延迟。外观特征提取器用 DeepSORT 自带的mars-small128.pb就够但输入尺寸要从 128x64 改成 96x96因为手部和手机框接近正方形用行人那种长条形输入会拉伸变形特征区分度下降。这个改动在deep_sort/tracker.py的_feature_extractor调用处改不影响其他逻辑。3. 从零跑通最小系统环境、推理与追踪串联3.1 环境搭建与依赖版本锁定Python 版本建议 3.8这是 YOLOv5 和 DeepSORT 兼容性最稳的版本。3.9 以上在torch和opencv的某些组合下会出现numpy版本冲突新手很容易卡在这里。# 创建虚拟环境避免污染系统 Python python3.8 -m venv dms_env source dms_env/bin/activate # Windows 用 dms_env\Scripts\activate # 锁定版本安装不要用最新版 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install opencv-python4.7.0.72 pip install numpy1.23.5 pip install scipy1.10.1 pip install filterpy1.4.5 pip install tensorflow2.11.0 # DeepSORT 特征提取器依赖这里torch用 1.13.1 配 cu117是因为 YOLOv5 的某些算子在这个组合下编译最顺。numpy锁 1.23.5 是为了避开 1.24 移除np.float导致的filterpy报错。tensorflow只用来加载 DeepSORT 的 pb 模型装 CPU 版就行不用 GPU 版省得和 torch 抢显存。装完后验证import torch, cv2, numpy as np print(torch.__version__, torch.cuda.is_available()) print(cv2.__version__, np.__version__)torch.cuda.is_available()返回True说明 GPU 可用。如果返回False但你有显卡检查 CUDA 驱动版本是否匹配 cu117。3.2 YOLOv5 推理封装与检测结果解析YOLOv5 的推理不要直接用detect.py脚本那个是给命令行用的集成到系统里要自己封装。核心是加载模型、预处理、推理、NMS、解析输出。import torch import cv2 import numpy as np class YOLOv5Detector: def __init__(self, weights_path, devicecuda, conf_thres0.4, iou_thres0.45): # 加载模型weights_path 指向训练好的 .pt 文件 self.model torch.hub.load(ultralytics/yolov5, custom, pathweights_path, force_reloadFalse) self.model.to(device).eval() self.conf_thres conf_thres # 置信度阈值低于此值的框丢弃 self.iou_thres iou_thres # NMS 的 IoU 阈值控制重叠框合并 self.device device def detect(self, frame): # frame 是 BGR 格式的 numpy 数组shape 为 (H, W, 3) img_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results self.model(img_rgb, size640) # 输入尺寸 640和训练时一致 # results.xyxy[0] 格式: [x1, y1, x2, y2, conf, class_id] detections results.xyxy[0].cpu().numpy() # 按置信度过滤 detections detections[detections[:, 4] self.conf_thres] return detectionssize640必须和训练时的imgsz一致否则检测框坐标会偏移。conf_thres设 0.4 是车载场景的经验值太低会引入大量误检太高会漏掉遮挡目标。iou_thres设 0.45 是因为手部和手机框经常重叠NMS 阈值太高会把手机框当冗余框删掉。解析出来的detections每行是[x1, y1, x2, y2, conf, cls]后面要转成 DeepSORT 需要的格式[x1, y1, w, h, conf, cls, feature]。注意 DeepSORT 用左上角坐标加宽高不是右下角坐标。3.3 DeepSORT 追踪器初始化与 ID 绑定DeepSORT 的初始化要指定特征提取器路径和追踪参数。下面这段代码把检测结果喂给追踪器拿到带 track ID 的输出。from deep_sort import DeepSort from deep_sort.utils.parser import get_config class DMSSTracker: def __init__(self, model_pathmars-small128.pb, max_dist0.35, max_iou_distance0.5, n_init2): cfg get_config() cfg.merge_from_file(deep_sort/configs/deep_sort.yaml) # 覆盖默认参数适配车载场景 cfg.DEEPSORT.MAX_DIST max_dist cfg.DEEPSORT.MAX_IOU_DISTANCE max_iou_distance cfg.DEEPSORT.N_INIT n_init self.tracker DeepSort(model_path, max_distcfg.DEEPSORT.MAX_DIST, max_iou_distancecfg.DEEPSORT.MAX_IOU_DISTANCE, n_initcfg.DEEPSORT.N_INIT) def update(self, detections, frame): # detections 格式: [[x1, y1, x2, y2, conf, cls], ...] if len(detections) 0: self.tracker.increment_ages() return [] # 转成 [x1, y1, w, h, conf, cls] bbox_xywh [] confs [] clss [] for det in detections: x1, y1, x2, y2, conf, cls det w, h x2 - x1, y2 - y1 bbox_xywh.append([x1, y1, w, h]) confs.append(conf) clss.append(cls) bbox_xywh np.array(bbox_xywh) confs np.array(confs) clss np.array(clss) # 调用追踪器返回格式: [[x1, y1, x2, y2, track_id, cls], ...] outputs self.tracker.update(bbox_xywh, confs, clss, frame) return outputsincrement_ages()在无检测时调用让追踪器知道这一帧没有新目标避免轨迹被误删。outputs里的track_id是跨帧稳定的同一个手部目标在连续帧里 ID 不变这是行为判定的基础。3.4 行为判定状态机与告警触发拿到带 ID 的追踪结果后用状态机判断行为。核心思路是维护一个字典记录每个 track ID 的类别、位置历史和持续帧数。from collections import defaultdict class BehaviorFSM: def __init__(self, phone_frames15, smoke_frames20, dist_thres200): self.history defaultdict(list) # track_id - [(cls, cx, cy, frame_idx), ...] self.phone_frames phone_frames # 打电话持续帧数阈值 self.smoke_frames smoke_frames # 抽烟持续帧数阈值 self.dist_thres dist_thres # 手机与脸部中心点距离阈值像素 def update(self, tracks, frame_idx): alerts [] # 按 track_id 分组 id_map defaultdict(dict) for t in tracks: x1, y1, x2, y2, tid, cls t cx, cy (x1 x2) / 2, (y1 y2) / 2 id_map[tid][int(cls)] (cx, cy) self.history[tid].append((int(cls), cx, cy, frame_idx)) # 只保留最近 30 帧历史防止内存膨胀 if len(self.history[tid]) 30: self.history[tid].pop(0) # 判定打电话phone 和 face 同时存在且距离小于阈值持续 phone_frames 帧 for tid, cls_dict in id_map.items(): if 1 in cls_dict and 5 in cls_dict: # 1phone, 5face phone_cx, phone_cy cls_dict[1] face_cx, face_cy cls_dict[5] dist ((phone_cx - face_cx) ** 2 (phone_cy - face_cy) ** 2) ** 0.5 if dist self.dist_thres: # 统计最近 phone_frames 帧里同时出现 phone 和 face 的次数 recent self.history[tid][-self.phone_frames:] count sum(1 for c, _, _, _ in recent if c in (1, 5)) if count self.phone_frames: alerts.append((phone, tid, frame_idx)) return alertsdist_thres设 200 像素是 1080p 车载视角下的经验值手机贴耳时中心点距离脸部大约 150-250 像素。phone_frames设 15 对应 30fps 下 0.5 秒太短会误报太长会漏报。history只保留 30 帧是为了控制内存长时间运行不会因为字典无限增长而卡顿。4. 避坑与排查分心预警系统上线前必须过的 5 道坎4.1 检测框抖动导致 ID 频繁切换现象同一个手机目标在连续帧里 track ID 从 3 跳到 7 再跳到 12状态机统计的持续帧数永远攒不够告警不触发。原因YOLOv5 在低置信度边界上的框位置不稳定相邻帧 IoU 低于 DeepSORT 的匹配阈值追踪器认为是新目标。解决在检测后加一个简单的卡尔曼滤波平滑框位置或者把max_iou_distance从 0.5 放宽到 0.6。更彻底的做法是提高检测置信度阈值到 0.5牺牲一点召回换稳定性。我一般先用后者快速验证不行再上前者。4.2 手部与手机框重叠导致类别混淆现象模型把手机框标成hand或者把手部框标成phone状态机拿到的类别对不上判定逻辑失效。原因训练数据里手部和手机经常一起出现标注边界模糊模型学到的特征区分度不够。解决在标注规范里明确——手机框只框手机本体手部框只框手掌和手指两者重叠区域不超过 20%。训练时对这两类加类别权重让损失函数更关注混淆样本。推理时如果phone和hand框 IoU 大于 0.5保留置信度高的那个另一个丢弃。4.3 树莓派部署时帧率掉到 5fps 以下现象在 PC 上跑 30fps部署到树莓派 4B 后掉到 5fps告警延迟明显状态机的帧数阈值全部失效。原因YOLOv5s 在树莓派 CPU 上推理一次要 150ms 以上加上 DeepSORT 的特征提取单帧耗时超过 200ms。解决用 ONNX Runtime 或 NCNN 做推理加速YOLOv5s 转 ONNX 后树莓派 4B 能到 12-15fps。如果还不行换 YOLOv5n 模型精度掉 3-5 个点但帧率翻倍。DeepSORT 的特征提取可以降频每 3 帧提一次特征中间帧只用卡尔曼预测对追踪精度影响不大。4.4 夜间红外画面下检测全挂现象白天正常晚上切到红外摄像头后YOLOv5 几乎检测不到任何目标画面全是噪点。原因训练数据全是可见光图像模型没学过红外域的特征分布把红外画面当成了分布外输入。解决训练时做数据增强把一部分可见光图像转成灰度并加高斯噪声模拟红外效果。更直接的做法是采集夜间红外数据重新微调至少 500 张。如果短期做不到在红外模式下降低置信度阈值到 0.25同时把 NMS 的 IoU 阈值提到 0.6尽量多召回一些框靠追踪的时序一致性过滤误检。4.5 告警风暴一次分心触发几十条告警现象司机低头看手机 3 秒系统连续弹出 20 多条告警日志被刷爆。原因状态机每帧都判定一次只要条件满足就 append 告警没有做告警抑制。解决加一个告警冷却期同一个 track ID 的同类告警60 秒内只触发一次。用一个字典记录last_alert_time[tid][behavior]触发前检查时间差。另外告警要分级持续 0.5 秒是黄色预警持续 2 秒是红色告警红色才触发声音提示黄色只记日志。5. 进阶技巧用轨迹平滑和分级告警把误报压下去5.1 轨迹平滑让状态机看到更稳的位置序列原始检测框的位置序列噪声很大直接拿来做距离判定手机和脸部的距离会在阈值附近反复横跳。我一般会在状态机前面加一层滑动窗口平均窗口大小 5 帧对每个 track ID 的cx, cy做均值滤波。class TrackSmoother: def __init__(self, window5): self.window window self.buffers defaultdict(list) def smooth(self, tid, cx, cy): self.buffers[tid].append((cx, cy)) if len(self.buffers[tid]) self.window: self.buffers[tid].pop(0) avg_cx sum(p[0] for p in self.buffers[tid]) / len(self.buffers[tid]) avg_cy sum(p[1] for p in self.buffers[tid]) / len(self.buffers[tid]) return avg_cx, avg_cy窗口设 5 是折中太小平滑效果不够太大引入延迟。30fps 下 5 帧延迟约 160ms对预警系统可接受。平滑后的坐标再喂给状态机距离判定的抖动明显减小误报率能降一半左右。5.2 分级告警的参数表与调优方法分级告警的关键是阈值怎么定。下面这张表是我在 1080p、30fps 车载场景下调出来的经验值可以直接用也可以根据自己场景微调。行为黄色预警帧数红色告警帧数冷却时间关键判定条件打电话102560sphone 与 face 中心距离 200px抽烟153060scigarette 与 face 中心距离 250px喝水122845scup 与 face 中心距离 220px低头82030sface 框中心 y 坐标下移超过 80px调优方法先跑一段正常驾驶视频统计各行为的误报次数如果误报集中在黄色预警把黄色帧数阈值加 5如果红色告警漏报把红色帧数阈值减 5。每次只调一个参数调完跑同一段视频对比避免多参数同时动导致无法归因。5.3 用日志回放验证告警逻辑系统上线前一定要做日志回放。把推理过程中的检测框、track ID、状态机判定结果全部写进日志格式用 JSON Lines每行一条记录。然后写一个回放脚本读日志重新跑状态机对比告警输出和预期是否一致。import json def replay(log_path, fsm): alerts [] with open(log_path, r) as f: for line in f: record json.loads(line) tracks record[tracks] # [[x1,y1,x2,y2,tid,cls], ...] frame_idx record[frame_idx] frame_alerts fsm.update(tracks, frame_idx) alerts.extend(frame_alerts) return alerts回放的好处是你可以反复调状态机参数不用重新跑推理几分钟就能验证一轮。我一般会把误报和漏报的片段单独截出来反复回放直到参数稳定。这个习惯帮我省了大量上车测试的时间。这套系统从检测到追踪再到告警最深的体会是模型精度只决定上限工程细节才决定能不能用。标注边界、追踪参数、状态机阈值、告警抑制每一个环节偷懒最后都会在实车上以误报或漏报的形式还回来。我自己的习惯是任何参数改动都先跑日志回放确认无误再上车别拿真实路测当调试环境。希望帮到你。本文还有配套的精品资源点击获取
返回列表