MediaPipe 系列 28:Hand Tracking——手部检测与追踪完整指南

前言:为什么需要手部追踪?

28.1 Hand Tracking 的重要性

手部追踪在车内场景的应用:

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
┌─────────────────────────────────────────────────────────────────────────┐
│ Hand Tracking 在 IMS 中的应用 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 车内场景手部检测需求: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ • 驾驶员手势控制(手势交互) │ │
│ │ • 手部位置检测(方向盘握持判断) │ │
│ │ • 打电话检测(分心行为识别) │ │
│ │ • 抽烟检测(危险行为识别) │ │
│ │ • 手势验证(身份认证辅助) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ MediaPipe Hand Tracking 特点: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ • 21 个 3D 关键点 │ │
│ │ • 单目摄像头即可工作 │ │
│ │ • 实时性能(~2ms GPU) │ │
│ │ • 轻量级模型(~3MB) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

28.2 功能特点

特性 说明
关键点数量 21 个 3D 点
检测方式 Palm Detection + Hand Landmark
左右手识别 支持(handedness)
速度 ~2ms (GPU), ~8ms (CPU)
模型大小 ~3MB (Palm + Landmark)

28.3 两阶段架构

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
┌─────────────────────────────────────────────────────────────────────────┐
│ Hand Tracking 两阶段架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 第一阶段:手掌检测(Palm Detection) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 输入:256×256 RGB 图像 │ │
│ │ 模型:BlazePalm(轻量级手掌检测器) │ │
│ │ 输出:手掌边界框 + 7 个关键点 │ │
│ │ 速度:~1ms (GPU) │ │
│ │ │ │
│ │ 为什么检测手掌而不是手? │ │
│ │ • 手掌面积大,更容易检测 │ │
│ │ • 手掌形状规则,特征明显 │ │
│ │ • 手掌边界框更稳定 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第二阶段:关键点预测(Hand Landmark) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 输入:256×256 裁剪的手部区域 │ │
│ │ 模型:Hand Landmark Model │ │
│ │ 输出:213D 关键点 + 左右手标签 │ │
│ │ 速度:~1ms (GPU) │ │
│ │ │ │
│ │ 关键点预测: │ │
│ │ • 坐标归一化到 [0, 1] │ │
│ │ • Z 值表示相对深度 │ │
│ │ • 左右手概率(handedness) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第三阶段:追踪(下一帧 ROI 预测) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 输入:上一帧关键点 │ │
│ │ 方法:基于关键点预测下一帧手部位置 │ │
│ │ 优势:跳过检测阶段,直接追踪 │ │
│ │ │ │
│ │ 追踪失败时:重新检测 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

二十九、关键点布局

29.1 21 个关键点详解

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
┌─────────────────────────────────────────────────────────────────────────┐
│ Hand Landmarks (21 points) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
4 8 12 16 20
│ │ │ │ │ │ │
│ ┌─●───●───●───●───●─┐ 指尖 (TIP) │
│ │ │ │ │ │ │ │ │
│ │ 3 7 11 15 19 │ 远端指节 (DIP) │
│ │ │ │ │ │ │ │ │
│ │ 2 6 10 14 18 │ 中间指节 (PIP) │
│ │ │ │ │ │ │ │ │
│ │ 1 5 9 13 17 │ 近端指节 (MCP) │
│ │ │ │ │ │ │ │ │
│ └─●───●───●───●───●─┘ │
│ │ ╲ │ ╱ │ │
│ │ ╲ │ ╱ │ │
│ │ ╲ │ ╱ │ │
│ │ ●0 │ 手腕 (WRIST) │
│ │ │ │
│ └─────────────────┘ │
│ │
│ 关键点索引: │
│ ───────────────────────────────────────────────────────────────── │
│ 索引 名称 描述 │
│ ───────────────────────────────────────────────────────────────── │
0 WRIST 手腕 │
1 THUMB_CMC 大拇指腕掌关节 │
2 THUMB_MCP 大拇指掌指关节 │
3 THUMB_IP 大拇指指间关节 │
4 THUMB_TIP 大拇指指尖 │
5 INDEX_MCP 食指掌指关节 │
6 INDEX_PIP 食指近端指节 │
7 INDEX_DIP 食指远端指节 │
8 INDEX_TIP 食指指尖 │
9 MIDDLE_MCP 中指掌指关节 │
10 MIDDLE_PIP 中指近端指节 │
11 MIDDLE_DIP 中指远端指节 │
12 MIDDLE_TIP 中指指尖 │
13 RING_MCP 无名指掌指关节 │
14 RING_PIP 无名指近端指节 │
15 RING_DIP 无名指远端指节 │
16 RING_TIP 无名指指尖 │
17 PINKY_MCP 小指掌指关节 │
18 PINKY_PIP 小指近端指节 │
19 PINKY_DIP 小指远端指节 │
20 PINKY_TIP 小指指尖 │
│ ───────────────────────────────────────────────────────────────── │
│ │
└─────────────────────────────────────────────────────────────────────────┘

