MediaPipe 系列 12:图像处理 Calculator——输入输出 ImageFrame 完整指南

前言:为什么需要理解 ImageFrame?

12.1 ImageFrame 的核心地位

ImageFrame 是 MediaPipe 图像数据的统一格式:

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
┌─────────────────────────────────────────────────────────────────────────┐
│ ImageFrame 的核心地位 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 问题:如何在 Calculator 之间传递图像数据? │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 挑战: │ │
│ │ │ │
│ │ • 不同图像格式(RGB、RGBA、灰度、浮点) │ │
│ │ • 不同数据来源(摄像头、文件、内存) │ │
│ │ • 内存管理(谁分配、谁释放) │ │
│ │ • 零拷贝需求(避免重复复制) │ │
│ │ • 跨平台兼容 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 解决方案:ImageFrame │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ImageFrame = 统一的图像数据容器 │ │
│ │ │ │
│ │ • 标准化的格式定义 │ │
│ │ • 自动内存管理 │ │
│ │ • 零拷贝支持 │ │
│ │ • OpenCV 集成 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 数据流示意图: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Camera → ImageFrame → Calculator → ImageFrame → ... │ │
│ │ ↓ │ │
│ │ (OpenCV Mat 视图) │ │
│ │ ↓ │ │
│ │ 图像处理 │ │
│ │ ↓ │ │
│ │ ImageFrame 输出 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

12.2 ImageFrame 类结构

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
// ========== ImageFrame 类定义 ==========
// mediapipe/framework/formats/image_frame.h

#include "mediapipe/framework/formats/image_frame.h"

namespace mediapipe {

class ImageFrame {
public:
// ========== 构造函数 ==========

// 默认构造(空图像)
ImageFrame();

// 指定格式和尺寸
ImageFrame(ImageFormat::Format format, int width, int height);

// 指定格式、尺寸和行步长
ImageFrame(ImageFormat::Format format, int width, int height, int width_step);

// 外部数据(带释放回调)
ImageFrame(ImageFormat::Format format, int width, int height, int width_step,
uint8_t* pixel_data,
std::function<void(uint8_t*)> pixel_data_deleter);

// ========== 属性访问 ==========

// 图像格式
ImageFormat::Format Format() const;

// 尺寸
int Width() const;
int Height() const;

// 通道数
int NumberOfChannels() const;

// 每通道字节数
int ByteDepth() const;

// 行步长(字节数)
int WidthStep() const;

// 总数据大小
size_t PixelDataSize() const;

// ========== 数据访问 ==========

// 只读数据指针
const uint8_t* PixelData() const;

// 可写数据指针
uint8_t* MutablePixelData();

// 像素访问
template <typename T>
const T* GetPixelData() const;

// ========== 拷贝 ==========

// 深拷贝
std::unique_ptr<ImageFrame> Clone() const;

// 复制到目标
void CopyTo(ImageFrame* target) const;

// 从源复制
void CopyFrom(const ImageFrame& source);
};

} // namespace mediapipe

十三、ImageFormat 详解

13.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
┌─────────────────────────────────────────────────────────────┐
│ ImageFormat 格式 │
├─────────────────────────────────────────────────────────────┤
│ │
8-bit 整数格式: │
│ ┌─────────────────────────────────────────────┐ │
│ │ SRGB = RGB 8-bit (3 通道) │ │
│ │ SRGBA = RGBA 8-bit (4 通道) │ │
│ │ SBGR = BGR 8-bit (3 通道) │ │
│ │ SBGRA = BGRA 8-bit (4 通道) │ │
│ │ GRAY8 = 灰度 8-bit (1 通道) │ │
│ └─────────────────────────────────────────────┘ │
│ │
16-bit 整数格式: │
│ ┌─────────────────────────────────────────────┐ │
│ │ GRAY16 = 灰度 16-bit (1 通道) │ │
│ │ SRGB48 = RGB 16-bit (3 通道) │ │
│ │ SRGBA64 = RGBA 16-bit (4 通道) │ │
│ └─────────────────────────────────────────────┘ │
│ │
32-bit 浮点格式: │
│ ┌─────────────────────────────────────────────┐ │
│ │ VEC32F1 = 单通道 float (1 通道) │ │
│ │ VEC32F2 = 双通道 float (2 通道) │ │
│ │ VEC32F3 = 三通道 float (3 通道) │ │
│ │ VEC32F4 = 四通道 float (4 通道) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 使用场景: │
│ ┌─────────────────────────────────────────────┐ │
│ │ SRGB = 摄像头输入、显示输出 │ │
│ │ GRAY8 = 红外图像、深度图 │ │
│ │ VEC32F1 = 神经网络输出、概率图 │ │
│ │ VEC32F2 = 光流、位移场 │ │
│ │ VEC32F4 = 特征图、嵌入向量 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

