MediaPipe 系列 23:调试 Calculator——可视化与日志完整指南

前言:为什么需要完善的调试机制?

23.1 调试的重要性

调试是开发 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
┌─────────────────────────────────────────────────────────────────────────┐
│ 调试的重要性 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 问题:如何快速定位 Calculator 开发中的问题? │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Calculator 开发常见问题: │ │
│ │ │ │
│ │ • 输入数据格式错误 │ │
│ │ • 时间戳不匹配 │ │
│ │ • 内存泄漏 │ │
│ │ • 性能瓶颈 │ │
│ │ • 结果异常 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 挑战: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ • 内部数据无法直接访问 │ │
│ │ • 需要可视化验证结果 │ │
│ │ • 性能数据分散在多个步骤 │ │
│ │ • 需要详细的执行日志 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 解决方案:完善的调试机制 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. LOG/VLOG 日志:状态跟踪 │ │
│ │ 2. 可视化输出:结果验证 │ │
│ │ 3. Profiling:性能分析 │ │
│ │ 4. Packet 检查:数据验证 │ │
│ │ 5. Graph Visualizer:流程可视化 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

23.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
┌─────────────────────────────────────────────────────────────┐
│ 调试手段分类 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 日志系统 │
│ ┌─────────────────────────────────────────────┐ │
│ │ • LOG(INFO): 常规信息 │ │
│ │ • LOG(ERROR): 错误信息 │ │
│ │ • VLOG(1-5): 详细日志 │ │
│ │ • LOG_IF: 条件日志 │ │
│ │ │ │
│ │ 优点:实时输出,不影响性能 │ │
│ │ 用途:状态跟踪,问题定位 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 2. 可视化输出 │
│ ┌─────────────────────────────────────────────┐ │
│ │ • 检测框标注 │ │
│ │ • 关键点绘制 │ │
│ │ • 热力图显示 │ │
│ │ • 路径可视化 │ │
│ │ │ │
│ │ 优点:直观,便于验证结果 │ │
│ │ 用途:结果验证,调试算法 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 3. 性能分析 │
│ ┌─────────────────────────────────────────────┐ │
│ │ • Calculator 执行时间 │ │
│ │ • 内存使用 │ │
│ │ • 帧率统计 │ │
│ │ • Pipeline 阻塞分析 │ │
│ │ │ │
│ │ 优点:量化性能,定位瓶颈 │ │
│ │ 用途:性能优化,资源管理 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 4. 数据检查 │
│ ┌─────────────────────────────────────────────┐ │
│ │ • Packet 是否存在 │ │
│ │ • 数据类型验证 │ │
│ │ • 范围检查 │ │
│ │ • 数据校验 │ │
│ │ │ │
│ │ 优点:快速验证,减少错误 │ │
│ │ 用途:数据调试,单元测试 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 5. 工具支持 │
│ ┌─────────────────────────────────────────────┐ │
│ │ • Graph Visualizer: 在线可视化 │ │
│ │ • Profiling: 性能分析报告 │ │
│ │ • Debugger: 断点调试 │ │
│ │ • Valgrind: 内存泄漏检测 │ │
│ │ │ │
│ │ 优点:专业工具,全面支持 │ │
│ │ 用途:深度调试,性能调优 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

二十四、日志输出

24.1 GLOG 日志系统

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
// ========== GLOG 日志宏详解 ==========

#include "mediapipe/framework/port/logging.h"
#include <chrono>

// ========== 基本日志宏 ==========
absl::Status Process(CalculatorContext* cc) override {
// ========== LOG(INFO): 常规信息 ==========
LOG(INFO) << "Processing frame at timestamp: " << cc->InputTimestamp();

// ========== LOG(ERROR): 错误信息 ==========
if (error_condition) {
LOG(ERROR) << "Failed to process frame: " << error_message;
}

// ========== LOG(WARNING): 警告信息 ==========
if (warning_condition) {
LOG(WARNING) << "Low frame rate detected: " << fps << " FPS";
}

// ========== LOG(FATAL): 致命错误(会终止程序)==========
if (critical_error) {
LOG(FATAL) << "Critical error occurred, terminating...";
// 程序会在这里终止
}

return absl::OkStatus();
}

