MediaPipe 系列 09:Calculator Options——参数化配置完整指南

前言:为什么需要参数化配置?

9.1 Options 的核心价值

参数化配置是 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
┌─────────────────────────────────────────────────────────────────────────┐
│ Options 的核心价值 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 问题:如何让 Calculator 适应不同场景? │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 场景对比: │ │
│ │ │ │
│ │ 无 Options: │ │
│ │ ───────────────────────────────────────────────────── │ │
│ │ • 修改阈值 → 修改代码重新编译 │ │
│ │ • 不同场景 → 多个 Calculator 类 │ │
│ │ • 运行时调整 → 不可能 │ │
│ │ • 配置管理 → 困难 │ │
│ │ │ │
│ │ 有 Options: │ │
│ │ ───────────────────────────────────────────────────── │ │
│ │ • 修改阈值 → 修改配置文件 │ │
│ │ • 不同场景 → 一个 Calculator 多配置 │ │
│ │ • 运行时调整 → 动态覆盖 │ │
│ │ • 配置管理 → 集中管理 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ IMS DMS 实际案例: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 同一个 FatigueScoreCalculator: │ │
│ │ │ │
│ │ 配置 A(白天): │ │
│ │ perclos_threshold: 0.15 │ │
│ │ ear_threshold: 0.2 │ │
│ │ sensitivity: HIGH │ │
│ │ │ │
│ │ 配置 B(夜间): │ │
│ │ perclos_threshold: 0.20 │ │
│ │ ear_threshold: 0.25 │ │
│ │ sensitivity: MEDIUM │ │
│ │ │ │
│ │ 无需修改代码,只需切换配置 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