13.2 格式属性查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "mediapipe/framework/formats/image_format.h"

// ========== 格式属性查询 ==========

// 获取通道数
int channels = NumberOfChannelsForFormat(ImageFormat::SRGB); // 3
int channels = NumberOfChannelsForFormat(ImageFormat::GRAY8); // 1
int channels = NumberOfChannelsForFormat(ImageFormat::VEC32F4); // 4

// 获取每通道字节数
int depth = ImageFrame::ByteDepthForFormat(ImageFormat::SRGB); // 1
int depth = ImageFrame::ByteDepthForFormat(ImageFormat::GRAY16); // 2
int depth = ImageFrame::ByteDepthForFormat(ImageFormat::VEC32F1); // 4

// 获取数据类型大小
int pixel_size = ImageFrame::PixelSizeForFormat(ImageFormat::SRGB); // 3
int pixel_size = ImageFrame::PixelSizeForFormat(ImageFormat::SRGBA); // 4

// 判断是否为浮点格式
bool is_float = IsFloatFormat(ImageFormat::VEC32F1); // true
bool is_float = IsFloatFormat(ImageFormat::SRGB); // false

十四、创建 ImageFrame

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
// ========== 从原始数据创建 ImageFrame ==========

#include "mediapipe/framework/formats/image_frame.h"

// ========== 方式 1:分配新内存 ==========
int width = 640;
int height = 480;

// 创建空白图像(自动分配内存)
auto image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, width, height);

// 填充数据
uint8_t* data = image_frame->MutablePixelData();
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int idx = (y * width + x) * 3;
data[idx + 0] = 255; // R
data[idx + 1] = 0; // G
data[idx + 2] = 0; // B
}
}

// ========== 方式 2:使用外部数据(不复制)==========
uint8_t* external_data = new uint8_t[width * height * 3];

auto image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, width, height, width * 3,
external_data,
[](uint8_t* ptr) { delete[] ptr; }); // 释放回调

// ========== 方式 3:使用共享指针 ==========
std::shared_ptr<uint8_t> shared_data(
new uint8_t[width * height * 3],
std::default_delete<uint8_t[]>());

auto image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, width, height, width * 3,
shared_data.get(),
[shared_data](uint8_t*) {}); // 捕获 shared_ptr 延长生命周期

// ========== 方式 4:从现有数据复制 ==========
std::vector<uint8_t> source_data(width * height * 3, 128);

auto image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, width, height);
std::memcpy(image_frame->MutablePixelData(),
source_data.data(),
source_data.size());

14.2 从 OpenCV Mat 创建

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
// ========== 从 OpenCV Mat 创建 ImageFrame ==========

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

// ========== 方式 1:复制数据 ==========
cv::Mat mat = cv::imread("image.jpg");
cv::Mat rgb;
cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB);

auto image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, rgb.cols, rgb.rows);

std::memcpy(image_frame->MutablePixelData(),
rgb.data,
rgb.total() * rgb.elemSize());

// ========== 方式 2:零拷贝(推荐)==========
cv::Mat mat = cv::imread("image.jpg");
cv::Mat rgb;
cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB);

// 确保 Mat 数据连续
RET_CHECK(rgb.isContinuous());

