
简介基于深度学习骨骼点的人体姿态估计算法构建司机驾驶状态检测告警系统面向计算机相关专业学生和开发者适用于疲劳驾驶、异常姿态识别等场景可支撑毕业设计、课程大作业或项目初期演示。资源包含完整Python源码、预训练模型文件和使用说明建议在PyCharm中配合Anaconda环境运行。压缩包共28个文件以13个py源码文件为主体涵盖主程序入口、姿态特征提取、随机森林分类等模块另含6张运行效果图、2个疲劳检测语音提示音、模型权重、项目说明文档等整体大小约147.66MB已有238人学习下载。代码中提供带中文注释的函数支持库、数据集处理脚本和详细说明便于理解从骨骼点提取、姿态识别到状态告警的完整链路同时支持二次开发和功能扩展。适合计算机相关专业学生用于课程设计、毕业设计及实践入门。1. OpenPose 司机驾驶状态告警系统判断疲劳之前先看骨骼点靠人脸闭眼判断疲劳是绝大多数初做这个方向的人的第一反应但这个项目给了我一个反直觉的结论判断驾驶状态骨骼点的优先级比人脸高得多。一个司机低头看手机、歪身子拿后座东西、手脱离方向盘这些行为在骨骼点上暴露得远比面部表情早。这套基于 OpenPose 的源码包把两件事同时做了——用骨骼点喂随机森林做姿态分类用 dlib 68 点人脸关键点算 EAR 做疲劳告警最终在视频帧上叠加可视化结果并触发语音提示。它适合两类人一类是正在做毕设或课程设计、需要能跑通、有模型、有数据集的深度学习应用型项目的人另一类是刚接触姿态估计、想知道 OpenPose 和 dlib 怎么协作的前端算法工程师。源码自带三个训练好的模型文件和完整说明文档不需要你从头训 OpenPose改改路径就能出效果。2. 三个模型各管一摊OpenPose 出姿态、dlib 盯眼睛、随机森林做决策这套系统的核心不是某个算法多先进而是三个模型分工明确OpenPose 负责把人的骨架从图像里抽出来dlib 负责从脸上抓眼睛和嘴巴的几何特征随机森林负责把骨架特征归类成打电话、喝水、低头这类具体行为。理解这三者的边界你就知道改哪里、调哪里。2.1 Light-OpenPose 与 MobileNet 骨架实时性才是车载场景的第一诉求原版 OpenPose 在 GPU 上跑都吃力更别说车载场景。这个项目用的是 Light-OpenPosemodules 目录下的 with_mobilenet.py 定义了网络结构checkpoint_iter_370000.pth 是训练了 37 万步的权重文件。用 MobileNet 做骨干网络把 VGG 特征提取层换掉换来的是参数量下降一个量级。import torch from modules.pose import Pose # with_mobilenetTrue 时走轻量骨架False 时走原版 VGG 骨架 pose_model Pose(with_mobilenetTrue) checkpoint torch.load(models/checkpoint_iter_370000.pth, map_locationcpu) # 非严格模式加载避免模型结构微调后 key 对不上直接崩 pose_model.load_state_dict(checkpoint, strictFalse) pose_model.eval()strictFalse是很多人容易忽略的坑。如果你自己改了 modules 里网络结构的层名用默认的严格加载会直接抛 RuntimeError。用非严格模式未匹配的 key 只会告警不会中断。map_locationcpu 保证在没有 CUDA 的机器上也能加载权重。推理时输入固定到 256x256 分辨率输出是 19 张热图——18 个骨骼点各一张加一张背景图。每个点的坐标就是从对应热图的响应峰值位置反推出来的。2.2 dlib 68 点人脸关键点EAR 和 MAR 才是疲劳判据OpenPose 能看出头歪了、手垂了但看不出眼睛闭上了、嘴巴张太久。这部分交给 dlib。shape_predictor_68_face_landmarks.dat 用 iBUG 300W 数据集训练检测 68 个面部关键点。其中眼睛 6 个点、嘴巴 20 个点用这些点坐标可以算出两个关键指标EAREye Aspect Ratio眼睛纵横比和 MARMouth Aspect Ratio嘴巴纵横比。import dlib import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor( models/shape_predictor_68_face_landmarks.dat) # dlib 68点索引中左眼是36-41右眼是42-47嘴巴外轮廓是49-59 LEFT_EYE list(range(36, 42)) RIGHT_EYE list(range(42, 48)) MOUTH list(range(49, 60)) def eye_aspect_ratio(eye_pts): # 竖向量眼睑上下距离 v1 np.linalg.norm(eye_pts[1] - eye_pts[5]) v2 np.linalg.norm(eye_pts[2] - eye_pts[4]) # 横向量内外眼角距离 h np.linalg.norm(eye_pts[0] - eye_pts[3]) return (v1 v2) / (2.0 * h) def mouth_aspect_ratio(mouth_pts): # 上嘴唇中心到下巴尖与左右嘴角宽度的比值 v1 np.linalg.norm(mouth_pts[2] - mouth_pts[8]) v2 np.linalg.norm(mouth_pts[4] - mouth_pts[6]) h np.linalg.norm(mouth_pts[0] - mouth_pts[6]) return (v1 v2) / (2.0 * h)正常睁眼时 EAR 在 0.25~0.33 之间闭眼时会降到 0.12 以下。疲劳判断的逻辑是EAR 持续低于阈值超过一定帧数就认为闭眼MAR 持续高于 0.6 就认为在打哈欠。帧数阈值很关键眨眼只有 0.2~0.4 秒不能一帧低于阈值就叫醒司机否则系统会变成每眨一次眼响一次警报。2.3 RandomForest 分类器把骨骼点坐标映射成驾驶行为OpenPose 输出的 18 个骨骼点坐标在 256x256 图上不能直接用于分类同一个行为在不同距离下坐标数值差异很大。get_train.py 做的事就是把坐标转成姿态特征——各关节点相对置信度加权的位移、躯干夹角、手腕与肩膀的相对位置——然后交给随机森林。这里用随机森林而不是神经网络是务实的选择。骨骼点特征维度不高几十维的量级随机森林两百棵树就能学明白可解释性好还不需要做特征标准化。项目自带的 RandomForestClassifier_model.pkl 就是在这个逻辑下训练好的分类器支持 State Farm 数据集的 10 类驾驶行为。加载后直接 predict 即可推理耗时可以忽略。import joblib clf joblib.load(models/RandomForestClassifier_model.pkl) # 假设 keypoints_vector 是 get_train.py 生成的 54 维特征 # 前 18 维是 x 坐标18-35 维是 y 坐标36-53 维是置信度 behavior_id clf.predict([keypoints_vector])[0] behavior_name [safe, text_right, call_right, text_left, call_left, adjust_radio, drink, reach_back, hair_makeup, talk_passenger][behavior_id]随机森林有 200 棵树时单帧推理在毫秒级这是它能和 OpenPose 串行跑在同一个进程里而不拖垮帧率的原因。Project 里把 RF 和 OpenPose 分开处理OpenPose 单独用 GPU 或 CPU 出骨骼点RF 只吃特征向量两者解耦方便你单独替换分类器。3. 先把项目跑起来Anaconda 环境、模型放置与 main.py 主循环源码包下载下来先别急着双击 main.py你得先理解三件事Python 版本选哪个、模型文件放哪个目录、main.py 里哪些逻辑被硬编码了。很多人在这一步栽跟头不是因为代码写错而是环境与目录结构和项目假设的不一样。3.1 Python 版本选择与依赖安装3.7/3.8 是最稳的区间项目说明里明确建议 Python 3.7 或 3.8这个建议背后是实打实的兼容性问题。dlib 在 Python 3.9 以上需要自己编译OpenCV 的高版本与 PyTorch 老版本在 NumPy API 上有冲突。在 Anaconda 里新建一个虚拟环境是最省事的做法。conda create -n driver python3.8 conda activate driver pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python dlib scikit-learn pandas numpy如果 CPU 机器上不想额外装 CUDA 版 PyTorch上面这种方式只装 CPU 版省了两个 G 的下载量。dlib 在 Windows 上容易编译失败优先试pip install dlib如果报错缺失 CMake就用conda install -c conda-forge dlib直接装编译好的二进制。项目没有打包 requirements.txt 以外的锁定版本这意味着 torch、numpy 的版本错配全要自己扛。torch 和 numpy 的版本错配是最隐蔽的坑PyTorch 2.x 配 NumPy 1.24 在加载老权重时可能出现格式告警但通常不影响推理反过来 NumPy 1.19 配新 OpenCV 会直接抛 API 不存在的报错。装完环境后先用下面这个命令验证一遍python -c import torch, cv2, dlib, sklearn; print(torch.__version__, cv2.__version__, dlib.__version__, sklearn.__version__)输出完版本号只代表 import 成功不代表能跑通推理。我习惯再花十秒钟跑一段最小代码随机生成一张 256x256 的图过一遍 pose_model能出热图说明后端没问题。3.2 目录结构与模型摆放三个模型少一个都会静默失败项目的模型文件分成三个checkpoint_iter_370000.pth 是 OpenPose 权重shape_predictor_68_face_landmarks.dat 是 dlib 人脸关键点模型RandomForestClassifier_model.pkl 是行为分类器。这三者必须都在 models 目录下但加载失败时的表现完全不一样。OpenPose 权重缺失时程序会在 import 阶段抛 FileNotFoundError这类错误是显性的好排查dlib 模型缺失时同样抛异常最坑的是 pkl 缺失——有些版本的 joblib 加载时不会立刻报错而是等你调用 predict 时才抛 np 维度相关的诡异报错。所以模型文件路径必须是绝对路径或从项目根目录开始的相对路径不要在代码里写死C:\Users\xxx\...这种带中文或特殊字符的路径。# 正确做法基于当前文件位置推导项目根目录 import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) MODEL_DIR os.path.join(BASE_DIR, models) pose_path os.path.join(MODEL_DIR, checkpoint_iter_370000.pth) dlib_path os.path.join(MODEL_DIR, shape_predictor_68_face_landmarks.dat) rf_path os.path.join(MODEL_DIR, RandomForestClassifier_model.pkl)这样无论你是 pycharm 里跑还是命令行跑只要项目目录整体搬迁路径就不会断。video 目录放的是侧置摄像头与前置摄像头两个视频源音频在 sound 目录下自动播放。3.3 main.py 主流程双路推理与告警触发逻辑main.py 的执行入口逻辑不是单模型全流程而是三块并行视频帧读取、双路推理、触发告警状态机。每帧先取灰度图喂给 dlib 做 68 点检测同时把 BGR 帧压缩到 256x256 喂给 OpenPose。疲劳状态是累积的不是单帧判定的。# main.py 核心循环的结构化伪代码按项目实际文件整理 import cv2 import numpy as np cap cv2.VideoCapture(video/front.mp4) # 或换成侧置摄像头 skip_frames 3 # OpenPose 推理慢时每隔 3 帧做一次姿态推理 frame_count 0 ear_threshold 0.2 mar_threshold 0.6 closed_eye_frames 0 yawn_frames 0 ALARM_FRAMES 8 # 连续 8 帧低于阈值才触发告警 while cap.isOpened(): success, frame cap.read() if not success: break frame_count 1 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 人脸 68 点检测每一帧都做实时性不受影响 faces detector(gray, 0) if len(faces) 0: shape predictor(gray, faces[0]) left_ear eye_aspect_ratio([shape.part(i) for i in LEFT_EYE]) right_ear eye_aspect_ratio([shape.part(i) for i in RIGHT_EYE]) ear (left_ear right_ear) / 2.0 mouth mouth_aspect_ratio([shape.part(i) for i in MOUTH]) if ear ear_threshold: closed_eye_frames 1 if closed_eye_frames ALARM_FRAMES: play_wav(sound/eyes.mp3) else: closed_eye_frames 0 # 骨骼点推理跳帧执行 if frame_count % skip_frames 0: points extract_keypoints(frame) # 输出 18x3 的数组 feature build_feature(points) # 转成 54 维特征 behavior_id clf.predict([feature])[0] if behavior_id in (1, 2, 6, 8): # 打电话/喝水/化妆视为高风险 draw_warning(frame, behavior_id)闭眼计数用帧数而不是时间理由是不同机器帧率差异巨大——30 FPS 下连续 8 帧闭眼约 0.27 秒但在 15 FPS 的机器上就是 0.53 秒。想要在不同帧率下保持一致的灵敏度更靠谱的做法是以时间为单位记录睁眼/闭眼的时间戳计算连续闭眼时长超过 0.5 秒触发告警。4. 用自己的数据训练随机森林get_train.py 与特征 CSV 全流程只跑通 main.py 是第一步真正有价值的操作是重新走一遍数据链路拿原始图片提取骨骼点特征训练自己的分类器。只有到了这一步你才算是从会用跨到能改。4.1 State Farm 数据集10 类驾驶行为的标注体系State Farm Distracted Driver Detection 是 Kaggle 上一个经典竞赛训练集有 2 万多张驾驶员图片每张图标注一个行为类别。这十条类别是安全驾驶、右手发短信、右手打电话、左手发短信、左手打电话、操作收音机、喝水、拿身后的东西、整理头发和化妆、与乘客交谈。每类约 2400 张图整体分布相对均衡但安全驾驶本身就占近 2.4 万张。用这个数据集要注意图片是驾驶员在模拟驾驶舱里被多个摄像头拍的视角是固定的侧前方。这和实际车载摄像头的视角不完全一致但作为姿态分类器的训练数据足够了。如果你的场景是真正装车用这个模型做初版效果没问题但后期最好采集自己场景的数据微调。4.2 get_train.py 的提取逻辑从图片到 18 点骨骼特征get_train.py 的作用就是读图 → 跑 OpenPose → 提特征 → 写 CSV。核心逻辑分三步先对图片做尺寸归一化再将骨骼点坐标按身体结构转成相对特征最后拼接标签写到 CSV。直接丢原始坐标是常见错误——同一个姿势离镜头近和远坐标绝对值完全不同模型根本学不到通用规律。# get_train.py 核心逻辑整理 import csv import cv2 import numpy as np # 18 个 COCO 骨骼点的连接关系用于计算角度特征 BODY_CONNECTIONS [(0, 1), (0, 2), (1, 3), (2, 4), # 躯干到肩膀 (5, 7), (6, 8), # 手肘到手腕 (5, 6), (5, 11), (6, 12), (11, 13), (12, 14)] def build_feature(points): 把 18x3 的骨骼点转成 54 维特征向量 x points[:, 0] y points[:, 1] conf points[:, 2] # 归一化基准用颈部到髋部的中心距离作为尺度基准 neck points[1] # COCO 索引中 1 是颈部 hip_center (points[11] points[12]) / 2.0 scale np.linalg.norm(neck[:2] - hip_center[:2]) 1e-6 feat [] for i in range(18): feat.extend([(x[i] - neck[0]) / scale, (y[i] - neck[1]) / scale, conf[i] / 255.0]) return np.array(feat)这段代码把坐标系从图片像素转成以颈部为原点、以躯干长度为尺度的相对坐标系。为什么要这样司机身形高矮不一、座椅前后位置不同只有相对特征才能让模型在不同人之间泛化。置信度除以 255 是因为 OpenPose 热图输出的是 0~255 的像素值不归一化会让随机森林把置信度当成主导特征。此外你应该留意姿势角度特征——左右手臂与躯干的夹角、头部相对躯干的偏转角——这些是区分打电话和摸方向盘的关键。若原始坐标加置信度让模型效果不佳再自行加入角度特征维度上升到 70 左右随机森林依然能处理。4.3 openposeRandomForest.ipynb训练、评估与导出 pklget_train.py 输出 CSV 后openposeRandomForest.ipynb 负责训练模型。这个 notebook 把随机森林训练、交叉验证、混淆矩阵展示都串在一起。它的训练环节有几个参数值得注意n_estimators 定为 200max_depth 限定为 12class_weight 使用 balanced。import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split import joblib df pd.read_csv(openpose_train_data.csv) X df.drop(columns[label]) y df[label] # 分层抽样State Farm 类别不绝对均衡stratify 防止某类在训练集/验证集里比例失真 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42) clf RandomForestClassifier( n_estimators200, max_depth12, class_weightbalanced, random_state42, n_jobs-1 ) clf.fit(X_train, y_train) # 验证集上的分类准确率baseline 方案约在 85~90% 区间 print(clf.score(X_val, y_val)) joblib.dump(clf, models/RandomForestClassifier_model.pkl)用 class_weightbalanced 的原因如果某类行为在数据集中偏少比如喝水这类动作出现频率低随机森林会倾向把所有样本都判成多数类balanced 会给少数类更高惩罚权重。n_jobs-1 让所有 CPU 核心并行训练200 棵树在几十维特征上训练只需几十秒。验证集评估后看混淆矩阵重点看左撇子与右撇子的对应误判——很多人左右手动作镜像骨骼点特征容易混淆这是姿态类模型的通病。5. 避坑与常见问题环境报错、黑屏与骨骼点抖动源码包能跑通是基础但从能跑到稳定跑之间隔着不少坑。下面这几条是我反复踩过的按现象、原因、解决的顺序写节省你的排查时间。5.1 ModuleNotFoundError 连环报错一次装齐还是边报边装现象是 main.py 运行后第一个 import 就抛 ModuleNotFoundError。装上缺失的包后下一个 import 又报新的错误。循环往复。原因是项目依赖多个库且部分库之间存在版本绑定关系——OpenCV 与 NumPy 版本要匹配PyTorch 与 TorchVision 必须配套。边报边装看似省事实际把依赖树越装越乱。我的处理方式是先看 requirement.txt 里列了哪些库一次性装齐基础项然后运行python -c import main验证导入再根据报错补充缺失项。更稳妥的办法是用 conda 从 conda-forge 通道装 dlib 和 opencvconda 会自动解析依赖版本。5.2 模型路径错误导致的静默失败黑匣子式翻车现场现象是程序不报错但输出结果是乱的——要么所有帧都判定成同一个类别要么随机森林 predict 时抛出ValueError: operands could not be broadcast together之类的维度错误。原因是模型文件加载了但内容不对或者路径指向了一个损坏的 pkl。最常见的是在 root 权限下跑程序模型文件用了相对路径当前工作目录不是项目根目录。解决方式是在运行前打印实际加载的路径并检查文件大小RandomForestClassifier_model.pkl 大小如果只有几 KB大概率是空的。我还习惯在加载后立刻跑一次 predict 用假特征验证模型可交互。# 加载后立即做一次 sanity check import numpy as np dummy_feature np.zeros((1, 54)) try: clf.predict(dummy_feature) except Exception as e: print(f模型可能已损坏: {e})5.3 摄像头与视频文件切换黑屏但程序不报错现象是程序启动后画面全黑但控制台没有任何报错。原因在于 cv2.VideoCapture 的参数含义传整数代表打开摄像头设备号传字符串代表打开视频文件。如果你把视频文件的路径写对了但文件名里有中文或空格OpenCV 在 Windows 上会静默失败读到空帧。另外笔记本自带摄像头设备号通常是 0外接摄像头可能是 1这个数值在不同机器上不一样。解决方式是先用一个三行脚本测摄像头索引确认后再改 main.py 的 VideoCapture 参数。对于视频文件把文件直接放到 project 根目录下的 video 目录用相对路径引用避免中文路径问题。5.4 骨骼点抖动导致误报One Euro Filter 参数不会调现象是司机明明正常目视前方OpenPose 输出的手腕点却在几个像素范围内高频抖动随机森林把这种抖动识别成了手部移动触发告警。原因是 OpenPose 单帧推理本身有噪声尤其在低分辨率输入下手腕这类小目标的响应峰值位置跳动。项目里 modules 目录下的 one_euro_filter.py 就是为这个准备的。One Euro Filter 的核心参数是min_cutoff和betamin_cutoff 控制低频信号保留程度beta 控制速度变化时滤波器的响应速度。我一般把 min_cutoff 设成 1.0beta 设成 0.7 作为初始值再根据实际视频调。from modules.one_euro_filter import OneEuroFilter # 每个关节点的 x、y 各挂一个滤波器 filters {i: OneEuroFilter(min_cutoff1.0, beta0.7) for i in range(18)} def smooth_points(points, filters): smoothed [] for i in range(18): fx filters[i].filter(points[i][0], timestamp) fy filters[i].filter(points[i][1], timestamp) smoothed.append([fx, fy, points[i][2]]) return np.array(smoothed)需要注意是一个关节点对应两个滤波器x 和 y不是整个骨架共用一个。timestamp 参数必须递增如果丢帧会传出一个跳变大的时间间隔导致滤波器对下一次输出产生异常放大。处理方式是维护一个全局帧计数器当作时间戳。6. 把 EAR 阈值改成自适应一个能直接替换的小函数项目默认的 EAR 阈值是固定值 0.2这对大多数人是够用的但不同人脸型、不同摄像头分辨率下睁眼 EAR 的分布有差异——有的人基线是 0.3有的是 0.26用同一个 0.2 阈值前者比后者更早触发闭眼判断。更合理的方式是开机前自动校准基线。# 放在 main.py 的 cap.open 之后采集前 60 帧做基线统计 ear_history [] for _ in range(60): success, frame cap.read() if not success: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) if len(faces) 0: shape predictor(gray, faces[0]) ear eye_aspect_ratio([shape.part(i) for i in LEFT_EYE]) ear_history.append(ear) # 取耳部 EAR 的 75 分位作为该司机的睁眼基准 # 不用均值是因为偶尔的闭眼帧会被混进基线里拉低均值 import numpy as np ear_base np.percentile(ear_history, 75) dynamic_threshold ear_base * 0.78 print(f校准完成: 基线 EAR {ear_base:.2f}, 动态阈值 {dynamic_threshold:.2f})为什么用 75 分位数而不是均值采集前 60 帧时司机大概率有几次眨眼。闭眼帧的 EAR 值极低0.05~0.1 量级平均值会被这些闭眼帧拉低导致基线偏低、动态阈值偏宽松。75 分位数能过滤掉那些闭眼瞬间的极端低值更贴近正常睁眼状态。乘以 0.78 是经验系数——意味着闭眼程度达到正常睁眼的 22% 以上就判定为闭眼这个幅度与 PERCLOS 标准的阈值设计逻辑一致。我从那次把这个函数替换进 main.py 之后系统误报率肉眼可见地下降了。同一台摄像头从原来的打哈欠把手放脸上也报警变成真正闭眼超过 0.4 秒才报警。如果你在现场部署记得把 60 帧的校准期提示做成开机画面文字不然司机不知道为什么要等两秒才启动检测。从那以后我每次拿到新的摄像头或换人测试都会强制走一遍这个校准流程——虽然多花一分钟但换来的阈值适配性对检测体验的提升非常值得。希望帮到你。本文还有配套的精品资源点击获取