9.2 Options 工作流程

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
┌─────────────────────────────────────────────────────────────┐
Options 工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
1. 定义 Proto Options
│ ┌─────────────────────────────────────────────┐ │
│ │ message MyOptions { │ │
│ │ optional float threshold = 1; │ │
│ │ } │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
2. Calculator 声明使用 Options
│ ┌─────────────────────────────────────────────┐ │
│ │ static absl::Status GetContract(...) { │ │
│ │ cc->Options<MyOptions>(); │ │
│ │ } │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
3. Graph 配置传入 Options
│ ┌─────────────────────────────────────────────┐ │
│ │ options { │ │
│ │ [mediapipe.MyOptions.ext] { │ │
│ │ threshold: 0.75 │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
4. Calculator 读取 Options
│ ┌─────────────────────────────────────────────┐ │
│ │ absl::Status Open(...) { │ │
│ │ const auto& options = cc->Options<T>(); │ │
│ │ threshold_ = options.threshold(); │ │
│ │ } │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

十、Proto 文件详解

10.1 Proto 语法基础

Protobuf 是 Options 的基础:

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
# ========== Proto 语法基础 ==========

syntax = "proto3"; # 使用 proto3 语法

package mediapipe; # 包名(影响 C++ 命名空间)

# 导入 MediaPipe 框架定义
import "mediapipe/framework/calculator_options.proto";

# ========== 消息定义 ==========
message MyCalculatorOptions {

# ========== 1. 基本类型 ==========
# 数值类型
optional float threshold = 1 [default = 0.5]; # 浮点数
optional double weight = 2 [default = 1.0]; # 双精度浮点数
optional int32 max_results = 3 [default = 10]; # 32位整数
optional int64 timestamp = 4 [default = 0]; # 64位整数
optional uint32 count = 5 [default = 0]; # 无符号32位整数
optional uint64 id = 6 [default = 0]; # 无符号64位整数
optional sint32 offset = 7 [default = 0]; # 有符号32位整数
optional fixed32 value = 8 [default = 0]; # 固定32位整数

# 布尔类型
optional bool enabled = 9 [default = true]; # 布尔值

# 字符串类型
optional string model_path = 10 [default = ""]; # 字符串
optional string name = 11 [default = "unknown"]; # 带默认值的字符串

# 字节类型
optional bytes data = 12; # 字节数组

# ========== 2. 枚举类型 ==========
enum Mode {
UNKNOWN = 0; # 第一个枚举值必须是 0
FAST = 1;
ACCURATE = 2;
BALANCED = 3;
}
optional Mode mode = 13 [default = FAST];

enum Backend {
CPU = 0;
GPU = 1;
DSP = 2;
NPU = 3;
}
optional Backend backend = 14 [default = CPU];

# ========== 3. 嵌套消息 ==========
message Size {
optional int32 width = 1;
optional int32 height = 2;
}
optional Size input_size = 15;

message Point {
optional float x = 1;
optional float y = 2;
}
optional Point center = 16;

# ========== 4. 重复字段(数组)==========
repeated string labels = 17; # 字符串数组
repeated float anchors = 18; # 浮点数数组
repeated int32 layers = 19; # 整数数组
repeated Point points = 20; # 消息数组

# ========== 5. Map 类型 ==========
map<string, float> params = 21; # 字符串到浮点数的映射
map<string, string> metadata = 22; # 字符串到字符串的映射

# ========== 6. Oneof(互斥字段)==========
oneof source {
string file_path = 23; # 文件路径
bytes data = 24; # 内存数据
string url = 25; # URL
}

# ========== 7. 扩展 CalculatorOptions ==========
# 这是 MediaPipe 特有的,用于将 Options 关联到 Calculator
extend CalculatorOptions {
optional MyCalculatorOptions ext = 1000000; # 唯一扩展 ID
}
}

10.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
┌─────────────────────────────────────────────────────────────┐
│ 字段编号规则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 编号范围:1 - 536,870,911
│ │
│ 推荐范围: │
│ • 1-15:常用字段(单字节编码) │
│ • 16-2047:一般字段(两字节编码) │
│ • 2048+:不常用字段 │
│ │
│ 保留范围: │
│ • 19000-19999Protobuf 内部保留 │
│ • 不要使用这些编号 │
│ │
│ 示例: │
│ optional float threshold = 1; # 常用字段,编号 1 │
│ optional string model_path = 16; # 一般字段,编号 16 │
│ optional int64 timestamp = 1000; # 不常用字段 │
│ │
│ 注意: │
│ • 编号一旦使用,不能修改 │
│ • 删除字段时,使用 reserved 保留编号 │
│ • 新增字段使用新编号 │
│ │
│ reserved 7, 8, 9; # 保留编号 7, 8, 9 │
│ reserved "old_field"; # 保留字段名 │
│ │
└─────────────────────────────────────────────────────────────┘

10.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
# ========== 默认值规则 ==========

message DefaultValueOptions {
# ========== 显式默认值 ==========
optional float threshold = 1 [default = 0.5]; # 默认 0.5
optional int32 count = 2 [default = 10]; # 默认 10
optional bool enabled = 3 [default = true]; # 默认 true
optional string name = 4 [default = "default"]; # 默认 "default"

# ========== 隐式默认值 ==========
optional float ratio = 5; # 默认 0.0
optional int32 max_num = 6; # 默认 0
optional bool flag = 7; # 默认 false
optional string label = 8; # 默认 ""(空字符串)

# ========== 枚举默认值 ==========
enum Mode {
UNKNOWN = 0; # 枚举默认值必须是第一个(值为 0
FAST = 1;
ACCURATE = 2;
}
optional Mode mode = 9; # 默认 UNKNOWN
}

# C++ 中获取默认值
const auto& options = cc->Options<DefaultValueOptions>();

float threshold = options.threshold(); // 如果未设置,返回 0.5
float ratio = options.ratio(); // 如果未设置,返回 0.0
bool enabled = options.enabled(); // 如果未设置,返回 true
bool flag = options.flag(); // 如果未设置,返回 false
string name = options.name(); // 如果未设置,返回 "default"
string label = options.label(); // 如果未设置,返回 ""
Mode mode = options.mode(); // 如果未设置,返回 UNKNOWN

十一、Calculator 使用 Options

11.1 Calculator 声明 Options

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
// ========== Calculator 声明使用 Options ==========

#include "mediapipe/framework/calculator_framework.h"
#include "my_calculator_options.pb.h" // 由 Proto 编译生成

namespace mediapipe {

class MyCalculator : public CalculatorBase {
public:
// ========== GetContract 中声明使用 Options ==========
static absl::Status GetContract(CalculatorContract* cc) {
// 定义输入输出
cc->Inputs().Tag("INPUT").Set<InputType>();
cc->Outputs().Tag("OUTPUT").Set<OutputType>();

// 声明使用 Options
// 这会验证 Graph 配置中的 options 是否正确
cc->Options<MyCalculatorOptions>();

return absl::OkStatus();
}

// ========== Open 中读取 Options ==========
absl::Status Open(CalculatorContext* cc) override {
// 获取 Options(引用)
const auto& options = cc->Options<MyCalculatorOptions>();

// ========== 1. 读取基本类型 ==========
threshold_ = options.threshold();
max_results_ = options.max_results();
enabled_ = options.enabled();
model_path_ = options.model_path();

// ========== 2. 读取枚举类型 ==========
mode_ = options.mode();
backend_ = options.backend();

// 枚举值判断
if (mode_ == MyCalculatorOptions::FAST) {
// 快速模式
} else if (mode_ == MyCalculatorOptions::ACCURATE) {
// 精确模式
}

// ========== 3. 读取嵌套消息 ==========
if (options.has_input_size()) {
input_width_ = options.input_size().width();
input_height_ = options.input_size().height();
} else {
// 使用默认值
input_width_ = 320;
input_height_ = 240;
}

// ========== 4. 读取重复字段(数组)==========
for (const auto& label : options.labels()) {
labels_.push_back(label);
}

for (int i = 0; i < options.anchors_size(); ++i) {
anchors_.push_back(options.anchors(i));
}

// ========== 5. 读取 Map ==========
for (const auto& pair : options.params()) {
params_[pair.first] = pair.second;
}

// ========== 6. 读取 Oneof ==========
if (options.has_file_path()) {
source_type_ = SourceType::FILE;
source_path_ = options.file_path();
} else if (options.has_data()) {
source_type_ = SourceType::DATA;
source_data_ = options.data();
} else if (options.has_url()) {
source_type_ = SourceType::URL;
source_url_ = options.url();
}

// ========== 7. 验证配置 ==========
RET_CHECK(!model_path_.empty()) << "model_path is required";
RET_CHECK(threshold_ > 0.0f && threshold_ < 1.0f)
<< "threshold must be between 0 and 1";

// ========== 8. 日志输出 ==========
LOG(INFO) << "MyCalculator configured: "
<< "threshold=" << threshold_
<< ", mode=" << mode_
<< ", backend=" << backend_;

return absl::OkStatus();
}

private:
// ========== 配置参数 ==========
float threshold_;
int max_results_;
bool enabled_;
std::string model_path_;
MyCalculatorOptions::Mode mode_;
MyCalculatorOptions::Backend backend_;
int input_width_;
int input_height_;
std::vector<std::string> labels_;
std::vector<float> anchors_;
std::map<std::string, float> params_;

enum class SourceType { FILE, DATA, URL };
SourceType source_type_;
std::string source_path_;
std::string source_data_;
std::string source_url_;
};

REGISTER_CALCULATOR(MyCalculator);

} // namespace mediapipe

11.2 Options API 详解

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
// ========== Options API 详解 ==========

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

// ========== 1. 检查字段是否设置 ==========
bool has_threshold = options.has_threshold(); // 是否显式设置
bool has_input_size = options.has_input_size(); // 嵌套消息是否存在

// ========== 2. 获取字段值 ==========
float threshold = options.threshold(); // 获取字段值
std::string name = options.name(); // 获取字符串

// ========== 3. 获取枚举值 ==========
MyCalculatorOptions::Mode mode = options.mode();
std::string mode_name = MyCalculatorOptions::Mode_Name(mode); // 枚举名

// ========== 4. 获取重复字段 ==========
int labels_size = options.labels_size(); // 数组大小
std::string label0 = options.labels(0); // 索引访问
for (const auto& label : options.labels()) { // 遍历
// ...
}

// ========== 5. 获取嵌套消息 ==========
if (options.has_input_size()) {
const auto& size = options.input_size();
int width = size.width();
int height = size.height();
}

// ========== 6. 获取 Map ==========
int params_size = options.params_size(); // Map 大小
bool has_key = options.params().contains("key"); // 是否包含键
float value = options.params().at("key"); // 通过键访问
for (const auto& [key, val] : options.params()) { // 遍历
// ...
}

// ========== 7. 获取 Oneof ==========
MyCalculatorOptions::SourceCase source_case = options.source_case();
switch (source_case) {
case MyCalculatorOptions::kFilePath:
// options.file_path() 有效
break;
case MyCalculatorOptions::kData:
// options.data() 有效
break;
case MyCalculatorOptions::kUrl:
// options.url() 有效
break;
case MyCalculatorOptions::SOURCE_NOT_SET:
// 未设置
break;
}

return absl::OkStatus();
}

十二、Graph 配置详解

12.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
# ========== Graph 配置语法 ==========

node {
calculator: "MyCalculator"
input_stream: "INPUT:input_stream"
output_stream: "OUTPUT:output_stream"

# ========== Options 配置 ==========
options {
# 使用扩展语法
[mediapipe.MyCalculatorOptions.ext] {

# ========== 基本类型 ==========
threshold: 0.75
max_results: 20
enabled: true
model_path: "/models/face.tflite"

# ========== 枚举类型 ==========
mode: ACCURATE # 枚举值(不需要引号)
backend: GPU

# ========== 嵌套消息 ==========
input_size {
width: 320
height: 240
}

# ========== 重复字段(数组)==========
labels: "face" # 方式 1:多次使用
labels: "person"
labels: "car"

anchors: [0.1, 0.2, 0.3, 0.4] # 方式 2:数组语法

# ========== Map 类型 ==========
params {
key: "learning_rate"
value: 0.001
}
params {
key: "batch_size"
value: 32.0
}

# ========== Oneof ==========
file_path: "/path/to/model.tflite" # 只能设置一个

# 或者
# data: <base64 encoded data>
# 或者
# url: "https://example.com/model.tflite"
}
}
}

12.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
63
64
65
66
67
68
69
70
71
72
73
# ========== 高精度配置 ==========
node {
calculator: "InferenceCalculator"
input_stream: "IMAGE:image"
output_stream: "DETECTIONS:detections"
options {
[mediapipe.InferenceCalculatorOptions.ext] {
model_path: "/models/face_detection_large.tflite"
num_threads: 8
input_size { width: 640 height: 480 }
score_threshold: 0.75
iou_threshold: 0.5
max_detections: 200
backend: GPU
}
}
}

# ========== 高速配置 ==========
node {
calculator: "InferenceCalculator"
input_stream: "IMAGE:image"
output_stream: "DETECTIONS:detections"
options {
[mediapipe.InferenceCalculatorOptions.ext] {
model_path: "/models/face_detection_small.tflite"
num_threads: 4
input_size { width: 160 height: 120 }
score_threshold: 0.5
iou_threshold: 0.45
max_detections: 50
backend: CPU
}
}
}

# ========== 夜间配置 ==========
node {
calculator: "FatigueScoreCalculator"
input_stream: "EAR:ear"
input_stream: "PERCLOS:perclos"
input_stream: "POSE:head_pose"
output_stream: "SCORE:fatigue_score"
options {
[mediapipe.FatigueOptions.ext] {
perclos_weight: 0.4
ear_weight: 0.35
head_pose_weight: 0.25
perclos_threshold: 0.20
ear_threshold: 0.25
sensitivity: MEDIUM
}
}
}

# ========== 白天配置 ==========
node {
calculator: "FatigueScoreCalculator"
input_stream: "EAR:ear"
input_stream: "PERCLOS:perclos"
input_stream: "POSE:head_pose"
output_stream: "SCORE:fatigue_score"
options {
[mediapipe.FatigueOptions.ext] {
perclos_weight: 0.5
ear_weight: 0.3
head_pose_weight: 0.2
perclos_threshold: 0.15
ear_threshold: 0.2
sensitivity: HIGH
}
}
}

十三、动态配置覆盖

13.1 通过 Side Packet 覆盖

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
// ========== Side Packet 覆盖示例 ==========

static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("INPUT").Set<InputType>();
cc->Outputs().Tag("OUTPUT").Set<OutputType>();
cc->Options<MyCalculatorOptions>();

// 定义可选的 Side Packet 用于覆盖配置
cc->InputSidePackets().Tag("OVERRIDE_THRESHOLD").Set<float>().Optional();
cc->InputSidePackets().Tag("OVERRIDE_MODE").Set<int>().Optional();

return absl::OkStatus();
}

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