// 零拷贝创建 ImageFrame
auto image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, rgb.cols, rgb.rows, rgb.step,
rgb.data,
[rgb_capture = rgb](uint8_t*) mutable {
rgb_capture.release(); // 延长 Mat 生命周期
});

// 注意:image_frame 有效期间,rgb_capture 保持有效

// ========== 方式 3:使用 formats::MatView 反向 ==========
// 先创建 ImageFrame,再创建 Mat 视图处理
auto image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, 640, 480);

cv::Mat mat_view = formats::MatView(image_frame.get());

// 在 Mat 上处理
cv::randu(mat_view, cv::Scalar(0, 0, 0), cv::Scalar(255, 255, 255));

14.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
// ========== 特殊格式创建 ==========

// ========== 灰度图像 ==========
auto gray_frame = absl::make_unique<ImageFrame>(
ImageFormat::GRAY8, 640, 480);

// ========== 浮点格式(神经网络输出)==========
auto float_frame = absl::make_unique<ImageFrame>(
ImageFormat::VEC32F1, 224, 224); // 单通道浮点

// 初始化为 0
float* data = reinterpret_cast<float*>(float_frame->MutablePixelData());
std::fill(data, data + 224 * 224, 0.0f);

// ========== RGBA(带透明通道)==========
auto rgba_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGBA, 640, 480);

// ========== 16-bit 深度图 ==========
auto depth_frame = absl::make_unique<ImageFrame>(
ImageFormat::GRAY16, 640, 480);

uint16_t* depth_data = reinterpret_cast<uint16_t*>(
depth_frame->MutablePixelData());

十五、访问 ImageFrame 数据

15.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
// ========== 基本数据访问 ==========

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

// ========== 属性查询 ==========
int width = image.Width(); // 宽度
int height = image.Height(); // 高度
int channels = image.NumberOfChannels(); // 通道数
int depth = image.ByteDepth(); // 每通道字节数
int step = image.WidthStep(); // 行步长(字节)
size_t size = image.PixelDataSize(); // 总数据大小
ImageFormat::Format format = image.Format(); // 格式

// ========== 数据指针访问 ==========
const uint8_t* data = image.PixelData(); // 只读指针
uint8_t* mutable_data = image.MutablePixelData(); // 可写指针(需要副本)

// ========== 像素访问 ==========
// 访问 (x, y) 处的像素
for (int y = 0; y < height; ++y) {
const uint8_t* row = data + y * step;
for (int x = 0; x < width; ++x) {
int idx = x * channels;
uint8_t r = row[idx + 0];
uint8_t g = row[idx + 1];
uint8_t b = row[idx + 2];
// ...
}
}

// ========== 浮点数据访问 ==========
if (image.Format() == ImageFormat::VEC32F1) {
const float* float_data = image.GetPixelData<float>();
for (int i = 0; i < width * height; ++i) {
float value = float_data[i];
}
}

// ========== 深度图访问 ==========
if (image.Format() == ImageFormat::GRAY16) {
const uint16_t* depth_data =
reinterpret_cast<const uint16_t*>(image.PixelData());
for (int i = 0; i < width * height; ++i) {
uint16_t depth = depth_data[i];
}
}

15.2 OpenCV Mat 视图(零拷贝)

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
// ========== OpenCV Mat 视图 ==========

#include "mediapipe/framework/formats/image_frame_opencv.h"

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

// ========== 创建 Mat 视图(零拷贝)==========
cv::Mat mat_view = formats::MatView(&image);

// mat_view 是 image 的视图,不复制数据
// 注意:mat_view 是只读的!

// ========== 查询 Mat 类型 ==========
int mat_type = mat_view.type(); // CV_8UC3 for SRGB

// ========== 使用 OpenCV 处理 ==========
// 计算 ROI 均值
cv::Scalar mean = cv::mean(mat_view);
LOG(INFO) << "Mean: " << mean;