// ========== 条件日志宏 ==========
absl::Status Process(CalculatorContext* cc) override {
const auto& detections = cc->Inputs().Tag("DETECTIONS").Get<std::vector<Detection>>();

// LOG_IF: 条件满足时才输出
LOG_IF(INFO, detections.size() > 0)
<< "Found " << detections.size() << " detections";

// LOG_IF_EVERY_N: 每 N 次输出一次
LOG_IF_EVERY_N(INFO, detections.size() > 10, 100)
<< "Many detections found: " << detections.size();

// LOG_IF_FIRST_N: 前 N 次输出一次
LOG_IF_FIRST_N(INFO, detections.size() > 10, 10)
<< "Many detections found: " << detections.size();

return absl::OkStatus();
}

// ========== VLOG: 详细日志 ==========
absl::Status Process(CalculatorContext* cc) override {
// VLOG(1): 详细信息
VLOG(1) << "Processing frame: " << cc->InputTimestamp();

// VLOG(2): 更详细的信息
VLOG(2) << "Detections: ";
for (const auto& det : detections) {
VLOG(2) << " - Label: " << det.label()
<< ", Score: " << det.score();
}

// VLOG(3): 调试信息
VLOG(3) << "Memory usage: " << GetMemoryUsage() << " MB";

// VLOG(4): 性能数据
VLOG(4) << "Execution time: " << GetExecutionTime() << " ms";

// VLOG(5): 最详细的信息
VLOG(5) << "Full packet data: " << packet.DebugString();

return absl::OkStatus();
}

// ========== 自定义日志格式 ==========
absl::Status Process(CalculatorContext* cc) override {
// 使用 LOG_EVERY_N 输出固定频率的日志
static int frame_count = 0;
frame_count++;

LOG_EVERY_N(INFO, 100) << "Processed " << frame_count << " frames";

// 使用 LOG_FIRST_N 输出前 N 帧的日志
LOG_FIRST_N(INFO, 10) << "First 10 frames processed";

// 使用 LOG_IF_LEVEL 控制日志级别
LOG_IF_LEVEL(INFO, condition) << "Conditional log message";

return absl::OkStatus();
}

24.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
# ========== Linux 环境变量控制 ==========
# GLOG_logtostderr: 输出到标准错误
GLOG_logtostderr=1 GLOG_v=2 ./my_calculator

# GLOG_v: 设置 VLOG 级别
# v=0: 只输出 LOG(INFO)
# v=1: 输出 VLOG(1)
# v=2: 输出 VLOG(1) 和 VLOG(2)
# v=3: 输出 VLOG(1) ~ VLOG(3)
# v=4: 输出 VLOG(1) ~ VLOG(4)
# v=5: 输出 VLOG(1) ~ VLOG(5)

# GLOG_vmodule: 按模块控制 VLOG
GLOG_vmodule="FaceDetector=2,OCR=3" ./my_calculator

# GLOG_minloglevel: 最小日志级别
# 0=INFO, 1=WARNING, 2=ERROR, 3=FATAL
GLOG_minloglevel=2 ./my_calculator # 只输出错误和致命错误

# GLOG_log_dir: 指定日志目录
GLOG_log_dir="/var/log/mediapipe" ./my_calculator

# GLOG_log_prefix: 日志前缀
GLOG_log_prefix=true ./my_calculator

# GLOG_timestamp: 时间戳
GLOG_timestamp=true ./my_calculator

# 示例:详细调试
GLOG_logtostderr=1 GLOG_v=3 GLOG_vmodule="FaceDetector=2" ./my_calculator

# 示例:只看错误
GLOG_logtostderr=1 GLOG_minloglevel=2 ./my_calculator

24.3 日志输出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# LOG(INFO) 输出
[1234567890] Processing frame at timestamp: Timestamp(value=1000)
[1234567890] Found 3 detections

# VLOG(2) 输出
[1234567890] Detections:
[1234567890] - Label: face, Score: 0.95
[1234567890] - Label: person, Score: 0.88
[1234567890] - Label: person, Score: 0.72

# LOG(ERROR) 输出
[1234567890] ERROR: Failed to process frame: Invalid data format