29.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
// ========== 手部关键点索引 ==========
// mediapipe/modules/hand_landmark/hand_landmark.h

namespace hand_landmark {
// 基础索引
constexpr int WRIST = 0;

// 大拇指 (Thumb)
constexpr int THUMB_CMC = 1; // 腕掌关节
constexpr int THUMB_MCP = 2; // 掌指关节
constexpr int THUMB_IP = 3; // 指间关节
constexpr int THUMB_TIP = 4; // 指尖

// 食指 (Index)
constexpr int INDEX_MCP = 5;
constexpr int INDEX_PIP = 6;
constexpr int INDEX_DIP = 7;
constexpr int INDEX_TIP = 8;

// 中指 (Middle)
constexpr int MIDDLE_MCP = 9;
constexpr int MIDDLE_PIP = 10;
constexpr int MIDDLE_DIP = 11;
constexpr int MIDDLE_TIP = 12;

// 无名指 (Ring)
constexpr int RING_MCP = 13;
constexpr int RING_PIP = 14;
constexpr int RING_DIP = 15;
constexpr int RING_TIP = 16;

// 小指 (Pinky)
constexpr int PINKY_MCP = 17;
constexpr int PINKY_PIP = 18;
constexpr int PINKY_DIP = 19;
constexpr int PINKY_TIP = 20;

// 指尖索引数组
constexpr int FINGERTIPS[] = {THUMB_TIP, INDEX_TIP, MIDDLE_TIP, RING_TIP, PINKY_TIP};

// PIP 索引数组(用于判断手指伸展)
constexpr int FINGER_PIPS[] = {THUMB_IP, INDEX_PIP, MIDDLE_PIP, RING_PIP, PINKY_PIP};

// MCP 索引数组
constexpr int FINGER_MCPS[] = {THUMB_MCP, INDEX_MCP, MIDDLE_MCP, RING_MCP, PINKY_MCP};
}

三十、Graph 配置

30.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
# ========== Hand Tracking Graph 配置 ==========

# mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt

input_stream: "INPUT:Input"

output_stream: "LANDMARKS:multi_hand_landmarks"
output_stream: "HANDEDNESS:multi_handedness"
output_stream: "HAND_RECTS:hand_rects"

# ========== 1. 图像格式转换 ==========
node {
calculator: "ImageTransformationCalculator"
input_stream: "INPUT:Input"
output_stream: "IMAGE:converted_image"
options {
[mediapipe.ImageTransformationCalculatorOptions.ext] {
output_format: SRGB
}
}
}

# ========== 2. 手掌检测 ==========
node {
calculator: "PalmDetectionCalculator"
input_stream: "IMAGE:converted_image"
output_stream: "DETECTIONS:detections"
options {
[mediapipe.PalmDetectionCalculatorOptions.ext] {
model_path: "/models/palm_detection.tflite"
score_threshold: 0.5
max_results: 2
}
}
}

# ========== 3. 手部关键点检测 ==========
node {
calculator: "HandLandmarkCalculator"
input_stream: "IMAGE:converted_image"
input_stream: "DETECTIONS:detections"
output_stream: "LANDMARKS:multi_hand_landmarks"
output_stream: "HANDEDNESS:multi_handedness"
output_stream: "ROIS:hand_rects"
options {
[mediapipe.HandLandmarkCalculatorOptions.ext] {
model_path: "/models/hand_landmark.tflite"
}
}
}

