尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于MediaPipe的违规驾驶行为识别系统设计与实现

基于MediaPipe的违规驾驶行为识别系统设计与实现 简介一套面向计算机视觉/智能交通方向的毕设级Python项目源码专注于驾驶行为中的违规动作识别覆盖数据准备、模型训练到推理检测的完整链路。压缩包内含128个文件打包大小约64.93MB其中以81个py源码脚本为核心辅以sh部署脚本、pth权重、pkl序列化文件、npy数据文件及多份md/txt说明文档另有png可视化结果与pyc缓存文件可支撑环境搭建、二次训练与结果展示。已有1060人在CSDN学习或下载。除主体识别系统代码外包内还整合了依赖清单、LICENSE、README与CHANGELOG等工程化文档目录结构清晰便于对照复现适合需要以真实项目完成毕业设计、课程设计或积累视觉实战经验的开发者可直接运行调试也可围绕模型与接口扩展优化。1. 违规驾驶识别这个毕设题真正要解决的不是训练模型有的同学拿到这个题目第一反应是去找数据集、训一个行为分类网络结果标了一周数据发现答辩日期先到了。违规驾驶行为识别在毕设清单里的真实难度恰恰不在“识别”两个字而在于把一条开源视觉管线做成能实时响应的事件系统摄像头画面进来报警出去中间每一步状态变化都能解释给老师听。适合的人群是已会 Python 基础语法、想避开训练成本又能把工程做完整的同学。核心路径是用 MediaPipe 提取人脸和手部关键点再用 EAR、MAR、头部姿态这些几何特征做规则判定。这套方案没有训练步骤所有阈值可调落盘、截图、指标都能写进论文。2. 识别原理与选型关键点坐标如何变成行为判定2.1 从“检测到关键点”到“判定为违规”中间隔着一层规则引擎行为识别系统在架构上必须拆成两层感知层和判别层。感知层的任务只有一个——从 RGB 帧里拿到人脸 468 个关键点、手部 21 个关键点的归一化坐标判别层拿这些坐标算几何量比如眼睛开合程度、嘴巴张开幅度、鼻尖相对眼睛的位移再和阈值比较输出“闭眼”“打哈欠”“转头看手机”。两层之间通过坐标数组通信不混业务逻辑。这个分层不是写代码的习惯问题而是决定了毕设能不能收尾。判别层用规则而不是神经网络意味着你不需要准备大量带标签的行为视频只需要在 OpenCV 窗口里慢慢调几个阈值就能覆盖疲劳驾驶、分心驾驶、手持电话这三类最常见的违规场景。调阈值的过程本身就是毕设答辩时最有说服力的素材老师问“为什么这个值是 0.2”你能直接指着一帧画面上叠加的 EAR 数值回答而不是说“网络学出来的我也说不清”。网上流传的同类源码包质量差别很大很多只是把 MediaPipe 官方示例改了个界面单帧检测看起来有效一旦连续跑视频流报警就会反复抖动。原因就是没有在判别层做时间维度上的累积判断。后续章节的实现会从单帧特征一直做到带状态机的连续事件流。2.2 为什么选 MediaPipe 而不是 OpenPose 或 YOLOv8-Pose毕设选型要同时看三个约束能不能用 CPU 跑、能不能 pip 装完就运行、关键点数量够不够描述行为。三者都满足的目前最省心的是 MediaPipe。下表是三个方案在同样一台无 GPU 笔记本上的对比方案模型体积CPU 实时帧率关键点安装与使用毕设友好度OpenPose200MB5 FPS 以下身体 135 点需编译依赖复杂低YOLOv8-Pose6MB~50MBCPU 勉强 10 FPS身体 17 点pip 可装但人脸细节不足中MediaPipe10MB 以内30 FPS 左右人脸 468 手 21pip install mediapipe 即用高选 MediaPipe 的决定性理由不是精度而是它同时给你人脸和手两套关键点且在同一个进程里跑两个模型依旧能维持实时。OpenPose 的身体关键点能看出手是否靠近嘴但看不出眼睛是睁开还是闭上检测疲劳驾驶必须额外接一个人脸模型YOLOv8-Pose 的手腕点足够用来做“手靠近耳朵”的判断但 17 个点里没有眼皮和嘴唇的语义做打哈欠检测要自己再训练分类头。MediaPipe 的 FaceMesh 拥有眼皮、嘴唇、鼻尖的独立索引手部模型又有手掌中心和指尖坐标一个程序里能把闭眼、张嘴、低头、举电话四件事全做了。2.3 EAR、MAR 与低头角三个可解释的行为特征EAREye Aspect Ratio眼睛纵横比是疲劳检测最通用的指标。取单只眼睛周围 6 个关键点用两个垂直距离的平均值除以水平距离得到一个与脸到相机距离无关的比值。眨眼时 EAR 会掉到 0.15 左右正常平视在 0.25 到 0.35。公式和 MediaPipe 索引对应如下左眼索引[33, 160, 158, 133, 153, 144]右眼索引[362, 385, 387, 263, 373, 380]其中第 0 位和第 3 位是内、外眼角第 1、2 位是上眼皮第 4、5 位是下眼皮。EAR (d1 d2) / (2 * d0)分子是两个垂直距离分母是眼裂宽度。MARMouth Aspect Ratio嘴部纵横比用来识别打哈欠取上内唇点 13、下内唇点 14 的距离除以嘴角 61 与 291 的距离。哈欠时嘴部高度显著增加MAR 通常在 0.6 以上正常说话在 0.3 到 0.5。低头角则用一个近似量鼻尖点索引 1相对两眼中心连线中点的 y 方向偏移。图像坐标系里 y 轴向下平视时鼻尖在眼睛下方偏移量为正低头时鼻尖在画面中相对眼睛上移偏移量变小甚至变负。这三个特征全部基于归一化坐标计算不受画面分辨率影响。但注意MAR 和低头角对摄像头安装位置敏感摄像头装在挡风玻璃上方正中与装在仪表盘右侧的结果会差不少阈值必须在自己的拍摄条件下重新标定。3. 用 Python 搭最小检测管线人脸与手部联合提取3.1 工程目录、依赖与模型初始化先落地一个能跑的工程结构把后续的判别逻辑和日志功能留出位置避免写到后面把 main.py 堆成一千行driving_behavior/ ├── detectors/ │ ├── __init__.py │ ├── landmarks.py # 帧关键点提取 │ └── behavior.py # EAR/MAR/低头角计算与判定 ├── core/ │ ├── state_machine.py # 连续帧状态机 │ └── logger.py # CSV 日志与截图 ├── config.py # 所有阈值集中管理 ├── main.py # 视频/摄像头主循环 └── requirements.txt依赖安装很简单假定你已经在 Windows 或 Ubuntu 上装好了 Python 3.9 以上的环境在项目目录执行pip install opencv-python mediapipe numpyopencv-python负责视频读取和画面绘制mediapipe提供人脸和手部两套关键点模型numpy用来做坐标向量计算。三个库都不需要 GPU 就能运行安装完可以立刻用python -c import mediapipe验证是否装好。对入门来讲这是最省心的组合没有编译步骤也不涉及 CUDA 配置。3.2 一帧输入两个模型的输出如何对齐在detectors/landmarks.py里写一个类统一封装 MediaPipe 的两个模型。这个类只做一件事输入一帧 BGR 图像输出人脸关键点对象和手部关键点对象。import cv2 import mediapipe as mp class FrameLandmarks: def __init__(self): self.mp_face mp.solutions.face_mesh self.mp_hands mp.solutions.hands self.face self.mp_face.FaceMesh( static_image_modeFalse, max_num_faces1, refine_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.5, ) self.hands self.mp_hands.Hands( static_image_modeFalse, max_num_hands2, min_detection_confidence0.5, min_tracking_confidence0.5, ) def extract(self, frame_bgr): rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) rgb.flags.writeable False # 标记只读MediaPipe内部可跳过复制提速 face_res self.face.process(rgb) hand_res self.hands.process(rgb) face_pts hand_pts None if face_res.multi_face_landmarks: face_pts face_res.multi_face_landmarks[0] if hand_res.multi_hand_landmarks: hand_pts hand_res.multi_hand_landmarks[0] return face_pts, hand_pts这个类里有几个参数值得解释。refine_landmarksTrue会让 FaceMesh 额外输出瞳孔中心等 10 个点原始 468 点的索引不变瞳孔信息将来扩展注视方向检测时可以直接用而且这个开关还能轻微提升关键点稳定性默认建议开着。max_num_faces1是因为驾驶舱内我们只关心主驾驶位的人多一个人脸反而会让行为判定找不到目标。min_detection_confidence和min_tracking_confidence都取 0.5是权衡“漏检”和“抖动”之后的经验值调高到 0.7 会显著减少误检但在光照变化时会频繁丢失目标调低到 0.3 跟踪更连续却容易把窗外的广告牌人形误判成驾驶员。extract方法的返回值是 MediaPipe 官方的 landmark 对象里面每个关键点都有x、y、z三个属性且x、y已经归一化到 0 到 1。归一化坐标意味着行为特征与摄像头分辨率无关你也可以直接用landmark.x * frame.shape[1]换算回像素坐标画框打点都方便。3.3 行为判定函数与阈值参数表在detectors/behavior.py里写几何特征计算和行为判定。先定义关键点索引常量再实现 EAR、MAR 和低头角三个函数import numpy as np LEFT_EYE [33, 160, 158, 133, 153, 144] RIGHT_EYE [362, 385, 387, 263, 373, 380] MOUTH_UP, MOUTH_DOWN 13, 14 MOUTH_LEFT, MOUTH_RIGHT 61, 291 NOSE_TIP 1 EYE_LEFT_OUTER, EYE_RIGHT_OUTER 33, 263 def distance(p1, p2): return np.linalg.norm(np.array([p1.x, p1.y]) - np.array([p2.x, p2.y])) def calc_ear(landmarks): def eye_ear(idx): p [landmarks.landmark[i] for i in idx] return (distance(p[1], p[5]) distance(p[2], p[4])) / (2 * distance(p[0], p[3])) return (eye_ear(LEFT_EYE) eye_ear(RIGHT_EYE)) / 2 def calc_mar(landmarks): up landmarks.landmark[MOUTH_UP] down landmarks.landmark[MOUTH_DOWN] left landmarks.landmark[MOUTH_LEFT] right landmarks.landmark[MOUTH_RIGHT] return distance(up, down) / distance(left, right) def calc_look_down(landmarks): nose landmarks.landmark[NOSE_TIP] left landmarks.landmark[EYE_LEFT_OUTER] right landmarks.landmark[EYE_RIGHT_OUTER] eye_mid_y (left.y right.y) / 2 return nose.y - eye_mid_y # 平视为正低头变小或变负calc_ear对左右眼分别计算后取平均好处是单眼被头发遮挡时数据不会瞬间跳变。calc_mar用上下唇距离除以嘴角宽度做归一化所以脸离摄像头近还是远不影响结果。calc_look_down返回的不是真正的欧拉角而是鼻尖相对眼睛连线的 y 偏移这个量实现成本最低对“低头看手机”这个动作已经足够敏感。判定函数把这些特征和阈值比较输出一个行为字典def judge_behaviors(landmarks, hand_landmarks, threshold): if landmarks is None: return None ear calc_ear(landmarks) mar calc_mar(landmarks) nose_delta calc_look_down(landmarks) behavior { ear: ear, mar: mar, closed_eye: ear threshold[EAR_CLOSED], yawning: mar threshold[MAR_YAWN], look_down: nose_delta threshold[NOSE_DELTA], phone: False, } if hand_landmarks is not None: hand_center hand_landmarks.landmark[9] # 中指根部近似手掌中心 ear_point landmarks.landmark[127] # 右耳外沿点 if distance(hand_center, ear_point) threshold[HAND_EAR_DIST]: behavior[phone] True return behavior所有阈值集中在config.py方便调参THRESHOLD { EAR_CLOSED: 0.20, # EAR 低于 0.2 判定闭眼 MAR_YAWN: 0.60, # MAR 高于 0.6 判定哈欠 NOSE_DELTA: -0.03, # 鼻尖相对眼睛的偏移低于 -0.03 判定低头 HAND_EAR_DIST: 0.12, # 手与耳朵距离小于 0.12 判定打电话 }这里每个阈值都要解释一下取值范围。EAR 阈值在 0.18 到 0.25 之间常见戴眼镜会略微影响结果取值 0.2 比较保守避免把正常眨眼当疲劳。MAR 阈值 0.6 对应“嘴明显张大”如果测试视频里哈欠总被漏检降到 0.5 即可。NOSE_DELTA是最需要现场重标的阈值它跟摄像头安装高度强相关建议先用正常驾驶画面打印出这个值再低头看手机打印出这个值取两者中线。HAND_EAR_DIST使用归一化距离0.12 大约是画面宽度的 12%手举到耳边时通常小于这个数手放在方向盘上时大于 0.25。4. 从单帧判定到连续监测状态机、事件日志与性能取舍4.1 用状态机消除阈值抖动别让报警反复横跳单帧判定直接驱动报警画面里只要有一帧 EAR 掉到阈值以下就会触发一次“疲劳报警”下一帧恢复正常又消失。这种抖动在答辩演示里非常难看。正确做法是加一个三态状态机SAFE安全、WARNING警告、ALERT报警。状态转移规则如下当前状态转移条件下一状态SAFE最近 10 帧中有连续 3 帧判定违规WARNINGWARNING违规帧累计达到 30 帧ALERTWARNING连续 15 帧无违规SAFEALERT连续 15 帧无违规SAFE实现放在core/state_machine.pyclass BehaviorStateMachine: def __init__(self, warn_frames10, alert_frames30, recover_frames15): self.state safe self.violation_count 0 self.safe_count 0 self.warn_frames warn_frames self.alert_frames alert_frames self.recover_frames recover_frames def update(self, is_violation): if is_violation: self.violation_count 1 self.safe_count 0 else: self.safe_count 1 if self.state safe: if self.violation_count self.warn_frames: self.state warning self.violation_count 0 elif self.state warning: if self.violation_count self.alert_frames: self.state alert self.violation_count 0 if self.safe_count self.recover_frames: self.state safe self.violation_count 0 self.safe_count 0 return self.stateis_violation是单帧里任一行为判定为真的结果violation_count统计的是持续帧数不是总帧数。为什么用累计帧而不是“10 秒内违规次数”因为闭眼、低头这类行为天然是连续过程持续帧数直接反映行为时长而“10 秒 5 次”这种统计方式需要额外维护时间窗代码复杂度更高。recover_frames15让报警解除有一个延迟避免驾驶员闭眼一次、睁眼一帧、再闭眼时状态反复切换。这个状态机的输出要接在 UI 上同时驱动后续的日志和截图。4.2 事件落盘与告警截图CSV 和 JPG 各存什么报警不能只在终端打印一行毕设需要能回放评估的记录。设计一个简单的EventLogger在状态变为ALERT时写一条 CSV 记录并保存当前帧截图import csv import time from pathlib import Path import cv2 class EventLogger: def __init__(self, log_direvents): self.log_dir Path(log_dir) self.log_dir.mkdir(exist_okTrue) self.csv_path self.log_dir / violations.csv if not self.csv_path.exists(): with open(self.csv_path, w, newline) as f: csv.writer(f).writerow( [timestamp, state, ear, mar, phone, frame_no]) def log(self, frame, state, ear, mar, phone, frame_no): ts time.strftime(%Y%m%d_%H%M%S) with open(self.csv_path, a, newline) as f: csv.writer(f).writerow( [ts, state, round(ear, 3), round(mar, 3), int(phone), frame_no]) if state alert: img_path self.log_dir / falert_{ts}_frame{frame_no}.jpg cv2.imwrite(str(img_path), frame)CSV 里存的是判定依据不只是“报警了”这个结论。ear、mar这两列数值在后续计算指标和写论文分析时可以直接拿来画曲线比如截取报警前后各 5 秒画出 EAR 随时间下降的过程这是很直观的实验结果图。截图只在ALERT状态保存如果在WARNING就存图一次完整的疲劳过程会产生几十张高度相似的图片白白增加磁盘 IO。文件命名带时间戳和帧号方便和视频逐帧对齐。4.3 跳帧策略与多线程日志CPU 上维持实时性的手段MediaPipe 在普通笔记本 CPU 上处理一帧大约需要 20 到 40 毫秒加上 OpenCV 的画面绘制勉强能到 25 FPS。如果日志模块把写文件和视频处理放在同一个线程写盘瞬间主循环会被卡住画面直接掉到 15 FPS 以下。两个手段可以稳住实时性。第一个是跳帧处理行为判定不需要每帧都跑因为人闭眼、打哈欠都是持续几百毫秒的动作每秒处理 10 帧已经足够捕捉。在main.py的主循环里控制每 2 帧跑一次特征提取process_interval 2 frame_no 0 while cap.isOpened(): ok, frame cap.read() if not ok: break if frame_no % process_interval 0: landmarks, hands extractor.extract(frame) behavior judge_behaviors(landmarks, hands, THRESHOLD) state sm.update(any([behavior[closed_eye], behavior[yawning], behavior[look_down], behavior[phone]])) logger.log(frame, state, behavior[ear], behavior[mar], behavior[phone], frame_no) frame_no 1跳帧丢掉的不是报警本身只是报警的精确时间点对毕设来说完全可接受。第二个手段是把截图写盘放到线程池里避免主循环等磁盘from concurrent.futures import ThreadPoolExecutor executor ThreadPoolExecutor(max_workers1) def async_save(logger, frame, state, ear, mar, phone, frame_no): executor.submit(logger.log, frame, state, ear, mar, phone, frame_no)ThreadPoolExecutor在这里只开一个工作线程任务队列短内存占用可控。注意传给线程的frame必须copy()否则主循环里下一帧的读取操作会覆盖线程正在写的图像数据。5. 毕设验收前的三个加固技巧5.1 自制评估集按时间段标注比逐帧标注更省力答辩时老师几乎一定会问“效果怎么验证”。你需要一份自己的标注数据而不是只拿几段演示视频说“看着没问题”。做法是录制三段 30 秒左右的视频正常驾驶、打电话、瞌睡用start_s和end_s记录每段违规行为的时间区间存成 CSVstart_s,end_s,label 0.0,11.5,phone 14.2,21.8,yawn 25.0,29.5,closed_eye逐帧标注太耗时按时间段标注只需要对着播放器记两个时间点。评估时程序按帧号除以 FPS 换算成秒再查这个时刻是否落在标注区间内。5.2 指标计算精确率、召回率和 F1 的帧级实现有了标注 CSV写一个评估脚本把系统输出和标注对齐成逐帧的布尔数组import numpy as np def frame_label(events, t): return any(s t e for s, e, _ in events) def evaluate(gt_events, pred_events, total_frames, fps10): gt np.array([frame_label(gt_events, i / fps) for i in range(total_frames)]) pred np.array([frame_label(pred_events, i / fps) for i in range(total_frames)]) tp (gt pred).sum() fp (~gt pred).sum() fn (gt ~pred).sum() precision tp / (tp fp 1e-6) recall tp / (tp fn 1e-6) f1 2 * precision * recall / (precision recall 1e-6) return precision, recall, f1注意frame_label的时间对齐方式标注 CSV 里的start_s是视频时间轴秒数而程序输出 CSV 里的frame_no / fps也必须换算成同一个时间轴否则指标全是错的。视频较短时用any()线性扫描没问题长视频建议改用np.searchsorted把区间查询变成二分查找。5.3 展示画面上的数值叠层是最不值钱但最加分的细节演示时把 EAR、MAR 和当前状态直接绘制在画面左上角。这一步写的代码很少但对答辩观感提升极大老师一眼能看到判定依据在实时变化cv2.putText(frame, fEAR {ear:.2f} | MAR {mar:.2f} | {state}, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255) if state alert else (0, 255, 0), 2)顺手记录触发look_down那几帧的nose_delta值画一个时间序列折线图和 EAR 曲线并排放在论文的性能分析章节里。这一张图表就能同时说明特征定义、阈值来源和系统有效性比任何文字描述都有说服力。本文还有配套的精品资源点击获取
返回列表