# 日志文件格式
[1234567890] [my_calculator] Processing frame at timestamp: Timestamp(value=1000)
[1234567890] [my_calculator] Found 3 detections

二十五、可视化 Calculator

25.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
139
140
141
142
143
144
145
146
147
148
149
150
// detection_visualizer_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_DEBUG_DETECTION_VISUALIZER_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_DEBUG_DETECTION_VISUALIZER_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/formats/image_frame_opencv.h"
#include <opencv2/opencv.hpp>

namespace mediapipe {

// ========== Proto Options ==========
/*
syntax = "proto3";
package mediapipe;

message DetectionVisualizerOptions {
// 颜色配置
optional int32 box_color_r = 1 [default = 0];
optional int32 box_color_g = 2 [default = 255];
optional int32 box_color_b = 3 [default = 0];
optional int32 text_color_r = 4 [default = 0];
optional int32 text_color_g = 5 [default = 255];
optional int32 text_color_b = 6 [default = 0];

// 线宽
optional int32 line_width = 7 [default = 2];

// 标签字体
optional int32 font_face = 8 [default = 0]; // 0=FONT_HERSHEY_SIMPLEX
optional float font_scale = 9 [default = 0.5];

// 是否显示置信度
optional bool show_score = 10 [default = true];
}
*/

// ========== 检测框可视化 Calculator ==========
class DetectionVisualizerCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMAGE").Set<ImageFrame>();
cc->Inputs().Tag("DETECTIONS").Set<std::vector<Detection>>();
cc->Outputs().Tag("IMAGE").Set<ImageFrame>();

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

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

// 读取颜色配置
box_color_ = cv::Scalar(options.box_color_r(),
options.box_color_g(),
options.box_color_b());
text_color_ = cv::Scalar(options.text_color_r(),
options.text_color_g(),
options.text_color_b());

line_width_ = options.line_width();
font_scale_ = options.font_scale();
show_score_ = options.show_score();
font_face_ = options.font_face();

VLOG(1) << "DetectionVisualizerCalculator initialized: "
<< "color=(" << box_color_.val[0] << ","
<< box_color_.val[1] << "," << box_color_.val[2] << ")";

return absl::OkStatus();
}

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

// ========== 1. 获取输入图像 ==========
const ImageFrame& input = cc->Inputs().Tag("IMAGE").Get<ImageFrame>();
cv::Mat image = formats::MatView(&input).clone();

// ========== 2. 绘制检测框 ==========
if (!cc->Inputs().Tag("DETECTIONS").IsEmpty()) {
const auto& detections =
cc->Inputs().Tag("DETECTIONS").Get<std::vector<Detection>>();

VLOG(2) << "Drawing " << detections.size() << " detections";

for (size_t i = 0; i < detections.size(); ++i) {
const auto& det = detections[i];

// ========== 计算边界框坐标 ==========
int x1 = static_cast<int>(det.xmin() * image.cols);
int y1 = static_cast<int>(det.ymin() * image.rows);
int x2 = static_cast<int>(det.xmax() * image.cols);
int y2 = static_cast<int>(det.ymax() * image.rows);

// ========== 绘制矩形框 ==========
cv::rectangle(image, cv::Point(x1, y1), cv::Point(x2, y2),
box_color_, line_width_);

// ========== 绘制标签 ==========
std::string label = det.label();
if (show_score_) {
label += cv::format(" %.2f", det.score());
}

int baseline = 0;
cv::Size text_size = cv::getTextSize(label, font_face_, font_scale_,
line_width_, &baseline);

// ========== 绘制标签背景 ==========
cv::rectangle(image,
cv::Point(x1, y1 - text_size.height - 10),
cv::Point(x1 + text_size.width, y1),
box_color_, -1);

// ========== 绘制标签文字 ==========
cv::putText(image, label,
cv::Point(x1, y1 - 5),
font_face_, font_scale_, text_color_, line_width_);

VLOG(3) << "Drew detection " << i << ": " << label;
}
}

// ========== 3. 输出可视化结果 ==========
auto output_frame = absl::make_unique<ImageFrame>(
input.Format(), image.cols, image.rows, image.step,
image.data, [image](uint8_t*) mutable { image.release(); });

cc->Outputs().Tag("IMAGE").Add(output_frame.release(), cc->InputTimestamp());