# ========== 4. ROI 追踪(下一帧) ==========
node {
calculator: "RectTransformationCalculator"
input_stream: "NORM_RECTS:hand_rects"
output_stream: "NORM_RECTS:tracking_rects"
options {
[mediapipe.RectTransformationCalculatorOptions.ext] {
scale_x: 1.5
scale_y: 1.5
}
}
}

30.2 Palm 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
// palm_detection_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_TFLITE_PALM_DETECTION_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_TFLITE_PALM_DETECTION_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"

namespace mediapipe {

// ========== Palm Detection Calculator ==========
class PalmDetectionCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMAGE").Set<ImageFrame>();
cc->Outputs().Tag("DETECTIONS").Set<std::vector<Detection>>();

cc->Options<PalmDetectionOptions>();
return absl::OkStatus();
}

absl::Status Open(CalculatorContext* cc) override {
const auto& options = cc->Options<PalmDetectionOptions>();

// 加载模型
model_ = LoadTFLiteModel(options.model_path());
interpreter_ = CreateInterpreter(model_);

score_threshold_ = options.score_threshold();
max_results_ = options.max_results();

return absl::OkStatus();
}

absl::Status Process(CalculatorContext* cc) override {
const auto& image = cc->Inputs().Tag("IMAGE").Get<ImageFrame>();

// ========== 1. 预处理 ==========
cv::Mat input_mat = ImageFrameToMat(image);
cv::Mat resized;
cv::resize(input_mat, resized, cv::Size(256, 256));

// 归一化到 [0, 1]
resized.convertTo(resized, CV_32F, 1.0 / 255.0);

// ========== 2. 推理 ==========
CopyToInputTensor(resized, interpreter_->input_tensor(0));
interpreter_->Invoke();

// ========== 3. 后处理 ==========
auto detections = ParseDetections(
interpreter_->output_tensor(0), // boxes
interpreter_->output_tensor(1), // scores
interpreter_->output_tensor(2)); // keypoints

// 过滤低分数
detections.erase(
std::remove_if(detections.begin(), detections.end(),
[this](const Detection& d) { return d.score() < score_threshold_; }),
detections.end());

// NMS
detections = NonMaxSuppression(detections, 0.3);

// 限制数量
if (detections.size() > max_results_) {
detections.resize(max_results_);
}

cc->Outputs().Tag("DETECTIONS").AddPacket(
MakePacket<std::vector<Detection>>(detections).At(cc->InputTimestamp()));

return absl::OkStatus();
}

private:
std::unique_ptr<tflite::FlatBufferModel> model_;
std::unique_ptr<tflite::Interpreter> interpreter_;
float score_threshold_ = 0.5f;
int max_results_ = 2;

std::vector<Detection> ParseDetections(
TfLiteTensor* boxes, TfLiteTensor* scores, TfLiteTensor* keypoints);
};

REGISTER_CALCULATOR(PalmDetectionCalculator);

} // namespace mediapipe

#endif

三十一、手势识别

31.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
┌─────────────────────────────────────────────────────────────────────────┐
│ 手指伸展判断原理 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 判断方法:比较指尖与 PIP 关节的 Y 坐标 │
│ │
│ 手指伸展时: │
│ ┌─────────────────────────────────────────────┐ │
│ │ │ │
│ │ TIP ●───── 指尖(Y 值较小) │ │
│ │ │ │ │
│ │ DIP ● │ │
│ │ │ │ │
│ │ PIP ●───── 中间关节(Y 值较大) │ │
│ │ │ │ │
│ │ MCP ● │ │
│ │ │ │
│ │ 条件:TIP.y < PIP.y → 手指伸展 │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 手指弯曲时: │
│ ┌─────────────────────────────────────────────┐ │
│ │ │ │
│ │ MCP ● │ │
│ │ │ │ │
│ │ PIP ●───── 中间关节 │ │
│ │ ╱ │ │
│ │ DIP ● │ │
│ │ ╲ │ │
│ │ TIP ●─── 指尖(Y 值较大,弯曲) │ │
│ │ │ │
│ │ 条件:TIP.y > PIP.y → 手指弯曲 │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 大拇指特殊处理:比较 X 坐标(考虑左右手) │
│ │
└─────────────────────────────────────────────────────────────────────────┘

