前言:为什么需要人体姿态检测?
29.1 Pose Detection 的重要性
人体姿态检测在 IMS/OMS 中的应用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ┌─────────────────────────────────────────────────────────────────────────┐ │ Pose Detection 在 IMS/OMS 中的应用 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ IMS/OMS 场景需求: │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ • 驾驶员姿态监控(疲劳、分心、身体倾斜) │ │ │ │ • 乘员姿态检测(OOP 异常姿态) │ │ │ │ • 儿童位置检测(CPD 儿童检测辅助) │ │ │ │ • 乘员分类(成人/儿童/宠物) │ │ │ │ • 安全带佩戴检测(配合安全带位置分析) │ │ │ │ • 气囊部署决策(基于乘员姿态调整气囊策略) │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ MediaPipe Pose Detection 特点: │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ • 33 个 3D 关键点 │ │ │ │ • 实时性能(~5ms GPU) │ │ │ │ • 支持人体分割(背景去除) │ │ │ │ • 轻量级模型(~9MB) │ │ │ │ • 全身/上半身模式可选 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
29.2 功能特点
| 特性 |
说明 |
| 关键点数量 |
33 个 3D 点 |
| 坐标系 |
归一化坐标 + 世界坐标 |
| 人体分割 |
可选输出分割 mask |
| 速度 |
~5ms (GPU), ~15ms (CPU) |
| 模型大小 |
~9MB (Full Body) |
29.3 BlazePose 架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| ┌─────────────────────────────────────────────────────────────────────────┐ │ BlazePose 架构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 第一阶段:人体检测(Person Detection) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 输入:256×256 RGB 图像 │ │ │ │ 模型:BlazePose Detector │ │ │ │ 输出:人体边界框 + 关键区域 │ │ │ │ 速度:~2ms (GPU) │ │ │ │ │ │ │ │ 特点: │ │ │ │ • 基于轻量级 SSD 架构 │ │ │ │ • 多尺度特征金字塔 │ │ │ │ • 专为人体姿态优化 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 第二阶段:关键点检测(Pose Landmark) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 输入:256×256 裁剪的人体区域 │ │ │ │ 模型:Pose Landmark Model │ │ │ │ 输出:33 个 3D 关键点 + 分割 mask │ │ │ │ 速度:~3ms (GPU) │ │ │ │ │ │ │ │ 输出格式: │ │ │ │ • x, y: 归一化坐标 (0-1) │ │ │ │ • z: 相对深度 │ │ │ │ • visibility: 可见度 (0-1) │ │ │ │ • presence: 存在概率 (0-1) │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 第三阶段:追踪(Tracking) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 输入:上一帧关键点 │ │ │ │ 方法:基于关键点预测下一帧 ROI │ │ │ │ 追踪失败时:重新检测 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
三十、关键点布局
30.1 33 个关键点详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| ┌─────────────────────────────────────────────────────────────────────────┐ │ Pose Landmarks (33 points) │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 头部区域 (0-10) │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ ●0 (nose) │ │ │ │ ╱ ╲ │ │ │ │ ●1───● ●───●2 (eyes) │ │ │ │ │ │ │ │ │ │ ●3 ●4 (ears) │ │ │ │ │ │ │ │ ●5-●6-●7-●8-●9-●10 (mouth) │ │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 上肢区域 (11-22) │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ ●11 ●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●12 │ (shoulders) │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ●13 ●14 │ (elbows) │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ●15 ●16 │ (wrists) │ │ │ ╱│╲ ╱│╲│ │ │ │ ●17 ●18●19 ●20●21●22│ (hands) │ │ │ pinky index thumb pinky index thumb │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 下肢区域 (23-32) │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ ●23 ●24 │ (hips) │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ●25 ●26 │ (knees) │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ●27 ●28 │ (ankles) │ │ │ │ │ │ │ │ │ ●29 ●30 │ (heels) │ │ │ │ │ │ │ │ │ ●31 ●32 │ (feet) │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
30.2 关键点索引定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
namespace pose_landmark { constexpr int NOSE = 0; constexpr int LEFT_EYE_INNER = 1; constexpr int LEFT_EYE = 2; constexpr int LEFT_EYE_OUTER = 3; constexpr int RIGHT_EYE_INNER = 4; constexpr int RIGHT_EYE = 5; constexpr int RIGHT_EYE_OUTER = 6; constexpr int LEFT_EAR = 7; constexpr int RIGHT_EAR = 8; constexpr int MOUTH_LEFT = 9; constexpr int MOUTH_RIGHT = 10; constexpr int LEFT_SHOULDER = 11; constexpr int RIGHT_SHOULDER = 12; constexpr int LEFT_ELBOW = 13; constexpr int RIGHT_ELBOW = 14; constexpr int LEFT_WRIST = 15; constexpr int RIGHT_WRIST = 16; constexpr int LEFT_PINKY = 17; constexpr int RIGHT_PINKY = 18; constexpr int LEFT_INDEX = 19; constexpr int RIGHT_INDEX = 20; constexpr int LEFT_THUMB = 21; constexpr int RIGHT_THUMB = 22; constexpr int LEFT_HIP = 23; constexpr int RIGHT_HIP = 24; constexpr int LEFT_KNEE = 25; constexpr int RIGHT_KNEE = 26; constexpr int LEFT_ANKLE = 27; constexpr int RIGHT_ANKLE = 28; constexpr int LEFT_HEEL = 29; constexpr int RIGHT_HEEL = 30; constexpr int LEFT_FOOT_INDEX = 31; constexpr int RIGHT_FOOT_INDEX = 32; constexpr int UPPER_BODY[] = { LEFT_SHOULDER, RIGHT_SHOULDER, LEFT_ELBOW, RIGHT_ELBOW, LEFT_WRIST, RIGHT_WRIST }; constexpr int LOWER_BODY[] = { LEFT_HIP, RIGHT_HIP, LEFT_KNEE, RIGHT_KNEE, LEFT_ANKLE, RIGHT_ANKLE }; constexpr int FACE[] = { NOSE, LEFT_EYE, RIGHT_EYE, LEFT_EAR, RIGHT_EAR }; }
|
三十一、Graph 配置
31.1 完整 Graph 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| # ========== Pose Detection Graph 配置 ==========
# mediapipe/graphs/pose_tracking/pose_tracking_gpu.pbtxt
input_stream: "INPUT:Input"
output_stream: "LANDMARKS:pose_landmarks" output_stream: "SEGMENTATION:segmentation_mask" output_stream: "WORLD_LANDMARKS:pose_world_landmarks"
# ========== 1. 图像格式转换 ========== node { calculator: "ImageTransformationCalculator" input_stream: "INPUT:Input" output_stream: "IMAGE:converted_image" options { [mediapipe.ImageTransformationCalculatorOptions.ext] { output_format: SRGB } } }
# ========== 2. 人体检测 ========== node { calculator: "PoseDetectionCalculator" input_stream: "IMAGE:converted_image" output_stream: "DETECTIONS:detections" options { [mediapipe.PoseDetectionCalculatorOptions.ext] { model_path: "/models/pose_detection.tflite" score_threshold: 0.5 max_results: 1 } } }
# ========== 3. ROI 裁剪 ========== node { calculator: "DetectionToRectCalculator" input_stream: "DETECTIONS:detections" output_stream: "RECT:roi_rect" }
node { calculator: "RectTransformationCalculator" input_stream: "RECT:roi_rect" output_stream: "RECT:transformed_rect" options { [mediapipe.RectTransformationCalculatorOptions.ext] { scale_x: 1.25 scale_y: 1.25 } } }
# ========== 4. 关键点检测 ========== node { calculator: "TfLiteInferenceCalculator" input_stream: "IMAGE:converted_image" input_stream: "RECT:transformed_rect" output_stream: "TENSORS:landmark_tensors" output_stream: "TENSORS:segmentation_tensors" options { [mediapipe.TfLiteInferenceCalculatorOptions.ext] { model_path: "/models/pose_landmark.tflite" delegate { gpu { use_advanced_gpu_api: true } } } } }
# ========== 5. 关键点后处理 ========== node { calculator: "PoseLandmarksFromTensorCalculator" input_stream: "TENSORS:landmark_tensors" input_stream: "RECT:transformed_rect" output_stream: "LANDMARKS:pose_landmarks" output_stream: "WORLD_LANDMARKS:pose_world_landmarks" options { [mediapipe.PoseLandmarksFromTensorCalculatorOptions.ext] { num_landmarks: 33 } } }
# ========== 6. 人体分割 ========== node { calculator: "SegmentationPostprocessorCalculator" input_stream: "TENSORS:segmentation_tensors" output_stream: "MASK:segmentation_mask" options { [mediapipe.SegmentationPostprocessorCalculatorOptions.ext] { threshold: 0.5 } } }
|
三十二、姿态分析
32.1 身体朝向分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| #ifndef MEDIAPIPE_CALCULATORS_IMS_BODY_ORIENTATION_CALCULATOR_H_ #define MEDIAPIPE_CALCULATORS_IMS_BODY_ORIENTATION_CALCULATOR_H_
#include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/landmark.pb.h"
namespace mediapipe {
enum class BodyOrientation { FORWARD = 0, TURNING_LEFT = 1, TURNING_RIGHT = 2, LEANING_FORWARD = 3, LEANING_BACK = 4, UNKNOWN = 5, };
message BodyOrientationResult { BodyOrientation orientation = 1; float left_turn_angle = 2; float right_turn_angle = 3; float forward_lean_angle = 4; float confidence = 5; uint64 timestamp_ms = 6; }
class BodyOrientationCalculator : public CalculatorBase { public: static absl::Status GetContract(CalculatorContract* cc) { cc->Inputs().Tag("LANDMARKS").Set<std::vector<NormalizedLandmarkList>>(); cc->Outputs().Tag("ORIENTATION").Set<BodyOrientationResult>(); return absl::OkStatus(); }
absl::Status Process(CalculatorContext* cc) override { if (cc->Inputs().Tag("LANDMARKS").IsEmpty()) { return absl::OkStatus(); }
const auto& pose_landmarks = cc->Inputs().Tag("LANDMARKS").Get<std::vector<NormalizedLandmarkList>>(); if (pose_landmarks.empty()) { return absl::OkStatus(); }
const auto& landmarks = pose_landmarks[0];
float left_shoulder_x = landmarks.landmark(pose_landmark::LEFT_SHOULDER).x(); float left_shoulder_y = landmarks.landmark(pose_landmark::LEFT_SHOULDER).y(); float right_shoulder_x = landmarks.landmark(pose_landmark::RIGHT_SHOULDER).x(); float right_shoulder_y = landmarks.landmark(pose_landmark::RIGHT_SHOULDER).y(); float shoulder_center_x = (left_shoulder_x + right_shoulder_x) / 2.0f; float shoulder_center_y = (left_shoulder_y + right_shoulder_y) / 2.0f; float left_hip_x = landmarks.landmark(pose_landmark::LEFT_HIP).x(); float left_hip_y = landmarks.landmark(pose_landmark::LEFT_HIP).y(); float right_hip_x = landmarks.landmark(pose_landmark::RIGHT_HIP).x(); float right_hip_y = landmarks.landmark(pose_landmark::RIGHT_HIP).y(); float hip_center_x = (left_hip_x + right_hip_x) / 2.0f; float hip_center_y = (left_hip_y + right_hip_y) / 2.0f; float shoulder_width = std::abs(right_shoulder_x - left_shoulder_x); float hip_width = std::abs(right_hip_x - left_hip_x); float body_offset = shoulder_center_x - hip_center_x; float forward_lean = shoulder_center_y - hip_center_y; BodyOrientation orientation = BodyOrientation::FORWARD; float left_turn_angle = 0.0f; float right_turn_angle = 0.0f; float forward_lean_angle = 0.0f; if (body_offset < -0.05f) { orientation = BodyOrientation::TURNING_LEFT; left_turn_angle = std::abs(body_offset) * 180.0f; } else if (body_offset > 0.05f) { orientation = BodyOrientation::TURNING_RIGHT; right_turn_angle = body_offset * 180.0f; } if (forward_lean > 0.1f) { orientation = BodyOrientation::LEANING_FORWARD; forward_lean_angle = forward_lean * 90.0f; } else if (forward_lean < -0.05f) { orientation = BodyOrientation::LEANING_BACK; forward_lean_angle = std::abs(forward_lean) * 90.0f; } BodyOrientationResult result; result.set_orientation(orientation); result.set_left_turn_angle(left_turn_angle); result.set_right_turn_angle(right_turn_angle); result.set_forward_lean_angle(forward_lean_angle); result.set_confidence(1.0f); result.set_timestamp_ms(cc->InputTimestamp().Value() / 1000);
cc->Outputs().Tag("ORIENTATION").AddPacket( MakePacket<BodyOrientationResult>(result).At(cc->InputTimestamp()));
VLOG(1) << "Body orientation: " << static_cast<int>(orientation);
return absl::OkStatus(); } };
REGISTER_CALCULATOR(BodyOrientationCalculator);
}
#endif
|
32.2 姿态评分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
absl::Status PostureEvaluationCalculator::Process(CalculatorContext* cc) override { const auto& landmarks = cc->Inputs().Tag("LANDMARKS").Get<std::vector<NormalizedLandmarkList>>()[0];
float nose_y = landmarks.landmark(pose_landmark::NOSE).y(); float left_shoulder_y = landmarks.landmark(pose_landmark::LEFT_SHOULDER).y(); float right_shoulder_y = landmarks.landmark(pose_landmark::RIGHT_SHOULDER).y(); float shoulder_center_y = (left_shoulder_y + right_shoulder_y) / 2.0f; float left_hip_y = landmarks.landmark(pose_landmark::LEFT_HIP).y(); float right_hip_y = landmarks.landmark(pose_landmark::RIGHT_HIP).y(); float hip_center_y = (left_hip_y + right_hip_y) / 2.0f; float head_position = nose_y - shoulder_center_y; float shoulder_asymmetry = std::abs(left_shoulder_y - right_shoulder_y); float forward_lean = shoulder_center_y - hip_center_y; float score = 100.0f; if (head_position < -0.05f) { score -= 20.0f; } else if (head_position > 0.15f) { score -= 15.0f; } if (shoulder_asymmetry > 0.03f) { score -= shoulder_asymmetry * 300.0f; } if (forward_lean > 0.05f) { score -= forward_lean * 200.0f; } score = std::max(0.0f, std::min(100.0f, score)); PostureEvaluationResult result; result.set_score(score); result.set_head_position(head_position); result.set_shoulder_asymmetry(shoulder_asymmetry); result.set_forward_lean(forward_lean); cc->Outputs().Tag("EVALUATION").AddPacket( MakePacket<PostureEvaluationResult>(result).At(cc->InputTimestamp()));
return absl::OkStatus(); }
|
三十三、IMS 实战:OOP 检测
33.1 OOP (Out-of-Position) 检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ┌─────────────────────────────────────────────────────────────────────────┐ │ OOP 检测原理 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ OOP (Out-of-Position) 定义: │ │ 乘员位置异常,可能导致安全气囊部署时造成伤害。 │ │ │ │ 常见 OOP 情况: │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 1. 身体前倾过度(距离仪表盘/方向盘太近) │ │ │ │ 2. 身体侧倾(靠在车门或中控台上) │ │ │ │ 3. 腿部异常位置(脚放在仪表盘上) │ │ │ │ 4. 儿童坐姿异常(跪在座椅上) │ │ │ │ 5. 头部位置异常(靠近侧窗或挡风玻璃) │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ OOP 检测目标: │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ • 调整气囊部署策略(延迟/禁用/降级) │ │ │ │ • 发出警告提示乘员调整位置 │ │ │ │ • 记录乘员状态用于事故分析 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
33.2 OOP Detection Graph
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| # ims_oop_detection_graph.pbtxt
input_stream: "OMS_IMAGE:oms_image" output_stream: "OOP_RESULT:oop_result" output_stream: "ALERT:alert"
# ========== 1. Pose Detection ========== node { calculator: "PoseTrackingGpu" input_stream: "IMAGE:oms_image" output_stream: "LANDMARKS:pose_landmarks" output_stream: "SEGMENTATION:segmentation_mask" options { [mediapipe.PoseTrackingOptions.ext] { model_complexity: 0 # 0: Lite, 1: Full, 2: Heavy enable_segmentation: false smooth_landmarks: true } } }
# ========== 2. 身体朝向分析 ========== node { calculator: "BodyOrientationCalculator" input_stream: "LANDMARKS:pose_landmarks" output_stream: "ORIENTATION:orientation" }
# ========== 3. 姿态评分 ========== node { calculator: "PostureEvaluationCalculator" input_stream: "LANDMARKS:pose_landmarks" output_stream: "EVALUATION:evaluation" }
# ========== 4. OOP 检测 ========== node { calculator: "OOPDetectionCalculator" input_stream: "LANDMARKS:pose_landmarks" input_stream: "ORIENTATION:orientation" input_stream: "EVALUATION:evaluation" output_stream: "OOP_RESULT:oop_result" output_stream: "ALERT:alert" options { [mediapipe.OOPDetectionOptions.ext] { forward_lean_threshold: 0.15 side_lean_threshold: 0.10 head_deviation_threshold: 0.08 } } }
|
33.3 OOP Detection Calculator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
| #ifndef MEDIAPIPE_CALCULATORS_IMS_OOP_DETECTION_CALCULATOR_H_ #define MEDIAPIPE_CALCULATORS_IMS_OOP_DETECTION_CALCULATOR_H_
#include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/landmark.pb.h"
namespace mediapipe {
enum class OOPType { NONE = 0, FORWARD_LEAN = 1, SIDE_LEAN = 2, HEAD_DEVIATION = 3, LEG_ABNORMAL = 4, UNKNOWN = 5, };
message OOPResult { OOPType oop_type = 1; float severity = 2; float forward_lean = 3; float side_lean = 4; float head_deviation = 5; bool alert_required = 6; uint64 timestamp_ms = 7; }
class OOPDetectionCalculator : public CalculatorBase { public: static absl::Status GetContract(CalculatorContract* cc) { cc->Inputs().Tag("LANDMARKS").Set<std::vector<NormalizedLandmarkList>>(); cc->Inputs().Tag("ORIENTATION").Set<BodyOrientationResult>(); cc->Inputs().Tag("EVALUATION").Set<PostureEvaluationResult>(); cc->Outputs().Tag("OOP_RESULT").Set<OOPResult>(); cc->Outputs().Tag("ALERT").Set<bool>(); cc->Options<OOPDetectionOptions>(); return absl::OkStatus(); }
absl::Status Open(CalculatorContext* cc) override { const auto& options = cc->Options<OOPDetectionOptions>(); forward_lean_threshold_ = options.forward_lean_threshold(); side_lean_threshold_ = options.side_lean_threshold(); head_deviation_threshold_ = options.head_deviation_threshold(); return absl::OkStatus(); }
absl::Status Process(CalculatorContext* cc) override { if (cc->Inputs().Tag("LANDMARKS").IsEmpty()) { return absl::OkStatus(); }
const auto& pose_landmarks = cc->Inputs().Tag("LANDMARKS").Get<std::vector<NormalizedLandmarkList>>(); if (pose_landmarks.empty()) { return absl::OkStatus(); }
const auto& landmarks = pose_landmarks[0];
float shoulder_center_y = (landmarks.landmark(pose_landmark::LEFT_SHOULDER).y() + landmarks.landmark(pose_landmark::RIGHT_SHOULDER).y()) / 2.0f; float hip_center_y = (landmarks.landmark(pose_landmark::LEFT_HIP).y() + landmarks.landmark(pose_landmark::RIGHT_HIP).y()) / 2.0f; float forward_lean = shoulder_center_y - hip_center_y; float shoulder_center_x = (landmarks.landmark(pose_landmark::LEFT_SHOULDER).x() + landmarks.landmark(pose_landmark::RIGHT_SHOULDER).x()) / 2.0f; float hip_center_x = (landmarks.landmark(pose_landmark::LEFT_HIP).x() + landmarks.landmark(pose_landmark::RIGHT_HIP).x()) / 2.0f; float side_lean = std::abs(shoulder_center_x - hip_center_x); float nose_x = landmarks.landmark(pose_landmark::NOSE).x(); float head_deviation = std::abs(nose_x - shoulder_center_x);
OOPType oop_type = OOPType::NONE; float severity = 0.0f; if (forward_lean > forward_lean_threshold_) { oop_type = OOPType::FORWARD_LEAN; severity = (forward_lean - forward_lean_threshold_) / (1.0f - forward_lean_threshold_); } if (side_lean > side_lean_threshold_) { if (severity < (side_lean - side_lean_threshold_) / (1.0f - side_lean_threshold_)) { oop_type = OOPType::SIDE_LEAN; severity = (side_lean - side_lean_threshold_) / (1.0f - side_lean_threshold_); } } if (head_deviation > head_deviation_threshold_) { if (severity < (head_deviation - head_deviation_threshold_) / (1.0f - head_deviation_threshold_)) { oop_type = OOPType::HEAD_DEVIATION; severity = (head_deviation - head_deviation_threshold_) / (1.0f - head_deviation_threshold_); } }
bool alert_required = severity > 0.5f; OOPResult result; result.set_oop_type(oop_type); result.set_severity(std::min(1.0f, severity)); result.set_forward_lean(forward_lean); result.set_side_lean(side_lean); result.set_head_deviation(head_deviation); result.set_alert_required(alert_required); result.set_timestamp_ms(cc->InputTimestamp().Value() / 1000);
cc->Outputs().Tag("OOP_RESULT").AddPacket( MakePacket<OOPResult>(result).At(cc->InputTimestamp())); cc->Outputs().Tag("ALERT").AddPacket( MakePacket<bool>(alert_required).At(cc->InputTimestamp()));
if (oop_type != OOPType::NONE) { LOG(INFO) << "OOP detected: type=" << static_cast<int>(oop_type) << ", severity=" << severity; }
return absl::OkStatus(); }
private: float forward_lean_threshold_ = 0.15f; float side_lean_threshold_ = 0.10f; float head_deviation_threshold_ = 0.08f; };
REGISTER_CALCULATOR(OOPDetectionCalculator);
}
#endif
|
三十四、总结
| 要点 |
说明 |
| 关键点数 |
33 个 3D 点 |
| 身体区域 |
面部、上肢、下肢、脚部 |
| 人体分割 |
可选背景去除 |
| 姿态分析 |
身体朝向、前倾、侧倾 |
| IMS 应用 |
OOP 检测、乘员分类、安全带检测 |
下篇预告
MediaPipe 系列 30:Object Detection——通用目标检测
深入讲解通用目标检测、SSD 架构、IMS 车内物品检测应用。
参考资料
- Google AI Edge. Pose Detection
- MediaPipe. BlazePose Paper
- V. Bazarevsky et al. “BlazePose: On-device Real-time Body Pose tracking”
系列进度: 29/55
更新时间: 2026-03-12