// 直方图
cv::Mat hist;
cv::Mat gray;
cv::cvtColor(mat_view, gray, cv::COLOR_RGB2GRAY);
int histSize = 256;
float range[] = {0, 256};
const float* histRange = {range};
cv::calcHist(&gray, 1, 0, cv::Mat(), hist, 1, &histSize, &histRange);

// ========== 需要修改时必须复制 ==========
cv::Mat mat_copy = mat_view.clone(); // 深拷贝

// 现在可以修改
cv::GaussianBlur(mat_copy, mat_copy, cv::Size(5, 5), 1.0);

// ========== 注意事项 ==========
// 1. MatView 依赖 ImageFrame 的生命周期
// 2. 不要在 ImageFrame 销毁后访问 MatView
// 3. MatView 是只读的,修改需要先复制

十六、图像处理 Calculator 完整示例

16.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
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
// resize_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMAGE_RESIZE_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMAGE_RESIZE_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/formats/image_frame_opencv.h"
#include "mediapipe/framework/port/opencv_imgproc.h"
#include "mediapipe/framework/port/ret_check.h"
#include "mediapipe/framework/port/status.h"

namespace mediapipe {

// ========== Proto Options ==========
// resize_calculator_options.proto
/*
syntax = "proto3";
package mediapipe;

message ResizeCalculatorOptions {
optional int32 target_width = 1;
optional int32 target_height = 2;

enum ResizeMode {
DEFAULT = 0; // 默认插值
LINEAR = 1; // 双线性
AREA = 2; // 区域
CUBIC = 3; // 双三次
LANCZOS4 = 4; // Lanczos
}
optional ResizeMode resize_mode = 3 [default = LINEAR];

optional bool preserve_aspect_ratio = 4 [default = false];
}
*/

class ResizeCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMAGE").Set<ImageFrame>();
cc->Outputs().Tag("IMAGE").Set<ImageFrame>();
cc->Options<ResizeCalculatorOptions>();
return absl::OkStatus();
}

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

// 验证配置
RET_CHECK(options.has_target_width() && options.has_target_height())
<< "target_width and target_height are required";

target_width_ = options.target_width();
target_height_ = options.target_height();
preserve_aspect_ratio_ = options.preserve_aspect_ratio();

// 选择插值方法
switch (options.resize_mode()) {
case ResizeCalculatorOptions::LINEAR:
interpolation_ = cv::INTER_LINEAR;
break;
case ResizeCalculatorOptions::AREA:
interpolation_ = cv::INTER_AREA;
break;
case ResizeCalculatorOptions::CUBIC:
interpolation_ = cv::INTER_CUBIC;
break;
case ResizeCalculatorOptions::LANCZOS4:
interpolation_ = cv::INTER_LANCZOS4;
break;
default:
interpolation_ = cv::INTER_LINEAR;
}

LOG(INFO) << "ResizeCalculator initialized: "
<< target_width_ << "x" << target_height_;

return absl::OkStatus();
}

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

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

// 创建 OpenCV 视图
cv::Mat input_mat = formats::MatView(&input);

// 计算目标尺寸
int target_width = target_width_;
int target_height = target_height_;

if (preserve_aspect_ratio_) {
// 保持宽高比
float scale_w = static_cast<float>(target_width_) / input.Width();
float scale_h = static_cast<float>(target_height_) / input.Height();
float scale = std::min(scale_w, scale_h);
target_width = static_cast<int>(input.Width() * scale);
target_height = static_cast<int>(input.Height() * scale);
}

// 缩放
cv::Mat output_mat;
cv::resize(input_mat, output_mat,
cv::Size(target_width, target_height),
0, 0, interpolation_);

// 创建输出 ImageFrame(零拷贝)
auto output_frame = absl::make_unique<ImageFrame>(
input.Format(), output_mat.cols, output_mat.rows, output_mat.step,
output_mat.data,
[output_mat](uint8_t*) mutable { output_mat.release(); });

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

return absl::OkStatus();
}

private:
int target_width_ = 320;
int target_height_ = 240;
bool preserve_aspect_ratio_ = false;
int interpolation_ = cv::INTER_LINEAR;
};

