ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于YOLO与MediaPipe的违规驾驶行为识别系统实战解析

2026/10/7 12:44:59 拓冰建站 浏览量
基于YOLO与MediaPipe的违规驾驶行为识别系统实战解析 简介面向Python毕业设计的违规驾驶行为识别系统以源码与数据库文件打包交付适合计算机、人工智能及相关专业学生用于课程设计、毕业设计与二次开发。压缩包约64.93MB包含128个文件以81个Python源码文件为主体用于业务逻辑、算法实现与界面交互另有8个Shell脚本辅助环境安装与任务调度4个pkl与2个pth权重文件用于加载预训练模型2个npy文件保存处理后的数据6个png图片可展示效果或界面同时配合md文档、txt说明、pyx扩展和license许可等覆盖模型部署、数据读写、工程配置与文档整理多个环节。目前已有295人学习下载。材料包含可直接运行的项目工程、数据库文件、模型文件与脚本工具并附有README、CHANGELOG等说明文档便于理解目录结构与运行流程使用者可在此基础上调整识别参数、扩充数据集、修改界面逻辑也能为毕业设计论文中的系统设计、算法说明与实验分析直接提供素材。1. 违规驾驶行为识别系统是什么从人工查监控到自动筛漏日常场景里车辆监控后台通常挂着几十路视频违规驾驶行为识别系统要做的就是把这堆回放交给程序筛一遍司机有没有打电话、抽烟有没有连续闭眼、打哈欠然后把命中片段连同时间戳写成一条结构化记录。这类项目在毕业设计里最常见的交付形态就是“源码 数据库.zip”里面一般是一套 Python 推理脚本、若干模型文件、建表 SQL 和一条条行为记录。它服务的对象也很具体——需要交毕设的学生、打智能交通类比赛的小组、想给自家车队做试点的小团队。做之前先给个反直觉的结论这套项目里最花时间的往往不是模型训不出来而是“检测到了”和“算违规”之间的那一层规则。YOLO 能把手机框出来MediaPipe 能告诉你眼睛闭没闭但一件事要持续多久才算违规、怎么避免一帧误检就报警这些才是真正决定系统能不能用的地方。2. 先把架构立住为什么是 YOLO MediaPipe而不是全用端到端模型整套系统的技术栈看着杂但拆开就三层目标检测负责找“违规物品”人脸关键点负责看“疲劳状态”规则层负责把零散帧的检测结果变成一条条可入库、可追溯的报警记录。这一章把选型理由和整体数据流讲清楚后面写代码时才不会跑偏。2.1 检测方案选型YOLOv8n、YOLOv5s 与 Faster R-CNN 的取舍“违规驾驶行为识别”里最先要解决的是感知问题画面里有没有手机有没有点燃的烟司机嘴部是否张开、眼睛是否闭合。做感知的常见方案有三种我一般直接按这张表来选模型单帧推理速度小目标表现显存占用适合场景YOLOv8n快CPU 上也能跑中等车舱内小目标偏弱低毕设、试点项目首选YOLOv5s较快资料最多中等中需要大量教程、踩坑成本最低Faster R-CNN明显慢较好高只看离线录像不追求实时我的习惯是默认 YOLOv8n。车舱这种固定场景里目标不那么零散用 nano 版本在 CPU 上也能有十几帧的推理速度演示和毕设答辩都够用如果后面要上 Jetson 这类边缘盒子转 ONNX 也很顺。YOLOv5s 的优势是网上现成训练教程最多遇到报错搜起来快。Faster R-CNN 只在极看重小物体精度、完全不需要实时性的场合才值得换。疲劳判定则走另一条路用 MediaPipe 的 FaceMesh 抽 468 个人脸关键点再拿眼角、嘴角的几何比例算闭眼和打哈欠。选它的原因是这种几何指标天然具备跨人的鲁棒性——脸大脸小不影响比值不像直接丢一个 CNN 分类网络那样对训练数据的拍摄角度敏感。端到端方案比如视频分类网络听起来省事实际做行为识别时正负样本边界极模糊什么叫“拿手机一秒钟”、什么叫“打哈欠的幅度”标注一致性很难保证训练起来比拆成两段更痛苦。2.2 系统分层采集、检测、规则判定、持久化四层怎么串整个系统我按四层来组织每一层只做自己的事。采集层负责从摄像头或离线视频里抽帧检测层把每帧送进 YOLO 和 FaceMesh只输出“检测到手机”“当前 EAR 值”这类事实规则层拿着这些事实做时间维度的判断持久化层最后把报警结构化写进数据库。数据流 视频帧 → 抽帧 → YOLO 物品检测手机/烟/水杯 → FaceMesh 关键点 → EAR / MAR 计算 ↓ 规则状态机多帧投票、持续时间判断 ↓ 行为记录start_time / end_time / behavior_type ↓ SQLite / MySQL 写入“源码 数据库.zip”里的数据库部分我习惯建两张核心表。video_info存视频元信息behavior_record存行为事件。特别注意一点行为记录要存成“事件对”而不是流水账也就是一条记录里同时有开始时间和结束时间这样后续统计“今天打电话几次、总共持续多久”都方便。CREATE TABLE video_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, video_name TEXT NOT NULL, file_path TEXT NOT NULL, fps REAL NOT NULL, duration_sec REAL NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE behavior_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, video_id INTEGER NOT NULL, behavior_type TEXT NOT NULL CHECK (behavior_type IN (phone, smoke, yawn, eye_close)), start_time REAL NOT NULL, end_time REAL NOT NULL, confidence REAL NOT NULL, frame_range TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里把fps单独存下来是有原因的后续判断“闭眼持续多久”要基于时间而不是帧数否则视频帧率一变同样的帧数代表的时间长度完全不同。behavior_type用 CHECK 约束限定四类免得脏数据混进来。很多毕设项目里数据库落地就直接用 SQLite因为交付时只要带上.db文件就能演示“数据库增删改查”全套功能如果学校明确要求 MySQL只需把连接串换成pymysql或者 SQLAlchemySQL 本身几乎不用改。2.3 运行环境python 版本与依赖安装的版本坑这套系统的环境坑集中在 Python 与 torch、mediapipe 的版本匹配上不能无脑pip install。我的建议是用 conda 单独建一个环境Python 版本锁定在 3.8 到 3.10 之间太低跑不了新版 MediaPipe太高容易出现轮子缺失。# requirements.txt ultralytics8.2.0 opencv-python4.9.0.80 mediapipe0.10.11 numpy1.26.4 torch2.1.0 torchvision0.16.0 pymysql1.1.0安装时最容易翻车的点是 torch 默认装成 CPU 版跑是能跑但速度打折扣。先执行python -c import torch; print(torch.cuda.is_available())看一眼如果机器有 N 卡而且返回 True就不用折腾如果是 False 但有 N 卡去 PyTorch 官网按 CUDA 版本复制对应安装命令。MediaPipe 这边则要记得它要求的 protobuf 版本范围内不能乱升装完先跑一个最小 FaceMesh 样例验证别等整套代码跑起来才报TypeError之类的兼容错误。3. 实现疲劳驾驶检测用眼角和嘴部的几何比例判断闭眼与哈欠疲劳驾驶是这套系统里规则最成熟、也最容易调出效果的部分。核心思路不复杂用 FaceMesh 拿到眼睛和嘴巴周围的关键点坐标算几何比例再用时间维度上的连续帧数做判定。这一章从数学定义到最小代码把“闭眼判疲劳、张嘴判哈欠”完整跑通。3.1 疲劳指标的数学定义EAR、MAR 与连续帧判定疲劳检测里最常用的两个指标是 EAREye Aspect Ratio眼部纵横比和 MARMouth Aspect Ratio嘴部纵横比。EAR 的计算方式是取眼睛周围 6 个关键点用纵向距离除以横向距离人睁眼时这个比值稳定在 0.3 左右闭眼时急剧下降到 0.1 上下。EAR 公式EAR (||P2 - P6|| ||P3 - P5||) / (2 × ||P1 - P4||)MAR 公式类似把眼部关键点换成嘴部关键点用上下嘴唇的纵向距离比上左右嘴角的横向距离。打哈欠时嘴张大MAR 会超过平时的阈值。两套指标都不是绝对值甚至不同设备同一个人也会有细微差别所以我在项目里从不直接套死阈值而是先录一段司机正常驾驶的素材取 EAR 和 MAR 的均值作为基准再乘一个比例系数作为阈值。常见起点是 EAR 阈值 0.25、MAR 阈值 0.6但最终要在自己的视频集上微调。时间维度的判定规则我通常写成“连续 N 帧超过阈值才触发”——单帧的检测噪声太多连续帧过滤能挡掉大部分误判。比如闭眼判定连续 40 帧 EAR 低于阈值才记一次疲劳这个“40 帧”要在 30fps 视频里换算成时间大约是 1.3 秒符合正常人眨眼时长的量级。3.2 MediaPipe 抽取脸部关键点与 EAR / MAR 的最小代码下面这段是从视频流里抽人脸关键点并计算 EAR、MAR 的最小实现。FaceMesh 可以输出 468 个关键点其中眼睛和嘴各取固定索引。import cv2 import math import mediapipe as mp # FaceMesh 关键点索引 LEFT_EYE [33, 160, 158, 133, 153, 144] RIGHT_EYE [362, 385, 387, 263, 373, 380] MOUTH [78, 81, 13, 311, 308, 402, 14, 82] def dist(p1, p2): return math.hypot(p1.x - p2.x, p1.y - p2.y) def aspect_ratio(landmarks, idx_list): # 纵向两点距离之和 / 横向两点距离 top dist(landmarks[idx_list[1]], landmarks[idx_list[5]]) bottom dist(landmarks[idx_list[2]], landmarks[idx_list[4]]) width dist(landmarks[idx_list[0]], landmarks[idx_list[3]]) return (top bottom) / (2.0 * width) mp_face_mesh mp.solutions.face_mesh face_mesh mp_face_mesh.FaceMesh( static_image_modeFalse, max_num_faces1, min_detection_confidence0.5, min_tracking_confidence0.5 ) cap cv2.VideoCapture(test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # MediaPipe 要求 RGBOpenCV 读出来是 BGR rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb) if results.multi_face_landmarks: lm results.multi_face_landmarks[0].landmark ear (aspect_ratio(lm, LEFT_EYE) aspect_ratio(lm, RIGHT_EYE)) / 2.0 mar aspect_ratio(lm, MOUTH) print(fEAR{ear:.3f} MAR{mar:.3f}) cap.release()代码里最需要注意的两处一是 OpenCV 读入的帧是 BGR而 MediaPipe 的 FaceMesh 接口接收 RGB必须在 process 前转换否则关键点定位会明显偏移二是min_detection_confidence控制在 0.5 左右比较合适太低会把椅子、方向盘误判成人脸太高在逆光或戴墨镜时干脆检测不到人脸。EAR 取左右眼的平均值是为了抵消侧脸时单眼被遮挡带来的抖动MAR 则只取全嘴的比值不做左右对称处理。关于这个计算资源的一个提醒FaceMesh 的process本身比较吃 CPU常规做法是先把视频抽帧到 10 fps 再送进去疲劳行为属于慢变量不需要每秒分析 30 帧。3.3 疲劳阈值的参数调法与侧脸干扰的过滤阈值不是一个固定数而是要根据素材调整。我常用的调法是把一段带人工标注的视频喂给上面的脚本画出 EAR 随时间变化的曲线然后看闭眼段落在图上对应的数值区间再在这个区间和正常值之间取中间点作为阈值。下面是典型的起点参数表参数默认值调节方向EAR 阈值0.25频繁误报就调低漏报就调高MAR 阈值0.6打哈欠漏了就调低正常说话误报就调高连续闭眼帧数40 帧按视频帧率换算时间固定 1.2 秒左右连续张嘴帧数15 帧对应一次明显哈欠的时长下限侧脸干扰是疲劳检测里最普遍的问题。司机转头看后视镜时鼻子会把一只眼睛挡住EAR 会被严重压低直接触法闭眼报警。我的处理办法是一帧里如果 FaceMesh 输出的人脸置信度低于 0.7 或者只有单侧眼睛的关键点可用就跳过这一帧不参与连续帧计数再用一个长度为 15 的滑动窗口取 EAR 中位数代替原始值做判断这样偶尔一两帧的抖动不会把结论带偏。4. 实现违规行为识别YOLO 检测结果怎么变成报警记录疲劳之外更大的需求是识别“接打电话”“抽烟”“喝水”这类分心驾驶行为。这一层我的做法是 YOLO 先检测物品类别再用多帧投票看这个物品在画面里是不是持续出现。核心思想很简单——短暂拿起手机和持续摆放手机在单帧上长得一模一样只有时间维度能区分。4.1 用 YOLO 检测手机、香烟、水杯的推理代码车舱内检测手机这类目标通常不直接拿 COCO 预训练模型开箱即用因为 COCO 的 80 类里包含手机但“香烟”“水杯”这类小目标是缺失的。常见做法是用 YOLOv8n 的 COCO 预训练权重做底在自己的数据集上把类别改成三类phone、cigarette、cup微调几十个 epoch 就够了。推理代码写法如下。from ultralytics import YOLO model YOLO(yolov8n.pt) # 换成自己微调后的权重 classes [67] # COCO 里 67 对应手机自定义权重则对应自己的类别 id results model.predict( frame, conf0.55, iou0.45, classesclasses, verboseFalse ) for r in results: boxes r.boxes for box in boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) print(fclass{cls_id} conf{conf:.2f})conf0.55是经验值。调太低方向盘、圆形的仪表盘会被误判成手机调太高光线暗的小手机会被漏掉。iou0.45用于去重同一个目标同时被输出多个重叠框时只保留一个。classes参数用来过滤类别只检测自己需要的目标能省掉不少误报——车舱背景里“人”这个类别一直在变不参与行为判定最好直接从推理里排除。一个很现实的坑是用 COCO 预训练模型检测手机效果尚可但检测香烟几乎不可用因为香烟在 COCO 里根本没有类别。所以凡是做到“抽烟识别”这一步的项目几乎都要自己收集数据来训练而这个问题我会放到第五章避坑里详细说。4.2 多帧投票状态机怎么区分“短暂拿起”和“持续使用”单帧检测出手机 ≠ 正在打电话。司机可能只是拿起来看一眼又放下这个动作应当被记成一次“短暂分心”而不是“持续违规”。我一般用一个滑动窗口做投票窗口内出现某类物品的帧数占比超过一定比例才触发行为状态。from collections import deque class BehaviorVoter: def __init__(self, window_size15, threshold0.6): self.window deque(maxlenwindow_size) self.threshold threshold def update(self, cls_id): # cls_id 为 None 表示这一帧没有检测到目标 self.window.append(cls_id) def is_active(self, target_cls): if not self.window: return False hit sum(1 for c in self.window if c target_cls) return (hit / len(self.window)) self.thresholdwindow_size15在 30fps 的视频里代表半秒threshold0.6意味着半秒里至少 9 帧检测到同一类目标才判定“正在使用”。这个比例可以过滤掉单帧的“幽灵检测”因为目标检测的误检通常是瞬时噪声很难连续多帧命中同一类别。这里有个参数说明窗口太长会让报警反应迟钝窗口太短又起不到过滤作用。判定“违规行为持续”的另一个关键是持续时间不是帧数而是时间。我习惯给行为记录设一个“持续 5 秒才报警”的规则用上一节读到的视频 fps 把帧数换算成时间或者直接用时间戳。打一个电话至少要持续数秒而拿起手机看一眼通常不会超过 2 秒这个天然的时间分界线比什么模型都可靠。4.3 报警记录入库写数据库的时间与去重策略行为状态确认后要写behavior_record表。这里最容易犯的错误是一条一写导致报警刷屏我记得见过有的记录表里同一司机五秒内出现七八条“phone”记录数据完全没法看。所以我在写数据库前先把“行为必须闭合”这个逻辑讲清楚一个行为从开始时间到结束时间只对应一条记录。def insert_behavior(cursor, video_id, btype, start_t, end_t, conf): sql INSERT INTO behavior_record (video_id, behavior_type, start_time, end_time, confidence) VALUES (%s, %s, %s, %s, %s) cursor.execute(sql, (video_id, btype, start_t, end_t, conf))写入前做一次查重同一个video_id、同一个behavior_type、且时间区间和已有记录重叠超过一半的就更新end_time把事件延长而不是新插入。这是“数据库增删改查”里少见的“改”多于“增”的场景。事件闭合还有一个附带好处统计报表时可以按behavior_type分组做聚合直接算出每天哪类违规最多不用再清洗脏数据。行为持续过程中要不要实时更新end_time取决于项目需求。毕设答辩场景里我建议每 10 秒更新一次即可频繁 UPDATE 对 SQLite 这种文件型数据库压力较大没必要。5. 常见问题与避坑从训练到部署的 5 个真实翻车点这一章把我在做这类系统时踩过、也在帮别人排查时看过的坑汇总一下。每一条都按“现象 → 原因 → 解决”来写这些坑几乎不看项目代码就一定会遇到提前知道能省下大量调试时间。5.1 数据集没覆盖夜间与雨天导致模型在夜幕降临时疯狂漏检现象白天测试的检测准确率挺高一到夜间视频里手机、人脸大面积漏检偶尔还把路灯误判成手机。原因公开数据集和自采数据集里白天样本占了绝大多数模型对夜间低照度、强光反射的特征没有学到。尤其是车舱内开灯状态下挡风玻璃的反光会把目标检测的特征完全打乱。解决收集夜间、雨天、逆光三类素材补进去做数据增强。数据增强里加hsv随机变化和brightness扰动基本是标配。夜间实在没素材的对帧先做一次自适应直方图均衡化再送进模型能缓和一部分光照问题但治标不治本最稳妥还是补数据。5.2 侧脸时关键点漂移导致打哈欠误报现象司机正常转头看后视镜系统报了一次“打哈欠”司机手托下巴说话系统报了“疲劳闭眼”。原因MediaPipe 的人脸网格在正面时最稳定大角度侧脸时部分关键点被遮挡或坐标漂移MAR 会瞬间被撑大EAR 被压小直接就触发了阈值。解决三层过滤。第一FaceMesh 输出的置信度低于 0.7 的帧不参与统计第二EAR、MAR 先过低通滤波再判断我用的是 15 帧滑动窗口中位数第三判定条件里加上“左右眼的 EAR 同时低于阈值才算闭眼”避免单眼遮挡造成误判。5.3 阈值设得太紧报警记录刷屏到没法看现象系统上线后一天产生数百条报警记录其中大量是同一司机拿手机划了一下屏幕就触发一条。监控后台完全被淹没真正的疲劳事件反而没被看到。原因把“单帧检测到目标”直接当成“违规行为”来入库没有经过多帧投票也没有设置冷却时间。解决行为判定必须走状态机。检测结果先进入滑动窗口命中比例超过 0.6 才进入“候选违规”状态从候选到正式报警还要满足持续时长条件最后加一个冷却时间同一种行为在 120 秒内只报一次。学生做的毕设里这一条最容易被忽视因为演示的时候环境干净、只测一段干净视频看起来确实准时报警拿到真实素材里就原形毕露。5.4 视频帧率不一致同一套阈值在不同素材上表现天差地别现象一段 30fps 的离线录像检测正常换到 15fps 的线上视频素材后“连续 40 帧闭眼才报警”的规则整段失效因为 40 帧在 15fps 下代表 2.6 秒条件比原来苛刻得多。原因把“N 帧”当成了规则的绝对单位忽略了帧率和时间的关系。帧是采集频率的产物不是行为的固有属性。解决所有规则判断统一换算成时间。闭眼持续 1.2 秒、哈欠持续 0.5 秒这些值固定不变帧数只是“当前视频帧率 × 目标时长”算出来的临时参数。video_info表里存 fps 的目的就在这。5.5 Python 环境与 torch 版本安装翻车模型跑不起来现象pip 安装 ultralytics 后跑推理提示torch.cuda.is_available()为 False或者mediapipe与protobuf版本冲突导致导入报错。原因默认 pip 源安装的是 CPU 版 torchMediaPipe 对 protobuf 版本有较强约束版本一升就出现二进制接口不匹配的报错。解决用 conda 单独建 Python 3.9 环境torch 从官网按 CUDA 版本挑选安装命令没有独显就老老实实用 CPU 版跑yolov8n这在小视频上完全能跑只是慢一些。MediaPipe 装在 requirements 里时把版本号钉死不要写“”让它自由浮动。6. 进阶把单文件检测器改造成可上线的行为告警服务做到这里你已经能对一段视频输出一条条行为记录了。但离“能持续运行的服务”还差一步——把逐帧推理塞进一个独立线程做好帧率归一化和结果验证。这一章的技巧能让你的项目在结构和说服力上明显高出一个档次。6.1 用队列和独立线程把抽帧与推理解耦我见过很多同学把整个检测循环写在主程序里一个while True里同时做读帧、推理、显示、写库帧率稍快一点就卡住。常见做法是用生产者-消费者模式一个线程读帧一个线程做行为判定中间靠队列传递最新帧。import threading import queue frame_queue queue.Queue(maxsize2) def capture_loop(video_path): cap cv2.VideoCapture(video_path) while True: ret, frame cap.read() if not ret: break # 丢旧帧保最新queue 满时清掉旧帧 if frame_queue.full(): try: frame_queue.get_nowait() except queue.Empty: pass frame_queue.put(frame) def infer_loop(): while True: frame frame_queue.get() results model.predict(frame, conf0.55, verboseFalse) # 规则状态机在这里消费检测结果 process(results) t1 threading.Thread(targetcapture_loop, args(test.mp4,)) t2 threading.Thread(targetinfer_loop) t1.start(); t2.start()线程解耦最大的好处是“抽帧是实时源、推理按能力消费”。如果摄像头 30fps 输出而单帧推理要 80ms系统自动丢帧也不会堆积延迟。这也是后端服务化改造的原型之后把infer_loop换成 HTTP 接口或者消息队列消费端架构都不用变。6.2 验证方法用标注片段算检出率而不是“看几眼觉得很准”最后说说怎么证明你的系统是可靠的这也是一份成果交付时最有说服力的部分。我一般准备 10 段短视频5 段包含目标行为的正样本和 5 段正常驾驶的负样本。跑完后统计两点——正样本里行为正确触发的比例检出率、负样本里误报的次数。比如“打手机行为的检出率是 80% 以上负样本误报不超过 1 次”这个指标比任何截图都有说服力。在调参阶段我会把 EAR/MAR 曲线画出来对照检测结果做诊断。画曲线时注意帧数一多横坐标必然拥挤用 matplotlib 加plt.xticks(range(0, total, 30))之类的方式抽稀不然图形压成一团黑看不出趋势这也算是一个实操小细节。我自己在这个项目上最吃亏的就是一开始拿着模型输出直接当报警记录冷却和去重逻辑都是事后补的改得满盘皆乱。先把事件模型想清楚再写推理代码顺序对了整个项目都会顺很多。希望帮到你。本文还有配套的精品资源点击获取