31.2 Gesture Recognition 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// gesture_recognition_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMS_GESTURE_RECOGNITION_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMS_GESTURE_RECOGNITION_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/landmark.pb.h"

namespace mediapipe {

// ========== 手势枚举 ==========
enum class Gesture {
UNKNOWN = 0,
FIST = 1, // 握拳
OPEN = 2, // 手掌张开
THUMBS_UP = 3, // 竖大拇指(赞)
THUMBS_DOWN = 4, // 大拇指朝下(踩)
POINTING = 5, // 指向(食指伸出)
PEACE = 6, // 和平手势(V字)
OK = 7, // OK 手势
CALL = 8, // 打电话手势
ROCK = 9, // 摇滚手势
};

// ========== 手势消息 ==========
message GestureResult {
Gesture gesture = 1;
float confidence = 2;
bool is_left_hand = 3;
uint64 timestamp_ms = 4;
}

// ========== Gesture Recognition Calculator ==========
class GestureRecognitionCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("LANDMARKS").Set<std::vector<NormalizedLandmarkList>>();
cc->Inputs().Tag("HANDEDNESS").Set<std::vector<ClassificationList>>();
cc->Outputs().Tag("GESTURE").Set<GestureResult>();

return absl::OkStatus();
}

absl::Status Process(CalculatorContext* cc) override {
if (cc->Inputs().Tag("LANDMARKS").IsEmpty()) {
return absl::OkStatus();
}

const auto& hand_landmarks =
cc->Inputs().Tag("LANDMARKS").Get<std::vector<NormalizedLandmarkList>>();

if (hand_landmarks.empty()) {
return absl::OkStatus();
}

const auto& landmarks = hand_landmarks[0];

// ========== 判断左右手 ==========
bool is_left_hand = false;
if (!cc->Inputs().Tag("HANDEDNESS").IsEmpty()) {
const auto& handedness =
cc->Inputs().Tag("HANDEDNESS").Get<std::vector<ClassificationList>>();
if (!handedness.empty() && !handedness[0].classification().empty()) {
is_left_hand = handedness[0].classification(0).label() == "Left";
}
}

// ========== 判断手指伸展状态 ==========
std::array<bool, 5> fingers_extended;

// 大拇指
fingers_extended[0] = IsThumbExtended(landmarks, is_left_hand);

// 其他四指
fingers_extended[1] = IsFingerExtended(landmarks,
hand_landmark::INDEX_TIP, hand_landmark::INDEX_PIP);
fingers_extended[2] = IsFingerExtended(landmarks,
hand_landmark::MIDDLE_TIP, hand_landmark::MIDDLE_PIP);
fingers_extended[3] = IsFingerExtended(landmarks,
hand_landmark::RING_TIP, hand_landmark::RING_PIP);
fingers_extended[4] = IsFingerExtended(landmarks,
hand_landmark::PINKY_TIP, hand_landmark::PINKY_PIP);

// ========== 识别手势 ==========
Gesture gesture = RecognizeGesture(fingers_extended, landmarks);

// ========== 输出 ==========
GestureResult result;
result.set_gesture(gesture);
result.set_confidence(1.0f);
result.set_is_left_hand(is_left_hand);
result.set_timestamp_ms(cc->InputTimestamp().Value() / 1000);

cc->Outputs().Tag("GESTURE").AddPacket(
MakePacket<GestureResult>(result).At(cc->InputTimestamp()));

VLOG(1) << "Gesture: " << static_cast<int>(gesture)
<< ", left_hand: " << is_left_hand;

return absl::OkStatus();
}

private:
bool IsThumbExtended(const NormalizedLandmarkList& landmarks, bool is_left_hand) {
// 大拇指伸展判断需要考虑左右手
float thumb_tip_x = landmarks.landmark(hand_landmark::THUMB_TIP).x();
float thumb_ip_x = landmarks.landmark(hand_landmark::THUMB_IP).x();

if (is_left_hand) {
// 左手:拇指指尖在 IP 关节的左侧
return thumb_tip_x < thumb_ip_x;
} else {
// 右手:拇指指尖在 IP 关节的右侧
return thumb_tip_x > thumb_ip_x;
}
}