// 读取默认配置
threshold_ = options.threshold();
mode_ = options.mode();

// ========== Side Packet 覆盖 ==========
// 检查 Side Packet 是否存在
if (cc->InputSidePackets().HasTag("OVERRIDE_THRESHOLD")) {
// 使用 Side Packet 覆盖
threshold_ = cc->InputSidePackets().Tag("OVERRIDE_THRESHOLD").Get<float>();
LOG(INFO) << "Threshold overridden to: " << threshold_;
}

if (cc->InputSidePackets().HasTag("OVERRIDE_MODE")) {
int mode_int = cc->InputSidePackets().Tag("OVERRIDE_MODE").Get<int>();
mode_ = static_cast<MyCalculatorOptions::Mode>(mode_int);
LOG(INFO) << "Mode overridden to: " << mode_int;
}

// 验证覆盖后的值
RET_CHECK(threshold_ > 0.0f && threshold_ < 1.0f);

return absl::OkStatus();
}

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
# ========== 默认配置 ==========
node {
calculator: "MyCalculator"
input_stream: "INPUT:input"
output_stream: "OUTPUT:output"
options {
[mediapipe.MyCalculatorOptions.ext] {
threshold: 0.5
mode: FAST
}
}
}

# ========== 使用 Side Packet 覆盖 ==========
input_side_packet: "DYNAMIC_THRESHOLD:threshold"
input_side_packet: "DYNAMIC_MODE:mode"