REGISTER_CALCULATOR(ResizeCalculator);

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_IMAGE_RESIZE_CALCULATOR_H_

16.2 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
# resize_graph.pbtxt

input_stream: "VIDEO:video"
output_stream: "RESIZED:resized"

# 高质量缩放
node {
calculator: "ResizeCalculator"
input_stream: "IMAGE:video"
output_stream: "IMAGE:resized"
options {
[mediapipe.ResizeCalculatorOptions.ext] {
target_width: 640
target_height: 480
resize_mode: CUBIC
preserve_aspect_ratio: true
}
}
}

# 快速缩放
node {
calculator: "ResizeCalculator"
input_stream: "IMAGE:video"
output_stream: "IMAGE:resized"
options {
[mediapipe.ResizeCalculatorOptions.ext] {
target_width: 320
target_height: 240
resize_mode: LINEAR
}
}
}

十七、零拷贝输出技巧

17.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
┌─────────────────────────────────────────────────────────────┐
│ 零拷贝输出原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 传统方式(数据复制): │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. 创建 ImageFrame(分配内存) │ │
│ │ 2. 处理数据到临时 Mat │ │
│ │ 3. 复制数据到 ImageFrame │ │
│ │ 4. 释放临时 Mat │ │
│ │ │ │
│ │ 问题: │ │
│ │ • 额外的内存分配 │ │
│ │ • 数据复制开销 │ │
│ │ • 缓存不友好 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 零拷贝方式: │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. 处理数据到临时 Mat │ │
│ │ 2. 创建 ImageFrame 直接使用 Mat 数据 │ │
│ │ 3. 通过 lambda 延长 Mat 生命周期 │ │
│ │ 4. 无数据复制 │ │
│ │ │ │
│ │ 优点: │ │
│ │ • 无额外内存分配 │ │
│ │ • 无数据复制 │ │
│ │ • 性能提升 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

17.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
// ========== 零拷贝输出实现 ==========

absl::Status Process(CalculatorContext* cc) override {
const ImageFrame& input = cc->Inputs().Tag("IMAGE").Get<ImageFrame>();
cv::Mat input_mat = formats::MatView(&input);

// 处理数据
cv::Mat output_mat;
cv::GaussianBlur(input_mat, output_mat, cv::Size(5, 5), 1.0);

// ========== 方式 1:使用 lambda 捕获 Mat ==========
auto output_frame = absl::make_unique<ImageFrame>(
input.Format(), output_mat.cols, output_mat.rows, output_mat.step,
output_mat.data,
[output_mat](uint8_t*) mutable {
// 当 ImageFrame 销毁时,释放 Mat
output_mat.release();
});

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

return absl::OkStatus();
}

// ========== 方式 2:使用 shared_ptr ==========
absl::Status Process(CalculatorContext* cc) override {
const ImageFrame& input = cc->Inputs().Tag("IMAGE").Get<ImageFrame>();
cv::Mat input_mat = formats::MatView(&input);

// 使用 shared_ptr 管理 Mat
auto mat_ptr = std::make_shared<cv::Mat>();
cv::GaussianBlur(input_mat, *mat_ptr, cv::Size(5, 5), 1.0);

auto output_frame = absl::make_unique<ImageFrame>(
input.Format(), mat_ptr->cols, mat_ptr->rows, mat_ptr->step,
mat_ptr->data,
[mat_ptr](uint8_t*) {
// shared_ptr 引用计数减少
});

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

return absl::OkStatus();
}

// ========== 方式 3:复用输入(如果不需要处理)==========
absl::Status Process(CalculatorContext* cc) override {
// 直接传递输入 Packet
Packet input_packet = cc->Inputs().Tag("IMAGE").Value();
cc->Outputs().Tag("IMAGE").AddPacket(input_packet);

return absl::OkStatus();
}

十八、图像格式转换

18.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
// ========== 格式转换 Calculator ==========

class FormatConverterCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMAGE").Set<ImageFrame>();
cc->Outputs().Tag("IMAGE").Set<ImageFrame>();
return absl::OkStatus();
}

absl::Status Process(CalculatorContext* cc) override {
const ImageFrame& input = cc->Inputs().Tag("IMAGE").Get<ImageFrame>();
cv::Mat input_mat = formats::MatView(&input);

cv::Mat output_mat;

switch (input.Format()) {
case ImageFormat::SRGB: {
// RGB -> Gray
cv::cvtColor(input_mat, output_mat, cv::COLOR_RGB2GRAY);
break;
}
case ImageFormat::GRAY8: {
// Gray -> RGB
cv::cvtColor(input_mat, output_mat, cv::COLOR_GRAY2RGB);
break;
}
case ImageFormat::SBGR: {
// BGR -> RGB
cv::cvtColor(input_mat, output_mat, cv::COLOR_BGR2RGB);
break;
}
default:
return absl::InvalidArgumentError("Unsupported format");
}

// 输出
auto output_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, output_mat.cols, output_mat.rows, output_mat.step,
output_mat.data,
[output_mat](uint8_t*) mutable { output_mat.release(); });

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

return absl::OkStatus();
}
};

18.2 OpenCV 格式对应

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
// ========== OpenCV 格式对应 ==========

// ImageFormat -> OpenCV type
int GetOpenCVType(ImageFormat::Format format) {
switch (format) {
case ImageFormat::GRAY8:
return CV_8UC1;
case ImageFormat::SRGB:
case ImageFormat::SBGR:
return CV_8UC3;
case ImageFormat::SRGBA:
case ImageFormat::SBGRA:
return CV_8UC4;
case ImageFormat::GRAY16:
return CV_16UC1;
case ImageFormat::VEC32F1:
return CV_32FC1;
case ImageFormat::VEC32F2:
return CV_32FC2;
case ImageFormat::VEC32F3:
return CV_32FC3;
case ImageFormat::VEC32F4:
return CV_32FC4;
default:
return -1; // Unknown
}
}

// OpenCV type -> ImageFormat
ImageFormat::Format GetImageFormat(int opencv_type) {
switch (opencv_type) {
case CV_8UC1:
return ImageFormat::GRAY8;
case CV_8UC3:
return ImageFormat::SRGB;
case CV_8UC4:
return ImageFormat::SRGBA;
case CV_16UC1:
return ImageFormat::GRAY16;
case CV_32FC1:
return ImageFormat::VEC32F1;
case CV_32FC2:
return ImageFormat::VEC32F2;
case CV_32FC3:
return ImageFormat::VEC32F3;
case CV_32FC4:
return ImageFormat::VEC32F4;
default:
return ImageFormat::UNKNOWN;
}
}

十九、IMS 实战:IR 图像预处理

19.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
// ir_preprocess_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IR_IR_PREPROCESS_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IR_IR_PREPROCESS_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/formats/image_frame_opencv.h"
#include "mediapipe/framework/port/opencv_imgproc.h"

namespace mediapipe {

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

message IRPreprocessOptions {
optional int32 target_width = 1 [default = 320];
optional int32 target_height = 2 [default = 240];
optional bool equalize_histogram = 3 [default = true];
optional bool normalize = 4 [default = true];
optional float gamma = 5 [default = 1.0];
optional int32 blur_size = 6 [default = 0]; // 0 = no blur
optional float clip_limit = 7 [default = 2.0]; // CLAHE
}
*/

class IRPreprocessCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IR_IMAGE").Set<ImageFrame>();
cc->Outputs().Tag("PROCESSED").Set<ImageFrame>();
cc->Options<IRPreprocessOptions>();
return absl::OkStatus();
}

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

target_width_ = options.target_width();
target_height_ = options.target_height();
equalize_hist_ = options.equalize_histogram();
normalize_ = options.normalize();
gamma_ = options.gamma();
blur_size_ = options.blur_size();
clip_limit_ = options.clip_limit();