bool IsFingerExtended(const NormalizedLandmarkList& landmarks,
int tip_idx, int pip_idx) {
// 指尖 Y 坐标小于 PIP 关节 Y 坐标表示伸展
return landmarks.landmark(tip_idx).y() < landmarks.landmark(pip_idx).y();
}

Gesture RecognizeGesture(const std::array<bool, 5>& fingers_extended,
const NormalizedLandmarkList& landmarks) {
int extended_count = std::count(fingers_extended.begin(), fingers_extended.end(), true);

// 握拳:所有手指弯曲
if (extended_count == 0) {
return Gesture::FIST;
}

// 手掌张开:所有手指伸展
if (extended_count == 5) {
return Gesture::OPEN;
}

// 竖大拇指:只有大拇指伸展
if (extended_count == 1 && fingers_extended[0]) {
// 判断大拇指朝上还是朝下
float thumb_tip_y = landmarks.landmark(hand_landmark::THUMB_TIP).y();
float wrist_y = landmarks.landmark(hand_landmark::WRIST).y();

if (thumb_tip_y < wrist_y) {
return Gesture::THUMBS_UP;
} else {
return Gesture::THUMBS_DOWN;
}
}

// 指向:只有食指伸展
if (extended_count == 1 && fingers_extended[1]) {
return Gesture::POINTING;
}

// 和平手势:食指和中指伸展
if (extended_count == 2 && fingers_extended[1] && fingers_extended[2]) {
return Gesture::PEACE;
}

// OK 手势:大拇指和食指接触
float thumb_tip_x = landmarks.landmark(hand_landmark::THUMB_TIP).x();
float thumb_tip_y = landmarks.landmark(hand_landmark::THUMB_TIP).y();
float index_tip_x = landmarks.landmark(hand_landmark::INDEX_TIP).x();
float index_tip_y = landmarks.landmark(hand_landmark::INDEX_TIP).y();

float distance = std::sqrt(std::pow(thumb_tip_x - index_tip_x, 2) +
std::pow(thumb_tip_y - index_tip_y, 2));

if (distance < 0.05f && !fingers_extended[2] && !fingers_extended[3] && !fingers_extended[4]) {
return Gesture::OK;
}

// 打电话:大拇指和小指伸展
if (extended_count == 2 && fingers_extended[0] && fingers_extended[4]) {
return Gesture::CALL;
}

// 摇滚手势:食指和小指伸展
if (extended_count == 2 && fingers_extended[1] && fingers_extended[4]) {
return Gesture::ROCK;
}

return Gesture::UNKNOWN;
}
};

REGISTER_CALCULATOR(GestureRecognitionCalculator);

} // namespace mediapipe

#endif

三十二、IMS 实战:驾驶员行为检测

32.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
# ims_driver_behavior_detection_graph.pbtxt

input_stream: "RGB_IMAGE:rgb_image"
output_stream: "BEHAVIOR_RESULT:behavior_result"
output_stream: "ALERT:alert"

# ========== 1. Hand Tracking ==========
node {
calculator: "HandTrackingGpu"
input_stream: "IMAGE:rgb_image"
output_stream: "LANDMARKS:multi_hand_landmarks"
output_stream: "HANDEDNESS:multi_handedness"
options {
[mediapipe.HandTrackingOptions.ext] {
max_num_hands: 2
min_detection_confidence: 0.5
min_tracking_confidence: 0.5
}
}
}

# ========== 2. 手势识别 ==========
node {
calculator: "GestureRecognitionCalculator"
input_stream: "LANDMARKS:multi_hand_landmarks"
input_stream: "HANDEDNESS:multi_handedness"
output_stream: "GESTURE:gesture_result"
}

# ========== 3. 打电话检测 ==========
node {
calculator: "PhoneCallDetectorCalculator"
input_stream: "LANDMARKS:multi_hand_landmarks"
input_stream: "GESTURE:gesture_result"
output_stream: "PHONE_CALL:phone_call_detected"
}

# ========== 4. 抽烟检测 ==========
node {
calculator: "SmokingDetectorCalculator"
input_stream: "LANDMARKS:multi_hand_landmarks"
input_stream: "GESTURE:gesture_result"
output_stream: "SMOKING:smoking_detected"
}