node {
calculator: "MyCalculator"
input_stream: "INPUT:input"
input_side_packet: "OVERRIDE_THRESHOLD:threshold"
input_side_packet: "OVERRIDE_MODE:mode"
output_stream: "OUTPUT:output"
options {
[mediapipe.MyCalculatorOptions.ext] {
threshold: 0.5 # 默认值,可被 Side Packet 覆盖
mode: FAST
}
}
}

13.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
// ========== 运行时配置文件示例 ==========

message DynamicConfigOptions {
optional string config_path = 1; # 配置文件路径
optional bool hot_reload = 2 [default = false]; # 是否热重载
}

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

// 从配置文件加载
if (!dynamic_options.config_path().empty()) {
DynamicConfig config;
MP_RETURN_IF_ERROR(LoadConfigFromFile(dynamic_options.config_path(), &config));

// 应用动态配置
if (config.has_threshold()) {
threshold_ = config.threshold();
} else {
threshold_ = options.threshold(); // 使用默认值
}

if (config.has_mode()) {
mode_ = config.mode();
} else {
mode_ = options.mode();
}
} else {
// 使用 Options 中的配置
threshold_ = options.threshold();
mode_ = options.mode();
}

return absl::OkStatus();
}

absl::Status LoadConfigFromFile(const std::string& path, DynamicConfig* config) {
// 读取 JSON 或 YAML 配置文件
std::ifstream file(path);
if (!file.is_open()) {
return absl::NotFoundError("Config file not found: " + path);
}

// 解析配置文件
// ...

return absl::OkStatus();
}