return absl::OkStatus();
}

private:
cv::Scalar box_color_;
cv::Scalar text_color_;
int line_width_;
float font_scale_;
bool show_score_;
int font_face_;
};

REGISTER_CALCULATOR(DetectionVisualizerCalculator);

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_DEBUG_DETECTION_VISUALIZER_CALCULATOR_H_

25.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
// ========== 关键点可视化函数 ==========
void DrawLandmarks(cv::Mat& image, const std::vector<Point>& landmarks,
const cv::Scalar& color = cv::Scalar(255, 0, 0),
int radius = 3) {
// ========== 绘制点 ==========
for (const auto& pt : landmarks) {
cv::circle(image, cv::Point(pt.x(), pt.y()), radius,
color, -1);
}

VLOG(3) << "Drew " << landmarks.size() << " landmarks";
}

// ========== 绘制连接线 ==========
void DrawLandmarkConnections(cv::Mat& image,
const std::vector<Point>& landmarks,
const std::vector<std::pair<int, int>>& connections,
const cv::Scalar& color = cv::Scalar(0, 255, 255),
int thickness = 1) {
for (const auto& conn : connections) {
if (conn.first < landmarks.size() && conn.second < landmarks.size()) {
cv::line(image,
cv::Point(landmarks[conn.first].x(), landmarks[conn.first].y()),
cv::Point(landmarks[conn.second].x(), landmarks[conn.second].y()),
color, thickness);
}
}
}

// ========== 绘制眼睛区域 ==========
void DrawEyeRegion(cv::Mat& image, const BoundingBox& bbox,
const cv::Scalar& color = cv::Scalar(0, 255, 0)) {
int x1 = static_cast<int>(bbox.xmin() * image.cols);
int y1 = static_cast<int>(bbox.ymin() * image.rows);
int x2 = static_cast<int>(bbox.xmax() * image.cols);
int y2 = static_cast<int>(bbox.ymax() * image.rows);

cv::rectangle(image, cv::Point(x1, y1), cv::Point(x2, y2), color, 2);
cv::putText(image, "Eye Region", cv::Point(x1, y1 - 10),
cv::FONT_HERSHEY_SIMPLEX, 0.5, color, 2);
}

// ========== 绘制疲劳热力图 ==========
void DrawFatigueHeatmap(cv::Mat& image, const std::vector<float>& heatmap,
int radius = 20) {
int step = image.rows / heatmap.size();

for (size_t i = 0; i < heatmap.size(); ++i) {
float value = heatmap[i];
if (value > 0.5) {
cv::Scalar color = cv::Scalar(0, 0, 255); // 红色
cv::circle(image, cv::Point(i * step, step / 2), radius, color, -1);
}
}

VLOG(3) << "Drew fatigue heatmap with " << heatmap.size() << " values";
}

二十六、性能分析

26.1 启用 Profiling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ========== 在 Graph 配置中启用 Profiling ==========
/*
node {
calculator: "MyCalculator"
input_stream: "INPUT:input"
output_stream: "OUTPUT:output"
options {
[mediapipe.CalculatorOptions.ext] {
[mediapipe.MyCalculatorOptions.ext] {
enable_profiling: true
}
}
}
}
*/

26.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
// ========== 在 Graph 关闭后获取性能报告 ==========
#include "mediapipe/framework/calculator_framework.h"

class MyCalculator : public CalculatorBase {
public:
absl::Status Close(CalculatorContext* cc) override {
// 获取 Profiling Context
auto profiler = cc->ProfilingContext();
if (profiler) {
// 打印性能摘要
profiler->PrintSummary();

// 获取详细数据
const auto& timeline = profiler->GetTimeline();
for (const auto& entry : timeline) {
LOG(INFO) << entry.first << ": " << entry.second << " ms";
}
}

return absl::OkStatus();
}
};

// ========== Profiling 输出示例 ==========
// Profile summary:
// Calculator Avg Time Calls Total Min Max
// FaceDetector 2.5ms 1000 2.5s 2.3ms 2.8ms
// LandmarkEstimator 3.2ms 1000 3.2s 3.0ms 3.5ms
// EyeStateCalculator 1.5ms 1000 1.5s 1.4ms 1.7ms
// Renderer 1.0ms 1000 1.0s 0.9ms 1.2ms