# ========== 5. 方向盘握持检测 ==========
node {
calculator: "SteeringWheelHoldDetectorCalculator"
input_stream: "LANDMARKS:multi_hand_landmarks"
output_stream: "STEERING_HOLD:steering_hold_result"
}

# ========== 6. 行为综合判断 ==========
node {
calculator: "DriverBehaviorDecisionCalculator"
input_stream: "GESTURE:gesture_result"
input_stream: "PHONE_CALL:phone_call_detected"
input_stream: "SMOKING:smoking_detected"
input_stream: "STEERING_HOLD:steering_hold_result"
output_stream: "BEHAVIOR_RESULT:behavior_result"
output_stream: "ALERT:alert"
}

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
// phone_call_detector_calculator.cc

absl::Status PhoneCallDetectorCalculator::Process(CalculatorContext* cc) override {
if (cc->Inputs().Tag("LANDMARKS").IsEmpty()) {
return absl::OkStatus();
}

const auto& hand_landmarks =
cc->Inputs().Tag("LANDMARKS").Get<std::vector<NormalizedLandmarkList>>();

bool phone_call_detected = false;

for (const auto& landmarks : hand_landmarks) {
// ========== 检测打电话手势 ==========
// 条件1:手在耳朵附近
float wrist_y = landmarks.landmark(hand_landmark::WRIST).y();
float wrist_x = landmarks.landmark(hand_landmark::WRIST).x();

// 耳朵区域假设:x 在 [0.3, 0.7],y 在 [0.1, 0.4]
bool near_ear = (wrist_x > 0.3f && wrist_x < 0.7f &&
wrist_y > 0.1f && wrist_y < 0.4f);

// 条件2:大拇指和小指伸出(打电话手势)
float thumb_tip_y = landmarks.landmark(hand_landmark::THUMB_TIP).y();
float thumb_ip_y = landmarks.landmark(hand_landmark::THUMB_IP).y();
float pinky_tip_y = landmarks.landmark(hand_landmark::PINKY_TIP).y();
float pinky_pip_y = landmarks.landmark(hand_landmark::PINKY_PIP).y();

bool thumb_extended = thumb_tip_y < thumb_ip_y;
bool pinky_extended = pinky_tip_y < pinky_pip_y;

// 条件3:其他手指弯曲
float index_tip_y = landmarks.landmark(hand_landmark::INDEX_TIP).y();
float index_pip_y = landmarks.landmark(hand_landmark::INDEX_PIP).y();
float middle_tip_y = landmarks.landmark(hand_landmark::MIDDLE_TIP).y();
float middle_pip_y = landmarks.landmark(hand_landmark::MIDDLE_PIP).y();
float ring_tip_y = landmarks.landmark(hand_landmark::RING_TIP).y();
float ring_pip_y = landmarks.landmark(hand_landmark::RING_PIP).y();

bool index_bent = index_tip_y > index_pip_y;
bool middle_bent = middle_tip_y > middle_pip_y;
bool ring_bent = ring_tip_y > ring_pip_y;

// 综合判断
if (near_ear && thumb_extended && pinky_extended &&
index_bent && middle_bent && ring_bent) {
phone_call_detected = true;
break;
}
}

cc->Outputs().Tag("PHONE_CALL").AddPacket(
MakePacket<bool>(phone_call_detected).At(cc->InputTimestamp()));

return absl::OkStatus();
}

三十三、总结

要点 说明
关键点数 21 个 3D 点
检测方式 Palm Detection + Hand Landmark
追踪 ROI 预测,失败时重新检测
手势识别 基于手指伸展状态判断
IMS 应用 打电话检测、抽烟检测、手势控制

下篇预告

MediaPipe 系列 29:Pose Detection——人体姿态检测

深入讲解人体姿态关键点检测、动作识别、IMS 驾驶员姿态监控应用。


参考资料

  1. Google AI Edge. Hand Tracking
  2. MediaPipe. Hand Tracking Paper
  3. F. Zhang et al. “MediaPipe Hands: On-device Real-time Hand Tracking”

系列进度: 28/55
更新时间: 2026-03-12


MediaPipe 系列 28:Hand Tracking——手部检测与追踪完整指南
https://dapalm.com/2026/03/13/MediaPipe系列28-Hand-Tracking:手部检测与追踪/
作者
Mars
发布于
2026年3月13日
许可协议