十四、实战:可配置推理 Calculator

14.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
133
134
135
136
137
138
// inference_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_INFERENCE_INFERENCE_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_INFERENCE_INFERENCE_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/formats/tensor.h"
#include "inference_calculator_options.pb.h"

namespace mediapipe {

class ConfigurableInferenceCalculator : 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<InferenceCalculatorOptions>();

// 可选的动态配置 Side Packet
cc->InputSidePackets().Tag("OVERRIDE_THRESHOLD").Set<float>().Optional();
cc->InputSidePackets().Tag("MODEL_PATH").Set<std::string>();

return absl::OkStatus();
}

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

// ========== 验证必需配置 ==========
RET_CHECK(options.has_input_size()) << "input_size is required";

// ========== 读取模型配置 ==========
model_path_ = cc->InputSidePackets().Tag("MODEL_PATH").Get<std::string>();
num_threads_ = options.num_threads();
backend_ = options.backend();

// ========== 读取输入配置 ==========
input_width_ = options.input_size().width();
input_height_ = options.input_size().height();

// ========== 读取后处理配置 ==========
score_threshold_ = options.score_threshold();
iou_threshold_ = options.iou_threshold();
max_detections_ = options.max_detections();

// ========== Side Packet 覆盖 ==========
if (cc->InputSidePackets().HasTag("OVERRIDE_THRESHOLD")) {
score_threshold_ = cc->InputSidePackets().Tag("OVERRIDE_THRESHOLD").Get<float>();
LOG(INFO) << "Score threshold overridden to: " << score_threshold_;
}

