ARTICLE DETAIL

资讯详情

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

YOLOv5+Deepsort驾驶员分心行为检测:从目标检测到疲劳报警实战

YOLOv5+Deepsort驾驶员分心行为检测:从目标检测到疲劳报警实战 简介深度学习与YOLOv5Deepsort结合的驾驶员分心驾驶行为预警系统是一套面向高校毕业设计、课程设计与期末大作业的完整工程。资源聚焦驾驶员疲劳、分心及危险动作检测基于YOLOv5完成目标识别、Deepsort实现目标跟踪并配有PyQt界面与部署说明适合需要快速落地项目的本科生或进阶学习者。压缩包共59个文件包含20个Python脚本核心检测、界面逻辑、18个YAML配置网络结构、预训练模型best.pt、人脸关键点模型dat文件、演示视频与操作动图以及DOCX格式手册和Dockerfile部署文件整体约118MB结构清晰便于按模块阅读和二次开发。已有106人学习下载项目含详尽代码注释新手亦可理解内置演示视频和部署文件帮助快速复现省去环境配置与调参弯路。作为导师认可的高分项目很适合用于毕业设计答辩或课程展示可直接修改后作为个人成果提交。1. 分心驾驶行为检测到底在做什么YOLOv5Deepsort毕业设计的完整画面驾驶员分心驾驶行为检测是毕业设计里的常青树要在实时视频流里把打电话、喝水、抽烟、打哈欠等行为识别出来并给出疲劳报警。基于深度学习的做法很多而这个标题里的YOLOv5Deepsort是一个很典型的组合——YOLOv5负责在单帧画面里找“人和物”Deepsort负责把同一目标跨帧关联起来行为判定则在轨迹上做。它的价值在于你不需要一帧帧碰运气而是能稳定得到“哪个驾驶员在做什么、持续了多久”的结构化信息。这套方案适合做毕业设计或课程设计的同学也适合想快速搭出可演示系统的工程师数据集有开源的、训练命令成熟、代码能拆能改三天就能跑通一个带报警的Demo。2. 整个系统怎么拆目标检测、多目标跟踪与行为判定2.1 为什么选YOLOv5Deepsort而不是直接做视频分类分心驾驶检测常见有三条路线。第一条是端到端的视频分类输入一段连续帧输出“打电话/正常驾驶”的类别典型如3D CNN、TSM。这种方式训练成本高而且行为开始和结束的时间边界是模糊的容易被一段画面里的干扰项欺骗。第二条是用姿态估计拿关键点再用关键点角度判断是否低头、偏头、抽烟这种方式对摄像头位置很敏感座舱内遮挡一多就翻车。第三条就是标题里这条用目标检测框住驾驶员的手、手机、烟、水杯再用多目标跟踪把框串成轨迹最后用规则或状态机判定行为。这条路的优势在于每一步都可视、可解释毕业设计答辩时你能指着画面说“这个人的嘴里有烟头、手部框稳定了5帧所以判定为抽烟”。YOLOv5在目标检测里承担“单帧感知”。它在一帧图像上直接回归出目标框和类别不需要候选区域和二次分类速度可以做到实时。Deepsort承担“时序关联”利用卡尔曼滤波预测每个目标的运动状态再用匈牙利算法把当前帧检测框和历史轨迹做最优匹配输出稳定轨迹ID。二者分工明确检测器告诉跟踪器“这一帧哪里有目标”跟踪器告诉行为判定“这个框是上几帧那个人”。选型时一个常见误区是直接看YOLOv5的检测结果去判断行为。比如检测到手机框就报警“打电话”这没有考虑驾驶员是否真的拿着手机讲话也没有区分同一画面里副驾的手机。所以正确做法是让Deepsort先做关联在轨迹维度上做持续状态判断误报才能降下来。这也是热词里“deepsort改进”最常出现的动机不是重新发明跟踪器而是把检测、关联、行为状态机接好。实际项目中还有一个现实考量毕业设计的时间通常只有两三个月端到端行为识别模型光准备视频数据就要标注大量“行为开始”“行为结束”片段数据平衡很难做。用YOLOv5Deepsort你只需要标注单帧检测框一个开源数据集就够起步后续加行为标注也容易补。所以这个组合在当前毕业设计里成了事实标准既踩了深度学习目标检测的热点又能在答辩时讲清楚时序建模不会让评委觉得只是调包。2.2 YOLOv5的检测输出到底怎么读先看最小可运行的推理代码。假设已有训练好的best.pt用PyTorch Hub加载这里以YOLOv5s为例import cv2 import torch model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt, force_reloadTrue) model.conf 0.45 model.iou 0.5 model.classes [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] frame cv2.imread(demo.jpg) results model(frame) detections results.xyxy[0].cpu().numpy() for det in detections: x1, y1, x2, y2, conf, cls det.astype(float) print(int(x1), int(y1), int(x2), int(y2), round(conf, 2), int(cls))关键是用results.xyxy[0]拿到坐标。YOLOv5的xyxy是左上角和右下角的绝对像素坐标不是归一化坐标conf是置信度cls是类别索引。如果需要传给跟踪器通常建议保留xyxy画框时直接cv2.rectangle。model.conf和model.iou分别是置信度阈值和NMS的IoU阈值。conf0.45表示低于0.45的检测框直接丢弃iou0.5表示两个框重叠超过50%时只保留分数高的。毕业设计里如果发现检测框很飘第一反应不是改模型而是把conf提到0.55以上因为漏检比误检更容易用跟踪补偿。这里还有一个容易忽略的点model.classes过滤。如果你训练时用了10类但推理时只想关心“人、手机、烟、水杯”这4类可以在这一步过滤掉其他类减少后续跟踪器的无效输入。不要等到跟踪更新之后才筛因为Deepsort的级联匹配会为每个目标计算代价无关目标会产生大量匹配反而拖慢速度。跑这套代码之前先按yolov5环境配置把依赖装齐torch.hub.load会自动补齐缺失的包但CUDA版本和PyTorch版本不一致时会报奇怪的libtorch错误所以建议直接用官方README里的环境配置命令起步。2.3 Deepsort跟踪器给每个目标一个“身份证”初始化跟踪器时每个参数都直接决定轨迹质量。以常见的deep_sort_pytorch实现为例from deep_sort import DeepSort deepsort DeepSort( model_pathdeep_sort/deepsort.pt, max_dist0.2, min_confidence0.3, nms_max_overlap1.0, max_iou_distance0.7, max_age70, n_init3 )max_dist是外观特征的最大余弦距离值越小表示两个目标越要长得像才认定为同一个max_iou_distance是卡尔曼预测框和检测框的IoU阈值值越大越容易匹配max_age是轨迹丢失后最多保留的帧数帧数太少驾驶员被方向盘遮挡后重新出现会被当成新IDn_init是轨迹初始化前需要连续匹配成功的帧数设成3可以避免单帧误检带来的噪声ID。这几个参数高度耦合。max_age设得太大目标长时间遮挡后重新出现可能匹配到错误目标设得太小ID切换频繁。常见做法是先固定max_iou_distance0.7、n_init3再根据车内的遮挡情况调max_age一般在50到100之间。夜间或反光场景外观特征不稳定需要把max_dist适当放宽到0.3以上。把检测结果喂给跟踪器的循环也很短import numpy as np xywhs xyxy2xywh(detections[:, :4]) confs detections[:, 4] clses detections[:, 5] tracker_outputs deepsort.update(xywhs, confs, clses, frame) for output in tracker_outputs: bx1, by1, bx2, by2, track_id output cv2.rectangle(frame, (int(bx1), int(by1)), (int(bx2), int(by2)), (0, 255, 0), 2) cv2.putText(frame, str(int(track_id)), (int(bx1), int(by1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)注意deep_sort的update接口一般要求输入xywh中心点坐标宽高所以要先做个转换。转换时如果xyxy中出现了超出图像边界的坐标要记得裁剪到图像范围内否则卡尔曼滤波的观测噪声会被异常值带偏。之后tracker_outputs里每一行就是跟踪框的左上右下坐标和track_id行为判定就以这个track_id为键维护一个历史队列。Deepsort只负责跟踪“人”不负责判断人的动作。行为判定的原料是轨迹也就是同一个track_id在过去N帧里的检测框序列和类别序列。有了这个序列你才有资格谈“持续了多久”也才能区分“打哈欠”和“正常张嘴说话”。所以在这一步务必要把每一帧的检测结果和track_id一起存下来而不是只存框不存类别。在实际毕设代码里我一般会把检测器封装成一个类跟踪器封装成另一个类行为判定单独放一个behavior.py。不要把所有逻辑堆在一个main.py里。至少分成这几个文件main.py # 视频入口与主循环 detector.py # 封装YOLOv5推理 tracker.py # 封装Deepsort update behavior.py # 轨迹历史与行为状态机 utils/video_stream.py # 读取摄像头/视频文件这样训练、测试、答辩演示都方便演示时只改main.py里的输入源模型评估时只调detector.py。如果你后面想换YOLOv8或者改进Deepsort也只需要替换对应的类行为判定和界面代码不用动。提示如果你把“左手”“右手”当成独立类别去检测Deepsort会把左右手弄混因为外观特征太像。类别设计最好以“人、手机、烟、水杯”为核心而不是“左手、右手”。3. 准备数据和训练模型从公开数据集到YOLOv5权重3.1 选哪个数据集公开竞赛数据与疲劳样本的补齐做驾驶行为检测最常见的起步数据是公开的驾驶员分心分类竞赛数据比如State Farm那个比赛。它的原始数据是图片和CSV标注一张图对应一个行为类别类别包括安全驾驶、发短信、打电话、操作仪表台、喝水、化妆等10类。但它本质是图像分类标注没有目标框需要转换成检测格式才能训练YOLOv5。疲劳行为闭眼、打哈欠在分类数据集里往往没有单独标注需要自己找或者补充。常见做法是用公开的YawnDD打哈欠检测或者自己录一段话把“正常说话”和“打哈欠”的嘴部变化采集出来。这里不必追求样本量很大几百张清晰的关键帧就够在迁移学习基础上微调。训练前要做的第一件事是把所有图片路径和标签对应关系理清楚。YOLO格式需要每个图片对应一个同名txt文件里面每行是class cx cy w h坐标是归一化的。原始CSV里如果给的是left, top, right, bottom那么宽高要用right-left、bottom-top计算注意不要用绝对像素直接用必须除以图片宽高。3.2 把图片分类数据转成YOLO检测格式转换脚本与四个边界坑下面脚本处理的是“单张图片只有一个目标框”的简单情况适合State Farm分类数据转检测格式。关键点都写在注释里。import pandas as pd import cv2 from pathlib import Path df pd.read_csv(driver_imgs_list.csv) # 期望列: img, class_id, left, top, right, bottom def convert_to_yolo(row, img_root, label_root): img_path img_root / row[img] img cv2.imread(str(img_path)) h, w img.shape[:2] x1 float(row[left]) y1 float(row[top]) x2 float(row[right]) y2 float(row[bottom]) # 边界框可能超过图片范围必须裁剪到[0, w]/[0, h] x1 min(max(x1, 0), w) y1 min(max(y1, 0), h) x2 min(max(x2, 0), w) y2 min(max(y2, 0), h) if x2 x1 or y2 y1: return False cx (x1 x2) / 2 / w cy (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h label_txt label_root / (Path(row[img]).stem .txt) with open(label_txt, w) as f: f.write(f{row[class_id]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n) return True img_root Path(data/imgs) label_root Path(data/labels) label_root.mkdir(exist_okTrue) for _, row in df.iterrows(): convert_to_yolo(row, img_root, label_root)这段脚本里最容易被忽视的是坐标裁剪。公开数据集的标注偶尔会出现right超出图片宽度、left为负数等情况如果不裁剪归一化坐标会小于0或大于1训练时YOLOv5的loss计算会出现难以追踪的nan。另一个坑是图片路径分隔符CSV里用\还是/在Windows和Linux上结果不一样建议统一转成Path再拼接。类别映射要单独做。比如原始class_id是0到9但你想重新排列为“安全驾驶0、打电话1、发短信2、喝水3、抽烟4、打哈欠5、眯眼6、其他7”就要在转换脚本里加一个字典不要让0到9直接进模型。因为后续行为判定逻辑要依赖语义化的类别名提前建映射表能省掉很多if else。还有一个容易被坑的边界同一张图只有一个目标的假设在State Farm上成立但如果你想加入自己录制的数据一帧里可能既有驾驶员又有副驾需要支持多行标注。转换时要先统计每个txt已有内容不能用w模式覆盖要追加写入并用\n换行。我一般会写一个独立的append_write分支。3.3 训练命令与参数用YOLOv5训练自己的数据集时最常调的5个参数数据转换完成后在项目根目录建driver.yaml内容指向训练集、验证集和类别名train: data/images/train val: data/images/val nc: 8 names: [safe, call, message, drink, smoke, yawn, eyes_closed, other]然后执行训练。显存不够就把batch减半或者加--amp用混合精度python train.py \ --img 640 \ --batch 16 \ --epochs 50 \ --data driver.yaml \ --weights yolov5s.pt \ --cache \ --workers 4--img 640是训练分辨率不要盲目从640降到320虽然显存占用变小但对小目标烟、水杯的召回会下降明显。--batch 16在单张2080Ti或3060上够用如果只有4G显存建议--batch 8并配合--amp。--epochs 50是起步值迁移学习下一般到40轮就稳定后面主要调的是--patience早停参数默认100过大训练到不涨时既浪费时间又可能过拟合我一般设20。权重用yolov5s.pt比从头训练收敛快很多但如果你的数据类别和COCO差异很大比如全是座舱内小物体可以考虑冻结前10层主干训练更稳。训练结束后看runs/train/exp/results.csv重点看mAP_0.5和mAP_0.5:0.95。如果你发现mAP_0.5在提高但0.95不涨说明框定位不够精细通常是把--img提高到768或者对标注框做轻微抖动增强能改善。还要在训练脚本加上--hyp自定义超参数热词里“yolov5超参数”说的就是这里关键是hsv_h、hsv_s、hsv_v和flipud。座舱摄像头画面经常是上面亮下面暗建议把hsv_v从默认0.4调到0.6并关闭flipud上下翻转会生成车顶朝下的假样本。训练完不要急着装进系统先跑一张验证集的图片看检测效果。用--conf阈值0.3检查是否把驾驶员的头、肩膀、手机都框出来。这个可视化步骤能暴露标签坐标系错误比如归一化中心点反了造成框偏到图外。4. 把训练好的模型接到摄像头检测、跟踪和行为判定完整流程4.1 封装一个实时检测线程主循环不要一帧一帧同步做所有事否则录像或检测一慢报警就会后延。更稳的结构是检测器独立线程读摄像头输出队列给跟踪线程跟踪结果喂给行为判定。下面这个简单封装可以拿来起步import threading import queue import cv2 import torch class Detector(threading.Thread): def __init__(self, video_path, output_q): super().__init__() self.cap cv2.VideoCapture(video_path) self.output_q output_q self.model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt) self.model.conf 0.5 def run(self): while True: ret, frame self.cap.read() if not ret: break results self.model(frame) dets results.xyxy[0].cpu().numpy() self.output_q.put((frame, dets)) q queue.Queue(maxsize8) detector Detector(driver.mp4, q) detector.start() while True: frame, dets q.get() # 后续跟踪和判定这里queue.maxsize8用来兜底如果跟不上就丢旧帧而不是无限积压。检测结果和原帧一起入队避免在后续线程里再读一次视频既省IO也减少帧错位。model.conf 0.5比训练时更高因为实时场景里误检带来的ID切换更影响体验。4.2 跟踪更新把检测框变成轨迹检测线程输出dets后主循环调用Deepsort更新轨迹。这里要注意Deepsort内部会用min_confidence过滤检测框但过滤后的检测框数量少跟踪器的级联匹配矩阵维度会变化所以传入confs时不要提前把所有低置信度框丢掉交给跟踪器统一处理反而更稳。from utils.bbox import xyxy2xywh xywhs xyxy2xywh(dets[:, :4]) confs dets[:, 4] clses dets[:, 5] outputs deepsort.update(xywhs, confs, clses, frame) for buf in outputs: x1, y1, x2, y2, track_id buf[:5] cls int(buf[5]) if len(buf) 5 else None注意看deepsort.update返回的格式不同开源实现差异很大。有的返回track_id, x1, y1, x2, y2有的还会带类别和置信度所以打印一条outputs的结构再继续写比盲改索引省时间。如果你在热词里搜过“deepsort改进”会知道很多实现加了车辆重识别之类的分支但毕业设计用基础版就够重点是别在返回格式上翻车。4.3 行为判定规则疲劳和危险行为各需要什么逻辑有了track_id和类别序列行为判定就可以做成一个状态机。危险行为相对简单如果同一个track_id连续N帧出现“打电话”或“喝水”类且驾驶员的检测框稳定存在就触发报警。疲劳行为则复杂一些因为闭眼和打哈欠是瞬时姿态需要窗口统计。下面给一个用队列累计状态的最小实现from collections import deque, defaultdict class BehaviorJudge: def __init__(self, window30, alarm_frames20): self.history defaultdict(lambda: deque(maxlenwindow)) self.status defaultdict(list) self.alarm_frames alarm_frames def update(self, track_id, is_danger, is_fatigue): self.history[track_id].append((is_danger, is_fatigue)) h self.history[track_id] danger_count sum(1 for d, f in h if d) fatigue_count sum(1 for d, f in h if f) if danger_count self.alarm_frames: self.status[track_id] danger elif fatigue_count self.alarm_frames * 0.7: self.status[track_id] fatigue else: self.status[track_id] normal return self.status[track_id]window30表示只看最近30帧大约是1秒到1.5秒取决于帧率。alarm_frames20表示危险行为要持续20帧才报警。这两个参数必须根据摄像头帧率校准如果视频是30fps30帧就是1秒20帧报警意味着检测到危险行为持续0.67秒才触发如果是15fps同样的窗口就是2秒报警会明显变慢。疲劳判定单独用alarm_frames * 0.7是为了打哈欠这种短暂动作也能被捕捉但不能低于10帧否则张嘴说话就会被误判。更严谨的做法是把is_fatigue拆成“闭眼持续帧数”和“哈欠频率”两个指标前者用EAR关键点距离计算后者用嘴部宽高比。毕业设计里如果没有关键点可以先按“检测到疲劳类别的连续帧数”来近似然后在答辩时说明这是简化方案。报警输出不要直接print或画红框建议维护一个alarm_log列表记录track_id、时间、行为类型用于事后统计误报率和漏报率。这对接下来的避坑非常关键很多问题必须靠日志反向定位而不是靠肉眼盯着屏幕。5. 避坑与排查训练和部署里那些靠经验解决的问题5.1 现象检测框乱跳、ID频繁切换这是部署时最常碰到的问题。帧与帧之间同一个驾驶员的框位置剧烈抖动Deepsort 的 track_id 频繁变化行为判定窗口里总是同一个 ID 的帧数不够报警失效。原因有三层第一置信度阈值设得太低背景误检被当作真实目标第二max_age设得太短驾驶员低头或转身时目标丢失几分钟后重新出现跟踪器把它当作新目标第三检测框本身不稳定小目标在相邻帧间的 IoU 波动大卡尔曼滤波的预测和观测难以收敛。解决方式依次是把model.conf提到 0.5 以上同时把max_age调到 70 左右然后在检测框输出和跟踪器之间加一个简单的平滑对同一个 track_id 的坐标做指数移动平均box_smooth alpha * box_old (1-alpha) * box_newalpha取 0.7。注意不要平滑太多否则驾驶员突然转头时框会跟不上。5.2 现象训练时 loss 不下降或者直接显存溢出训练第一天最难受的就是这个。并不是模型设计问题大概率是数据或超参数问题。先看数据用plot_labels.py或自己画一下标签分布如果类别索引超出nc-1训练 loss 直接 NaN如果某个类别的框宽高接近 0也会导致回归 loss 异常。再看显存YOLOv5 的默认 batch 是根据 COCO 评测设置的你用自己的数据集类别少但图片分辨率高照样爆显存。解决是--batch 8--amp--workers 2如果还爆就把--img 416先跑通再往上涨。还有一个玄学训练一开始 loss 很大是正常的但如果 10 个 epoch 后cls_loss还在 2 以上检查类别名文件是否写错。我曾经在一个数据集里把safe和other的 names 顺序写反导致模型学到反的语义测试时一直报警。5.3 现象夜间和强光下漏检严重摄像头朝前挡风玻璃或者朝驾驶员侧窗时逆光场景很常见。检测器对暗光下的手机、烟头召回率掉得厉害而且暗光下检测框置信度普遍低进入 Deepsort 后被min_confidence过滤掉目标消失。原因是训练数据里夜间占比较少深度模型对亮度变化敏感。解决从三方面做第一训练时把数据增强的hsv_v调高模拟明暗变化第二推理前对图像做 CLAHE 增强把局部对比度拉起来第三最稳的办法是把conf阈值降到 0.3对暗光目标先保留靠 Deepsort 的max_age扛过去。这样漏检率下降但因为暗光误检增多需要把行为判定的报警帧数从 20 调到 25平衡两者。5.4 现象行为判定误报率高频繁报警“疲劳”“打电话”这是规则和跟踪打架的结果。比如检测器把驾驶员摸方向盘的手当成了“喝水”或者把嘴里吃口香糖的咀嚼动作当成“打哈欠”的前奏。主要原因在于行为判定只用了瞬时类别没有结合上下文。同一 track_id 在很短时间内在“正常”和“危险”之间跳变实际上目标ID已经换了但你的history还挂在旧ID上。解决方式一是给每个 track_id 设置冷却时间进入报警状态后至少 3 秒内不重复报警二是用“连续帧 比例”而不是“数量”判断在 30 帧窗口内危险类帧数占比达到 60% 才触发。三是给跟踪器加n_init3的状态判断只有确认是新轨迹后才允许写入行为判定队列避免单帧误检测污染历史。5.5 现象CPU 推理速度太慢界面卡到没法演示毕业设计现场答辩常常只有一台笔记本没有 GPU。YOLOv5s 在 CPU 上 640 分辨率推理可能在 3-5 秒加上 Deepsort 每一帧都在做矩阵计算整体会卡顿。解决思路是分层降载。第一模型换成yolov5n或把--img降到 416在保证基本精度的前提下换取帧率第二检测器线程和显示线程分离使用队列检测慢时直接丢帧界面用最近的结果绘制第三把 Deepsort 的max_dist0.2放宽到 0.3减少特征提取时间第四如果还想再快可以把模型导出成 ONNX 用 OpenVINO 推理但这一步要额外装依赖不是必须项。答辩演示时用录制视频而不是实时摄像头能避开很多现场光线和帧率问题这是一个小技巧但不是作弊因为系统本身已经跑通了实时链路。6. 让这个毕业设计更拿得出手效果验证、报警联动与课题延伸6.1 验证指标别只看准确率要算误报率和响应延迟答辩时最怕被问“你效果到底怎么样”。除了YOLOv5训练时输出的mAP还要在测试视频上统计两个指标误报率正常驾驶段里触发报警的次数和报警延迟危险行为开始到报警经过的帧数。把报警日志和真值对齐画一条时间轴比给一张PR曲线更有说服力。6.2 报警联动接口让报警真正可用行为判定模块不要直接输出“danger”给界面而是提供一个回调接口这样接语音播报、Qt弹窗、甚至硬件蜂鸣器都方便。我一般这样收尾def on_alarm(track_id, behavior_type, timestamp): # 写日志、触发语音 alarm_log.append((track_id, behavior_type, timestamp)) judge BehaviorJudge() judge.on_alarm on_alarm6.3 课题延伸方向如果还有时间优先加人脸关键点用EAR和MAR算闭眼与打哈欠疲劳检测会专业很多其次可以把YOLOv5换成轻量化模型配合TensorRT部署到Jetson上这就是实打实的工程经验。最后想说的是整套系统最容易被忽略的不是模型精度而是时序状态机。我毕业设计第一次现场演示就是在“喝水”动作触发报警后ID切换导致同一人两秒后又被误判成“打电话”当场翻车。从那之后我养成了先写报警日志、再调规则的习惯。这个过程虽然有点玄学成分但踩完坑之后你会对目标检测、多目标跟踪和行为判定三者怎么配合有非常具体的理解。希望帮到你。本文还有配套的精品资源点击获取
返回列表