MediaPipe 系列 31:Image Segmentation——图像分割完整指南

前言:为什么需要图像分割?

31.1 Image Segmentation 的重要性

图像分割在 IMS/OMS 中的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────────────────────────┐
Image SegmentationIMS/OMS 中的应用 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
IMS/OMS 场景需求: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ • 乘员分割(分离乘员与背景) │ │
│ │ • 儿童座椅区域分割 │ │
│ │ • 背景去除(提高检测精度) │ │
│ │ • 乘员轮廓提取(用于 3D 重建) │ │
│ │ • 安全带检测辅助(排除背景干扰) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
MediaPipe Segmentation 特点: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ • 实时性能(~5ms GPU) │ │
│ │ • 轻量级模型(~2MB) │ │
│ │ • 支持二值分割和多类别分割 │ │
│ │ • 高精度边缘处理 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

31.2 功能特点

特性 说明
分割类型 二值分割 / 多类别分割
输出 分割掩码(Mask)
分辨率 256×256(可调整)
速度 ~5ms (GPU), ~15ms (CPU)
模型大小 ~2MB

31.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
┌─────────────────────────────────────────────────────────────────────────┐
│ 分割类型对比 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 语义分割(Semantic Segmentation): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 输入:RGB 图像 │ │
│ │ 输出:每个像素的类别标签 │ │
│ │ │ │
│ │ 示例: │ │
│ │ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ │ │
│ │ │ 人│ 人│ 人│背景│ │ 1 1 1 0 │ │ │
│ │ ├───┼───┼───┼───┤ ├───┼───┼───┼───┤ │ │
│ │ │ 人│ 人│背景│背景│ │ 1 1 0 0 │ │ │
│ │ ├───┼───┼───┼───┤ ├───┼───┼───┼───┤ │ │
│ │ │背景│背景│背景│背景│ │ 0 0 0 0 │ │ │
│ │ └───┴───┴───┴───┘ └───┴───┴───┴───┘ │ │
│ │ │ │
│ │ 类别:人=1, 背景=0 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 实例分割(Instance Segmentation): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 输入:RGB 图像 │ │
│ │ 输出:每个实例的独立掩码 │ │
│ │ │ │
│ │ 示例: │ │
│ │ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ │ │
│ │ │人1│人1│人2│背景│ │ 1 1 2 0 │ │ │
│ │ ├───┼───┼───┼───┤ ├───┼───┼───┼───┤ │ │
│ │ │人1│人1│人2│人2│ │ 1 1 2 2 │ │ │
│ │ ├───┼───┼───┼───┤ ├───┼───┼───┼───┤ │ │
│ │ │背景│背景│人2│人2│ │ 0 0 2 2 │ │ │
│ │ └───┴───┴───┴───┘ └───┴───┴───┴───┘ │ │
│ │ │ │
│ │ 类别:背景=0, 人1=1, 人2=2 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

三十二、SelfieSegmentation 架构

32.1 模型架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌─────────────────────────────────────────────────────────────────────────┐
SelfieSegmentation 架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 输入层 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 256×256×3 RGB │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
Encoder(特征提取) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ MobileNet V2 Backbone │ │
│ │ ├── Conv 3×3, stride=2128×128×16 │ │
│ │ ├── Inverted Residual Block 164×64×24 │ │
│ │ ├── Inverted Residual Block 232×32×32 │ │
│ │ ├── Inverted Residual Block 316×16×64 │ │
│ │ └── Inverted Residual Block 48×8×96 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
Decoder(上采样) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Bilinear Upsample 2× → 16×16×64 │ │
│ │ Skip Connection from Encoder │ │
│ │ Bilinear Upsample 2× → 32×32×32 │ │
│ │ Skip Connection from Encoder │ │
│ │ Bilinear Upsample 2× → 64×64×24 │ │
│ │ Bilinear Upsample 2× → 128×128×16 │ │
│ │ Bilinear Upsample 2× → 256×256×1 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 输出层 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 256×256×1 Mask │ │
│ │ (0-1 概率值) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