// ========== 验证配置 ==========
RET_CHECK(!model_path_.empty()) << "model_path is required";
RET_CHECK(input_width_ > 0 && input_height_ > 0) << "Invalid input size";
RET_CHECK(score_threshold_ > 0.0f && score_threshold_ < 1.0f)
<< "Invalid score threshold";
RET_CHECK(iou_threshold_ > 0.0f && iou_threshold_ < 1.0f)
<< "Invalid IoU threshold";

// ========== 加载模型 ==========
MP_RETURN_IF_ERROR(LoadModel(model_path_, backend_, num_threads_));

LOG(INFO) << "ConfigurableInferenceCalculator initialized: "
<< "model=" << model_path_
<< ", input_size=" << input_width_ << "x" << input_height_
<< ", threshold=" << score_threshold_
<< ", backend=" << Backend_Name(backend_);

return absl::OkStatus();
}

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

const ImageFrame& image = cc->Inputs().Tag("IMAGE").Get<ImageFrame>();

// ========== 预处理 ==========
cv::Mat input = Preprocess(image, input_width_, input_height_);

// ========== 推理 ==========
auto raw_output = Inference(input);

// ========== 后处理 ==========
std::vector<Detection> detections = Postprocess(
raw_output, score_threshold_, iou_threshold_, max_detections_);

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

process_count_++;

return absl::OkStatus();
}

absl::Status Close(CalculatorContext* cc) override {
if (interpreter_) {
interpreter_->Reset();
}
LOG(INFO) << "ConfigurableInferenceCalculator closed, processed "
<< process_count_ << " frames";
return absl::OkStatus();
}

private:
// ========== 配置参数 ==========
std::string model_path_;
int num_threads_ = 4;
int input_width_ = 320;
int input_height_ = 320;
float score_threshold_ = 0.5f;
float iou_threshold_ = 0.45f;
int max_detections_ = 100;
InferenceCalculatorOptions::Backend backend_ = InferenceCalculatorOptions::CPU;

// ========== 运行时状态 ==========
std::unique_ptr<tflite::Interpreter> interpreter_;
int process_count_ = 0;

// ========== 方法 ==========
absl::Status LoadModel(const std::string& path,
InferenceCalculatorOptions::Backend backend,
int num_threads);
cv::Mat Preprocess(const ImageFrame& image, int width, int height);
std::vector<float> Inference(const cv::Mat& input);
std::vector<Detection> Postprocess(const std::vector<float>& output,
float score_threshold,
float iou_threshold,
int max_detections);
};

REGISTER_CALCULATOR(ConfigurableInferenceCalculator);

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_INFERENCE_INFERENCE_CALCULATOR_H_

Proto 定义:

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
# inference_calculator_options.proto
syntax = "proto3";

package mediapipe;

message InferenceCalculatorOptions {
// 模型配置
optional int32 num_threads = 1 [default = 4];

// 输入配置
message InputSize {
optional int32 width = 1;
optional int32 height = 2;
}
optional InputSize input_size = 2;

// 后处理配置
optional float score_threshold = 3 [default = 0.5];
optional float iou_threshold = 4 [default = 0.45];
optional int32 max_detections = 5 [default = 100];

// 推理后端
enum Backend {
CPU = 0;
GPU = 1;
DSP = 2;
NPU = 3;
}
optional Backend backend = 6 [default = CPU];
}

十五、总结

要点 说明
Proto 定义 定义 Options 结构
字段类型 基本类型、枚举、嵌套、重复、Map
默认值 使用 [default = value]
Calculator 读取 cc->Options<T>()
Graph 配置 options { [...] { ... } }
动态覆盖 Side Packet 或配置文件

下篇预告

MediaPipe 系列 10:线程模型与调度策略

深入讲解 MediaPipe 线程模型、Executor 配置、调度策略。


参考资料

  1. Protocol Buffers. Language Guide (proto3)
  2. Google AI Edge. MediaPipe Calculator Options
  3. Protocol Buffers. C++ Generated Code

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


MediaPipe 系列 09:Calculator Options——参数化配置完整指南
https://dapalm.com/2026/03/12/MediaPipe系列09-Calculator-Options:参数化配置/
作者
Mars
发布于
2026年3月12日
许可协议