26.3 自定义 Profiling

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
// ========== 自定义 Profiling ==========
#include "mediapipe/framework/profiler.h"

class MyCalculator : public CalculatorBase {
public:
absl::Status Process(CalculatorContext* cc) override {
// 获取 Profiling Context
auto profiler = cc->ProfilingContext();

if (profiler) {
// 开始计时
auto start = profiler->ScopedTimer("MyCalculator::Process");

// 处理逻辑
// ...

// 自动结束计时
}

return absl::OkStatus();
}
};

// ========== 自定义指标 ==========
class MyCalculator : public CalculatorBase {
public:
absl::Status Process(CalculatorContext* cc) override {
auto profiler = cc->ProfilingContext();

if (profiler) {
// 记录自定义指标
profiler->AddMetric("detections_count", detections.size());
profiler->AddMetric("avg_confidence", avg_confidence);

// 记录内存使用
size_t memory = GetMemoryUsage();
profiler->AddMetric("memory_usage_mb", static_cast<double>(memory) / (1024 * 1024));
}

return absl::OkStatus();
}
};

二十七、Packet 检查

27.1 PacketPresenceCalculator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ========== 内置 Packet 检查 Calculator ==========
// MediaPipe 内置的 Packet 检查工具

node {
calculator: "PacketPresenceCalculator"
input_stream: "input"
output_stream: "presence"
options {
[mediapipe.PacketPresenceCalculatorOptions.ext] {
// 选项配置
}
}
}

// 输出:true=Packet 存在,false=Packet 不存在

27.2 自定义 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// ========== Packet 检查 Calculator ==========
class PacketInspectorCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Index(0).SetAny();
cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0));

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

absl::Status Process(CalculatorContext* cc) override {
// ========== 检查 Packet 是否存在 ==========
if (cc->Inputs().Index(0).IsEmpty()) {
LOG(WARNING) << "Packet is empty at timestamp: "
<< cc->InputTimestamp();
return absl::OkStatus();
}

// ========== 获取 Packet ==========
const Packet& packet = cc->Inputs().Index(0).Value();

// ========== 打印 Packet 信息 ==========
LOG(INFO) << "=== Packet Inspection ===";
LOG(INFO) << "Timestamp: " << packet.Timestamp();
LOG(INFO) << "Type: " << packet.Type().name();
LOG(INFO) << "DebugString: " << packet.DebugString();

// ========== 数据验证 ==========
if (packet.ValidateAs<float>().ok()) {
float value = packet.Get<float>();
LOG(INFO) << "Value: " << value;

// 检查范围
if (value < 0.0f || value > 1.0f) {
LOG(ERROR) << "Invalid value: " << value;
}
}

if (packet.ValidateAs<std::vector<Detection>>().ok()) {
const auto& detections = packet.Get<std::vector<Detection>>();
LOG(INFO) << "Detections count: " << detections.size();

// 检查数据完整性
for (size_t i = 0; i < detections.size(); ++i) {
const auto& det = detections[i];
if (det.xmin() < 0 || det.xmax() > 1 || det.ymin() < 0 || det.ymax() > 1) {
LOG(WARNING) << "Invalid detection " << i << ": " << det.DebugString();
}
}
}

// ========== 直接透传 ==========
cc->Outputs().Index(0).AddPacket(packet);

return absl::OkStatus();
}
};

REGISTER_CALCULATOR(PacketInspectorCalculator);

二十八、Graph Visualizer

28.1 在线可视化工具

1
https://viz.mediapipe.dev/

28.2 使用方法

1
2
3
4
5
6
7
8
9
10
11
1. 打开 https://viz.mediapipe.dev/
2. 粘贴 Graph 配置(pbtxt)
3. 点击 "Visualize" 按钮
4. 查看可视化图表

示例 Graph 配置:
node {
calculator: "FaceDetector"
input_stream: "IMAGE:image"
output_stream: "DETECTIONS:detections"
}