32.2 模型选择

模型 模型大小 速度 适用场景
General (0) 1.4MB ~5ms 通用场景
Landscape (1) 2.1MB ~8ms 景观/户外

三十三、Graph 配置

33.1 完整 Graph 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# ========== SelfieSegmentation Graph 配置 ==========

# mediapipe/graphs/segmentation/selfie_segmentation_gpu.pbtxt

input_stream: "INPUT:Input"

output_stream: "SEGMENTATION_MASK:segmentation_mask"

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

# ========== 2. 缩放到模型输入尺寸 ==========
node {
calculator: "ImageTransformationCalculator"
input_stream: "IMAGE:converted_image"
output_stream: "IMAGE:resized_image"
options {
[mediapipe.ImageTransformationCalculatorOptions.ext] {
output_width: 256
output_height: 256
}
}
}

# ========== 3. 转换为 Tensor ==========
node {
calculator: "ImageToTensorCalculator"
input_stream: "IMAGE:resized_image"
output_stream: "TENSORS:input_tensors"
options {
[mediapipe.ImageToTensorCalculatorOptions.ext] {
tensor_width: 256
tensor_height: 256
tensor_channels: 3
tensor_float_range {
min: -1.0
max: 1.0
}
}
}
}

# ========== 4. 模型推理 ==========
node {
calculator: "TfLiteInferenceCalculator"
input_stream: "TENSORS:input_tensors"
output_stream: "TENSORS:output_tensors"
options {
[mediapipe.TfLiteInferenceCalculatorOptions.ext] {
model_path: "/models/selfie_segmentation.tflite"
delegate {
gpu {
use_advanced_gpu_api: true
}
}
}
}
}

# ========== 5. 后处理 ==========
node {
calculator: "SegmentationPostprocessorCalculator"
input_stream: "TENSORS:output_tensors"
output_stream: "MASK:segmentation_mask"
options {
[mediapipe.SegmentationPostprocessorCalculatorOptions.ext] {
threshold: 0.5
}
}
}

33.2 分割后处理 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
// segmentation_postprocessor_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMAGE_SEGMENTATION_POSTPROCESSOR_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMAGE_SEGMENTATION_POSTPROCESSOR_CALCULATOR_H_

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

namespace mediapipe {

// ========== Segmentation Postprocessor Calculator ==========
class SegmentationPostprocessorCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("TENSORS").Set<std::vector<TfLiteTensor>>();
cc->Outputs().Tag("MASK").Set<ImageFrame>();

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

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

threshold_ = options.threshold();

return absl::OkStatus();
}

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

const auto& tensors =
cc->Inputs().Tag("TENSORS").Get<std::vector<TfLiteTensor>>();

const auto& output_tensor = tensors[0];

// ========== 1. 获取输出尺寸 ==========
int height = output_tensor.dims->data[1];
int width = output_tensor.dims->data[2];
int channels = output_tensor.dims->data[3];

const float* data = output_tensor.data.f;

// ========== 2. 创建掩码图像 ==========
auto mask_frame = absl::make_unique<ImageFrame>(
ImageFormat::GRAY8, width, height);

uint8_t* mask_data = mask_frame->MutablePixelData();

// ========== 3. 应用阈值 ==========
for (int i = 0; i < height * width; ++i) {
float probability = data[i];
mask_data[i] = (probability > threshold_) ? 255 : 0;
}

// ========== 4. 可选:形态学处理 ==========
ApplyMorphology(mask_data, width, height);

cc->Outputs().Tag("MASK").Add(mask_frame.release(), cc->InputTimestamp());

return absl::OkStatus();
}

private:
float threshold_ = 0.5f;

void ApplyMorphology(uint8_t* mask, int width, int height) {
// 形态学开运算(去除小噪点)
cv::Mat mask_mat(height, width, CV_8UC1, mask);

cv::Mat kernel = cv::getStructuringElement(
cv::MORPH_ELLIPSE, cv::Size(5, 5));

cv::morphologyEx(mask_mat, mask_mat, cv::MORPH_OPEN, kernel);

// 形态学闭运算(填充小孔洞)
cv::morphologyEx(mask_mat, mask_mat, cv::MORPH_CLOSE, kernel);
}
};