// 初始化 CLAHE
clahe_ = cv::createCLAHE(clip_limit_, cv::Size(8, 8));

LOG(INFO) << "IRPreprocessCalculator initialized: "
<< target_width_ << "x" << target_height_
<< ", equalize=" << equalize_hist_
<< ", gamma=" << gamma_;

return absl::OkStatus();
}

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

const ImageFrame& input = cc->Inputs().Tag("IR_IMAGE").Get<ImageFrame>();
cv::Mat ir_mat = formats::MatView(&input);

// ========== 1. 转换为灰度 ==========
cv::Mat gray;
if (input.Format() == ImageFormat::SRGB) {
cv::cvtColor(ir_mat, gray, cv::COLOR_RGB2GRAY);
} else if (input.Format() == ImageFormat::GRAY8) {
gray = ir_mat.clone();
} else {
return absl::InvalidArgumentError("Unsupported input format");
}

// ========== 2. Gamma 校正(增强暗区细节)==========
if (gamma_ != 1.0f) {
cv::Mat gamma_lut(1, 256, CV_8U);
uchar* p = gamma_lut.ptr();
for (int i = 0; i < 256; ++i) {
p[i] = cv::saturate_cast<uchar>(
std::pow(i / 255.0, gamma_) * 255.0);
}
cv::LUT(gray, gamma_lut, gray);
}

// ========== 3. 直方图均衡化(增强对比度)==========
if (equalize_hist_) {
if (clip_limit_ > 0) {
// 使用 CLAHE(限制对比度自适应直方图均衡化)
clahe_->apply(gray, gray);
} else {
// 普通直方图均衡化
cv::equalizeHist(gray, gray);
}
}

// ========== 4. 降噪 ==========
if (blur_size_ > 0) {
cv::GaussianBlur(gray, gray,
cv::Size(blur_size_, blur_size_), 0);
}

// ========== 5. 缩放 ==========
cv::Mat resized;
cv::resize(gray, resized,
cv::Size(target_width_, target_height_),
0, 0, cv::INTER_LINEAR);

// ========== 6. 归一化 ==========
if (normalize_) {
cv::normalize(resized, resized, 0, 255, cv::NORM_MINMAX, CV_8U);
}

// ========== 7. 转回 RGB(模型输入格式)==========
cv::Mat rgb;
cv::cvtColor(resized, rgb, cv::COLOR_GRAY2RGB);

// ========== 8. 输出(零拷贝)==========
auto output_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, rgb.cols, rgb.rows, rgb.step,
rgb.data,
[rgb](uint8_t*) mutable { rgb.release(); });

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

return absl::OkStatus();
}

private:
int target_width_ = 320;
int target_height_ = 240;
bool equalize_hist_ = true;
bool normalize_ = true;
float gamma_ = 1.0f;
int blur_size_ = 0;
float clip_limit_ = 2.0f;

cv::Ptr<cv::CLAHE> clahe_;
};

REGISTER_CALCULATOR(IRPreprocessCalculator);

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_IR_IR_PREPROCESS_CALCULATOR_H_

二十、总结

要点 说明
ImageFrame MediaPipe 图像容器
ImageFormat 支持多种格式(RGB、灰度、浮点)
创建方式 新分配、外部数据、OpenCV Mat
MatView 零拷贝 OpenCV 视图
零拷贝输出 使用 lambda 延长数据生命周期
格式转换 使用 OpenCV cvtColor

下篇预告

MediaPipe 系列 13:推理 Calculator——集成 TFLite 模型

深入讲解如何在 Calculator 中集成 TensorFlow Lite 模型。


参考资料

  1. Google AI Edge. ImageFrame Format
  2. OpenCV. Mat Class
  3. Google AI Edge. Image Processing Calculators

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


MediaPipe 系列 12:图像处理 Calculator——输入输出 ImageFrame 完整指南
https://dapalm.com/2026/03/12/MediaPipe系列12-图像处理Calculator:输入输出ImageFrame/
作者
Mars
发布于
2026年3月12日
许可协议