28.3 可视化输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────┐
│ Graph Visualizer 输出 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Graph │ │
│ │ │ │
│ │ [IMAGE] ──▶ [FaceDetector] ──▶ [DETECTIONS] │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ FaceDetector │ │ │
│ │ │ │ │ │
│ │ │ CPU: 2.5ms │ │ │
│ │ │ GPU: 1.8ms │ │ │
│ │ │ Memory: 50MB │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

二十九、调试技巧与最佳实践

29.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
// ========== 技巧 1: 渐进式调试 ==========
absl::Status Process(CalculatorContext* cc) override {
// 1. 先检查输入
if (cc->Inputs().Index(0).IsEmpty()) {
LOG(WARNING) << "Input is empty";
return absl::OkStatus();
}

// 2. 打印关键数据
const Packet& input = cc->Inputs().Index(0).Value();
LOG(INFO) << "Input timestamp: " << input.Timestamp();

// 3. 分步处理
auto step1 = Step1(input);
LOG(INFO) << "Step1 completed";

auto step2 = Step2(step1);
LOG(INFO) << "Step2 completed";

// 4. 输出检查
if (step2.empty()) {
LOG(ERROR) << "Output is empty!";
}

cc->Outputs().Index(0).AddPacket(MakePacket<Data>(step2).At(cc->InputTimestamp()));

return absl::OkStatus();
}

// ========== 技巧 2: 条件调试 ==========
absl::Status Process(CalculatorContext* cc) override {
// 只在特定条件下输出详细日志
if (cc->InputTimestamp().Value() % 1000 == 0) {
LOG(INFO) << "Processing frame: " << cc->InputTimestamp();
}

// 只在错误时输出
if (error_occurred) {
LOG(ERROR) << "Error details: " << error_message;
}

// 使用 VLOG 控制详细程度
VLOG(1) << "Processing...";
VLOG(2) << "Detailed: " << detailed_info;

return absl::OkStatus();
}

// ========== 技巧 3: 内存检查 ==========
absl::Status Process(CalculatorContext* cc) override {
// 记录内存使用
size_t mem_before = GetMemoryUsage();

// 处理逻辑
// ...

size_t mem_after = GetMemoryUsage();
size_t mem_delta = mem_after - mem_before;

if (mem_delta > 10 * 1024 * 1024) { // 超过 10MB
LOG(WARNING) << "Memory delta: " << mem_delta / (1024 * 1024) << " MB";
}

return absl::OkStatus();
}

// ========== 技巧 4: 性能分析 ==========
absl::Status Process(CalculatorContext* cc) override {
auto profiler = cc->ProfilingContext();
if (!profiler) {
return absl::OkStatus();
}

auto timer = profiler->ScopedTimer("MyCalculator::Process");

// 处理逻辑
// ...

return absl::OkStatus();
}

29.2 调试检查清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────┐
│ 调试检查清单 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 开发前: │
│ ☐ 理解输入输出格式 │
│ ☐ 定义调试日志策略 │
│ ☐ 准备测试数据 │
│ │
│ 开发中: │
│ ☐ 每个步骤都有日志输出 │
│ ☐ 关键数据都打印出来 │
│ ☐ 错误信息详细记录 │
│ ☐ 使用 VLOG 控制详细程度 │
│ │
│ 开发后: │
│ ☐ 检查日志输出是否正常 │
│ ☐ 验证可视化结果 │
│ ☐ 分析性能数据 │
│ ☡ 定位并修复问题 │
│ │
└─────────────────────────────────────────────────────────────┘

三十、总结

调试手段 说明 工具
LOG/VLOG 日志输出 GLOG
可视化 图像标注 OpenCV
Profiling 性能分析 ProfilingContext
Packet 检查 数据验证 PacketInspector
Visualizer Graph 可视化 viz.mediapipe.dev

下篇预告

MediaPipe 系列 24:性能优化 Calculator——零拷贝设计

深入讲解如何优化性能:零拷贝、内存管理、异步处理。


参考资料

  1. Google AI Edge. Logging
  2. Google AI Edge. Profiling
  3. Google AI Edge. Graph Visualizer

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


MediaPipe 系列 23:调试 Calculator——可视化与日志完整指南
https://dapalm.com/2026/03/13/MediaPipe系列23-调试Calculator:可视化与日志/
作者
Mars
发布于
2026年3月13日
许可协议