REGISTER_CALCULATOR(SegmentationPostprocessorCalculator);

} // namespace mediapipe

#endif

三十四、应用掩码

34.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
// background_blur_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMAGE_BACKGROUND_BLUR_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMAGE_BACKGROUND_BLUR_CALCULATOR_H_

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

namespace mediapipe {

// ========== Background Blur Calculator ==========
class BackgroundBlurCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMAGE").Set<ImageFrame>();
cc->Inputs().Tag("MASK").Set<ImageFrame>();
cc->Outputs().Tag("OUTPUT").Set<ImageFrame>();

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

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

blur_strength_ = options.blur_strength();

return absl::OkStatus();
}

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

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

// ========== 1. 转换为 OpenCV Mat ==========
cv::Mat img = formats::MatView(&image);
cv::Mat msk = formats::MatView(&mask);

// ========== 2. 调整掩码尺寸 ==========
cv::resize(msk, msk, img.size());

// ========== 3. 创建模糊背景 ==========
cv::Mat blurred;
int kernel_size = blur_strength_ * 2 + 1;
cv::GaussianBlur(img, blurred, cv::Size(kernel_size, kernel_size), 0);

// ========== 4. 应用掩码 ==========
cv::Mat result = img.clone();
blurred.copyTo(result, msk < 128); // 背景区域使用模糊图

// ========== 5. 边缘羽化 ==========
ApplyEdgeFeathering(result, img, blurred, msk);

// ========== 6. 输出 ==========
auto output_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, result.cols, result.rows);

cv::Mat output_mat = formats::MatView(output_frame.get());
result.copyTo(output_mat);

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

return absl::OkStatus();
}

private:
int blur_strength_ = 21;

void ApplyEdgeFeathering(cv::Mat& result, const cv::Mat& foreground,
const cv::Mat& background, const cv::Mat& mask) {
// 边缘羽化(alpha blending)
cv::Mat mask_float;
mask.convertTo(mask_float, CV_32F, 1.0 / 255.0);

// 高斯模糊掩码边缘
cv::GaussianBlur(mask_float, mask_float, cv::Size(7, 7), 0);

// Alpha blending
cv::Mat fg_float, bg_float;
foreground.convertTo(fg_float, CV_32F);
background.convertTo(bg_float, CV_32F);

cv::Mat blended = fg_float.mul(mask_float) + bg_float.mul(1.0 - mask_float);
blended.convertTo(result, CV_8U);
}
};

REGISTER_CALCULATOR(BackgroundBlurCalculator);

} // namespace mediapipe

#endif

34.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
// background_replace_calculator.h

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

cv::Mat img = formats::MatView(&image);
cv::Mat msk = formats::MatView(&mask);
cv::Mat bg = formats::MatView(&background);

// 调整背景尺寸
cv::resize(bg, bg, img.size());

// 调整掩码尺寸
cv::resize(msk, msk, img.size());

// 应用掩码
cv::Mat result = img.clone();
bg.copyTo(result, msk < 128); // 背景区域使用替换图

// 输出
auto output_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, result.cols, result.rows);

cv::Mat output_mat = formats::MatView(output_frame.get());
result.copyTo(output_mat);

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

return absl::OkStatus();
}

三十五、IMS 实战:乘员分割

35.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
# ims_occupant_segmentation_graph.pbtxt

input_stream: "OMS_IMAGE:oms_image"
output_stream: "MASK:occupant_mask"
output_stream: "SEGMENTED_IMAGE:segmented_image"

# ========== 1. 图像分割 ==========
node {
calculator: "SelfieSegmentationCalculator"
input_stream: "IMAGE:oms_image"
output_stream: "MASK:raw_mask"
options {
[mediapipe.SelfieSegmentationOptions.ext] {
model_selection: 0
}
}
}

