ARTICLE DETAIL

资讯详情

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

YOLOv5+Deepsort驾驶员分心行为检测实战:从权重训练到疲劳预警系统部署

YOLOv5+Deepsort驾驶员分心行为检测实战:从权重训练到疲劳预警系统部署 简介这是一份面向深度学习与计算机视觉学习者的驾驶员分心驾驶预警系统项目资源基于YOLOv5DeepSort实现同时覆盖疲劳检测与分心行为检测两大模块。疲劳检测借助Dlib人脸关键点与Perclos模型判断闭眼、打哈欠分心检测则通过YOLOv5识别玩手机、抽烟、喝水等动作。压缩包共58个文件以Python脚本与YAML配置文件为主另有PyQt界面文件、模型权重best.pt、人脸关键点模型dat、Dockerfile及演示GIF等整体约89MB便于快速搭建与二次开发。资源包已提供重新训练后的YOLOv5权重、精简后的疲劳检测逻辑及优化过的前端UI适合希望直接运行或在此基础上进行算法调优的开发者同时附带README说明可帮助理解项目结构。该资源已有729人学习下载可用于毕业设计、课程项目或工程实践参考也是入门目标检测与多目标跟踪的良好范例。1. 用YOLOv5Deepsort做驾驶员预警这套系统到底做了什么做深度学习项目实践的人十有八九会撞到同一个选题基于YOLOv5的驾驶员分心驾驶行为预警。这个项目把疲劳预警和危险行为预警两条线放在了一起——疲劳部分用Dlib做68点人脸关键点检测靠闭眼帧率和打哈欠帧率算Perclos疲劳度危险行为部分用YOLOv5检测玩手机、抽烟、喝水三类动作再配合Deepsort做目标跟踪避免单帧误判。我拆这份1.0工程时最直接的感受是它已经帮你省掉了从零搭环境、凑检测头、接UI的大半工作量适合拿来复现、二次改造成毕设或工程演示。你如果手里有摄像头或一段行车视频按工程里的目录结构把权重路径和类别数对准整套逻辑能直接跑起来。2. YOLOv5危险行为检测玩手机/抽烟/喝水检测的模型与推理链路2.1 检测部分为什么选YOLOv5危险行为检测要解决的是画面里有没有人在玩手机、抽烟、喝水这事本质是目标检测。YOLOv5在工程实践里被选中的理由很直接模型文件小、推理速度快、PyTorch生态里改起来顺手而且它对中近距离的目标框回归比两阶段模型更省算力。在驾驶室这种固定视角场景下人的手部动作区域基本落在画面中部YOLOv5的检测头足够用。这个1.0工程项目里作者做了一个很关键的动作把YOLOv5的权重重新训练过训练轮次比原版多。这意味着best.pt对应的不是COCO那80类通用物体而是针对驾驶舱场景下的自定义类别。复现时必须先确认这一点否则你拿一个官方yolov5s.pt跑代码结果只有person、cell phone这类COCO标签工程里预设的玩手机、抽烟、喝水判断逻辑根本对不上。工程中weights目录下的best.pt就是重训产物目录里还有yolov5s.yaml、yolov5m.yaml这些模型结构定义文件说明训练时是在v5s或v5m骨架基础上做的迁移学习。2.2 从best.pt到mydetect.py推理侧的调用顺序推理入口不复杂。mydetect.py负责加载模型并对单帧画面做检测常见实现方式是把YOLOv5的Detector封装成一个类初始化时传入weights路径和置信度阈值。下面这个代码块是典型的工程化调用方式你在自己项目里按这个套路改就行import torch from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords class DistractDetector: def __init__(self, weights_path, conf_thres0.4, iou_thres0.45): # 加载重训后的权重device按机器配置切CPU或GPU self.device torch.device(cuda if torch.cuda.is_available() else cpu) self.model attempt_load(weights_path, map_locationself.device) self.model.eval() self.conf_thres conf_thres self.iou_thres iou_thres # 注意类别名要和训练数据一致常见是[playphone, smoke, drink] self.class_names self.model.names def detect_frame(self, img): # img是BGR格式的numpy数组先转RGB再进模型 import cv2 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # YOLOv5要求输入归一化到0~1且shape为[N, C, H, W] import numpy as np blob torch.from_numpy(img_rgb.transpose((2, 0, 1))).float() blob blob.unsqueeze(0) / 255.0 blob blob.to(self.device) with torch.no_grad(): pred self.model(blob)[0] det non_max_suppression(pred, self.conf_thres, self.iou_thres)[0] if det is not None and len(det): # 把检测框坐标缩放到原始图像尺寸 det[:, :4] scale_coords(blob.shape[2:], det[:, :4], img.shape).round() results [] for *xyxy, conf, cls in det: label self.class_names[int(cls)] results.append((label, float(conf), [int(v) for v in xyxy])) return results return []这段代码里有几个参数直接决定检测效果conf_thres是置信度门槛默认0.4对驾驶室场景够用如果误报多就调到0.5iou_thres是NMS的IoU阈值同一目标多个框时用它去重0.45是工程里常见值。注意scale_coords这一步模型输入分辨率通常会被resize成640如果不把坐标缩放回原始尺寸画框位置会偏。工程里mydetect.py实际代码可能封装得更简洁但核心就是attempt_load加载权重、non_max_suppression做后处理这两件事。2.3 重新训练权重的边界识别的是重训后的类别表格务实用要有上下文放在2.3里权重来源类别数适合场景问题官方yolov5s.pt80类COCO通用目标检测类别标签是person/phone没有玩手机语义工程自带best.pt3类自定义驾驶室分心行为必须确认类别顺序和训练时一致自己重训best.pt按标注定义新增喝水/抽烟细分动作需要匹配数据集的标签映射文件这个边界很多人踩过。工程代码里的类别索引是从0开始数的如果你的权重只有3类而代码里硬编码了4个类别名去匹配索引错位会直接导致检测到喝水却报警抽烟。我一般拿到项目第一件事是打印model.names把实际类别顺序和UI里的报警条件对照一遍这个习惯能省掉半天调试时间。2.4 把推理结果交给跟踪与UI的约定检测框出来后下一步是把结果传给Deepsort更新跟踪器同时把报警信号抛给UI线程。工程里myframe.py承担了帧处理和检测结果的流转常见做法是维护一个全局的报警状态字典# 常见做法定义一个帧结果对象供UI线程读取 class FrameResult: def __init__(self): self.items [] # 检测与跟踪结果列表 self.fatigue_state {} # 疲劳状态eye_close, yawn等 result_holder FrameResult() def update_ui_state(det_results, tracker_updates): # 把检测结果和跟踪结果合并成UI消费的结构 for track in tracker_updates: if track.time_since_update 0: # 只有当前帧确认的轨迹才更新报警 label track.get_det_label() if label in (playphone, smoke, drink): result_holder.items.append({ label: label, bbox: track.to_tlbr(), conf: track.confidence })这里的关键是time_since_update这个字段。Deepsort会对每个目标维护生命周期如果一帧丢失track不会立刻消失而是进入未更新状态。UI层如果拿到的是旧轨迹报警会迟滞好几帧。所以约定是只用time_since_update 0的轨迹去刷新UI避免框和实际位置错位。这套约定在工程里可能就是几行代码的事但决定报警的实时性。3. Dlib疲劳检测与Perclos闭眼、打哈欠的量化口径3.1 68点关键点与EAR/MAR原理疲劳检测这条线依赖的是shape_predictor_68_face_landmarks.dat这个Dlib模型。它能定位人脸68个关键点其中36到47是眼睛轮廓点48到67是嘴部轮廓点。判断闭眼和打哈欠不能靠眼睛看起来小不小得用几何比值消除人脸距离和姿态的影响。EAREye Aspect Ratio是眼睛纵横比取眼睛上下眼皮关键点的垂直距离除以左右眼角水平距离。闭眼时这个值会明显下降。MARMouth Aspect Ratio同理是嘴部上下关键点的垂直距离除以嘴部宽度打哈欠时嘴巴张开MAR会变大。这两个比值是尺度不变的人脸离镜头远近变化不会导致阈值失效这是它适合做实时判断的根本原因。3.2 myfatigue.py里EAR/MAR计算与阈值判断工程里myfatigue.py的核心就是对人脸关键点算EAR和MAR然后和阈值比较。我按常见实现还原一下计算逻辑from scipy.spatial import distance as dist def eye_aspect_ratio(eye_landmarks): # eye_landmarks是6个点按Dlib索引顺序排列 # 计算垂直方向两组点的欧式距离 vertical_1 dist.euclidean(eye_landmarks[1], eye_landmarks[5]) vertical_2 dist.euclidean(eye_landmarks[2], eye_landmarks[4]) # 计算水平方向一组点的欧式距离 horizontal dist.euclidean(eye_landmarks[0], eye_landmarks[3]) # 加一个极小值防止除零 ear (vertical_1 vertical_2) / (2.0 * horizontal 1e-6) return ear def mouth_aspect_ratio(mouth_landmarks): # mouth_landmarks取嘴部外圈6个关键点 vertical_1 dist.euclidean(mouth_landmarks[1], mouth_landmarks[7]) vertical_2 dist.euclidean(mouth_landmarks[2], mouth_landmarks[6]) horizontal dist.euclidean(mouth_landmarks[3], mouth_landmarks[5]) mar (vertical_1 vertical_2) / (2.0 * horizontal 1e-6) return mar阈值不是玄学是有统计依据的正常睁眼时EAR一般大于0.2闭眼时会掉到0.15以下MAR在嘴巴闭合时约0.2打哈欠时能超过0.6。但这个值会因人脸形状略有浮动工程里可以提供一个配置入口微调。我建议设置两个阈值EAR低于0.18判定闭眼帧MAR高于0.55判定哈欠帧连续帧数超过设定值再触发报警。3.3 Perclos疲劳度统计的窗口口径Perclos是这里最容易写歪的地方。理论定义是闭眼时间占总时间的百分比但工程上必须明确统计窗口。常见做法是用滑窗统计最近60秒内闭眼帧占比超过40%判定疲劳。class PerclosCounter: def __init__(self, window_size600, threshold0.4): # window_size是帧数30fps下600帧就是20秒 # threshold表示闭眼帧占比超过40%触发疲劳 self.window_size window_size self.threshold threshold self.buffer [] def update(self, is_eye_closed): self.buffer.append(1 if is_eye_closed else 0) if len(self.buffer) self.window_size: self.buffer.pop(0) close_ratio sum(self.buffer) / len(self.buffer) return close_ratio self.threshold, close_ratio窗口选多少帧直接决定报警灵敏度。窗口太长短暂的疲劳状态被平均掉窗口太短偶尔眨眼也会触发报警。30fps下20秒窗口是工程上比较稳的折中既能过滤单次眨眼又能捕捉持续的困倦模式。3.4 在1.0版本中为什么可以去掉点头检测原版疲劳检测包含点头行为点头的特征是头部关键点整体位移的周期性变化。但1.0版本把点头去掉了只保留闭眼和打哈欠。这个改法从工程角度说得通点头检测依赖头部姿态估计和时序趋势分析如果只用Dlib的68点很难区分点头和低头看中控屏误报率会很高。保留闭眼和打哈欠这两个几何特征明确的动作系统更稳。复现时不用想着把点头加回来除非你同时接入IMU或专用的头部姿态模型。4. main.py与Qt UI集成Deepsort跟踪与两条检测线的调度4.1 主程序main.py的线程与帧流设计这套系统最容易卡住新手的地方不在检测算法而在main.py怎么把摄像头帧、YOLOv5推理、Dlib关键点检测和UI刷新串在一个程序里。单纯把检测逻辑写在UI的paintEvent里会让界面卡死因为推理是阻塞操作。工程里的常见做法是开一个独立的工作线程循环读帧、跑检测、写结果主线程只负责显示。# main.py中的典型线程结构PyQt5实现 import cv2 import sys from PyQt5.QtCore import QThread, pyqtSignal from mydetect import DistractDetector from myfatigue import FatigueDetector class DetectThread(QThread): frame_ready pyqtSignal(object, object) # 发送当前帧和检测结果 def __init__(self, source0): super().__init__() self.source source self.distract_det DistractDetector(weights/best.pt) self.fatigue_det FatigueDetector(shape_predictor_68_face_landmarks.dat) self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running: ret, frame cap.read() if not ret: break # 两条检测线共享同一帧 distract_items self.distract_det.detect_frame(frame) fatigue_state self.fatigue_det.analyze(frame) self.frame_ready.emit(frame, {distract: distract_items, fatigue: fatigue_state}) cap.release()这里的source可以是摄像头索引0也可以是视频文件路径。用pyqtSignal把帧和结果一起发出UI线程接收到信号后只做绘制这样即便推理掉帧界面也不会卡死。4.2 Deepsort在其中的作用Deepsort在这套系统里的定位容易被误解它不做检测只做多目标跟踪。它的输入是YOLOv5的检测框输出是稳定的轨迹ID。为什么要加这一层因为驾驶室场景里手部动作是连续的直接拿单帧检测结果去做报警一旦某一帧漏检报警信号就会闪烁。Deepsort把历史关联信息用起来让同一个目标持续跟踪一小段时间抑制漏检导致的报警抖动。# 常见做法初始化DeepSort并使用检测框更新轨迹 from deep_sort import DeepSort deep_sort DeepSort(deep_sort/deepsort.yaml) def update_tracker(detections, frame): # detections是YOLOv5输出的[x1, y1, x2, y2, conf, cls]列表 if len(detections) 0: # 空检测也要调用update让未匹配的轨迹进入丢失状态 tracked deep_sort.update(empty_boxes, frame) else: tracked deep_sort.update(detections, frame) return tracked工程目录里出现的Deepsort改动常见套路是把卡尔曼滤波的观测噪声调大一点让轨迹更平滑。这个参数藏在deepsort的yaml配置文件里复现时如果觉得框在抖优先调这个不要动检测阈值。4.3 Qt UI事件循环和报警状态位ui_mainwindow.py是基于Qt Designer生成的mainwindow.ui编译出来的界面。UI里通常有一个视频显示控件和几个状态指示灯。工程做法是用QLabel或QOpenGLWidget显示帧报警状态放在一组QLabel的背景色切换上。工作线程发信号过来时UI线程根据疲劳状态和危险行为状态刷新红绿灯。4.4 运行流程和参数开关入口在main.py的main函数里。一个简洁的调用流程是创建QApplication加载mainwindow.ui启动DetectThread把frame_ready信号连接到槽函数。槽函数内部把帧转成QPixmap显示到界面把检测结果映射到报警区域。整套流程跑通后你能直观看到玩手机框出一个边界框眼睛闭上超过阈值时疲劳指示灯变红。5. 部署避坑从权重路径到摄像头视角的五个常见问题5.1 模型加载路径没改对导致启动即崩溃现象程序启动时报文件找不到或ONNX加载失败。原因工程里weights目录和shape_predictor_68_face_landmarks.dat的路径是按作者机器写的你下载后目录层级变了相对路径失效。解决在main.py或mydetect.py里把这两个路径改成绝对路径或者用os.path.join基于项目根目录拼路径。不要用Windows下的反斜杠硬编码统一用正斜杠或Path对象。5.2 检测类别数对不上报警永远不触发现象程序能运行画面上也有检测框但玩手机、抽烟、喝水永远不会触发。原因你替换了best.pt新权重是80类COCO模型代码里用索引去匹配类别名时全部落空。解决打印model.names把实际类别顺序和代码里的class_names对齐。如果权重是3类确保索引0、1、2对应顺序正确。这个坑我复现时踩过当时花了两小时查UI代码最后发现是类别映射错位。5.3 侧脸导致关键点漂移闭眼误报现象驾驶员转头看后视镜或侧方来车时系统频繁报疲劳闭眼。原因Dlib的68点检测在侧脸角度超过45度时精度骤降眼角的点会漂移到脸颊边缘导致EAR计算失真。解决在FatigueDetector里加一个姿态判断只当正脸置信度大于某阈值才计算EAR。或者配置摄像头视角向下倾斜15度左右让驾驶员面部始终以接近正脸的姿态出现在画面中。5.4 Perclos窗口太短疲劳报警抖动现象疲劳指示灯在疲劳和正常之间来回跳。原因统计窗口只有两三秒一次长闭眼就拉高闭眼比率眨眼又迅速拉低。解决把窗口拉长到600帧约20秒并且加一个防抖逻辑连续3次判定疲劳才真正触发报警。可以把这个时间窗口参数提取到配置文件中方便现场调试。5.5 UI读取不到摄像头或视频文件现象程序启动后画面全黑控制台无报错但卡在视频流循环里。原因摄像头索引写死了0但机器上摄像头索引可能是1或者视频文件是GIF格式cv2.VideoCapture对GIF支持不好工程images目录里的gif主要用于演示。解决把source改成命令行参数或UI输入框摄像头用cap.isOpened()校验失败就弹提示框。视频文件优先用mp4或avi。6. 进阶用法用一段自录视频把阈值调到现场可用6.1 准备测试素材和视频流入口拿到的工程不要直接上摄像头先用一段自录视频做离线调参。自己用手机横屏录制20秒正常驾驶画面再录20秒模拟疲劳和分心动作的画面。中间刻意转头、低头、打哈欠、拿起手机制造典型正样本。把视频路径传给DetectThread工程里改一行source test_normal.mp4即可。这样反复调参不打扰真车环境。6.2 可调参数清单表格务必要有参数位置默认值调参方向conf_thresmydetect.py0.4误报多则升到0.5漏检多则降到0.3EAR闭眼阈值myfatigue.py0.18脸大或戴眼镜可降到0.16MAR哈欠阈值myfatigue.py0.55检测不到哈欠降到0.45Perclos窗口myfatigue.py600帧报警抖动则加长到900帧Perclos占比myfatigue.py0.4疲劳判定过敏感则升到0.45调整后跑一遍正常驾驶视频统计误报次数再跑一遍疲劳动作视频看是否都能触发报警。两个指标取平衡点不要只看单一视频。6.3 在myframe.py里加一个统计逻辑验证调参结果# 在myframe.py的帧处理循环中追加统计逻辑 class MetricStats: def __init__(self): self.total_frames 0 self.false_alarm_frames 0 def log(self, fatigue_alarm, actual_fatigue): self.total_frames 1 # actual_fatigue是手动标注的真值用了F1思路做验证 if fatigue_alarm and not actual_fatigue: self.false_alarm_frames 1 stats MetricStats() # 在拿到fatigue_state后调用 fatigue_alarm fatigue_state.get(alarm, False) stats.log(fatigue_alarm, actual_fatigueTrue) # 测试视频里这段知道是疲劳这套统计逻辑不复杂但它能把感觉还行变成误报率有数。我当时调EAR阈值时从0.2降到0.16误报从每10分钟12次降到2次代价是漏掉一次快速闭眼。现场部署一定要用视频把这两个数测出来心里才有底。6.4 复现这套系统我的固定验证顺序从那以后我每次复现这种双检测任务系统都强制走一遍验证先确认模型类别名和权重匹配再拿一段标注过的视频测误报和漏报最后才接摄像头。这套流程帮我排掉过至少三次类别错位和两次阈值失衡的问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表