# ========== 2. 掩码后处理 ==========
node {
calculator: "SegmentationPostprocessorCalculator"
input_stream: "TENSORS:raw_mask"
output_stream: "MASK:processed_mask"
options {
[mediapipe.SegmentationPostprocessorOptions.ext] {
threshold: 0.5
apply_morphology: true
}
}
}

# ========== 3. 应用分割 ==========
node {
calculator: "OccupantSegmentationCalculator"
input_stream: "IMAGE:oms_image"
input_stream: "MASK:processed_mask"
output_stream: "MASK:occupant_mask"
output_stream: "SEGMENTED_IMAGE:segmented_image"
}

35.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
74
75
76
77
78
79
80
81
82
83
84
85
// occupant_segmentation_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMS_OCCUPANT_SEGMENTATION_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMS_OCCUPANT_SEGMENTATION_CALCULATOR_H_

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

namespace mediapipe {

// ========== Occupant Segmentation Calculator ==========
class OccupantSegmentationCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMAGE").Set<ImageFrame>();
cc->Inputs().Tag("MASK").Set<ImageFrame>();
cc->Outputs().Tag("MASK").Set<ImageFrame>();
cc->Outputs().Tag("SEGMENTED_IMAGE").Set<ImageFrame>();

return absl::OkStatus();
}

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

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

cv::Mat img = formats::MatView(&image);
cv::Mat msk = formats::MatView(&mask);

// ========== 1. 计算乘员区域 ==========
double person_area = cv::countNonZero(msk);
double total_area = msk.rows * msk.cols;
double occupancy_ratio = person_area / total_area;

// ========== 2. 检测乘员边界 ==========
std::vector<std::vector<cv::Point>> contours;
cv::findContours(msk.clone(), contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

cv::Rect bounding_box;
if (!contours.empty()) {
// 找到最大轮廓
auto largest = std::max_element(contours.begin(), contours.end(),
[](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {
return cv::contourArea(a) < cv::contourArea(b);
});

bounding_box = cv::boundingRect(*largest);
}

// ========== 3. 输出分割图像 ==========
cv::Mat segmented = cv::Mat::zeros(img.size(), img.type());
img.copyTo(segmented, msk);

auto segmented_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, segmented.cols, segmented.rows);
cv::Mat segmented_mat = formats::MatView(segmented_frame.get());
segmented.copyTo(segmented_mat);

cc->Outputs().Tag("SEGMENTED_IMAGE").Add(
segmented_frame.release(), cc->InputTimestamp());

// ========== 4. 输出掩码 ==========
auto mask_frame = absl::make_unique<ImageFrame>(
ImageFormat::GRAY8, msk.cols, msk.rows);
cv::Mat output_mask = formats::MatView(mask_frame.get());
msk.copyTo(output_mask);

cc->Outputs().Tag("MASK").Add(mask_frame.release(), cc->InputTimestamp());

VLOG(1) << "Occupancy ratio: " << occupancy_ratio
<< ", bounding box: " << bounding_box;

return absl::OkStatus();
}
};

REGISTER_CALCULATOR(OccupantSegmentationCalculator);

} // namespace mediapipe

#endif

三十六、总结

要点 说明
分割类型 二值分割(人/背景)
输出 分割掩码(0-255)
模型 MobileNet V2 + Decoder
应用 背景虚化、背景替换、乘员分割
IMS 应用 乘员分割、背景去除

下篇预告

MediaPipe 系列 32:Iris Detection——虹膜定位

深入讲解虹膜检测、视线估计、IMS 视线追踪应用。


参考资料

  1. Google AI Edge. Selfie Segmentation
  2. L. Chen et al. “Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation”

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


MediaPipe 系列 31:Image Segmentation——图像分割完整指南
https://dapalm.com/2026/03/13/MediaPipe系列31-Image-Segmentation:语义分割/
作者
Mars
发布于
2026年3月13日
许可协议