MediaPipe 系列 22:结果输出 Calculator——标准化输出格式完整指南

前言:为什么需要标准化输出?

22.1 标准化输出的重要性

IMS 检测结果需要统一输出格式,便于后续处理、日志记录、CAN 发送:

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
┌─────────────────────────────────────────────────────────────────────────┐
│ 标准化输出的重要性 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 问题:检测结果如何标准化输出? │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ IMS 需要多种输出格式: │ │
│ │ │ │
│ │ • CAN 总线:二进制消息,车身控制 │ │
│ │ • 日志系统:JSON 格式,数据记录 │ │
│ │ • 可视化:Protobuf 格式,渲染显示 │ │
│ │ • 云平台:MQTT 消息,远程监控 │ │
│ │ • 调试接口:控制台打印,开发调试 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 挑战: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ • 不同目标需要不同格式 │ │
│ │ • 需要保持数据一致性 │ │
│ │ • 需要高性能序列化 │ │
│ │ • 需要支持多种传输协议 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 解决方案:标准化输出 Calculator │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 统一数据结构:Protobuf 定义 │ │
│ │ 2. 多种序列化:JSON、Binary、Text │ │
│ │ 3. 多种传输:CAN、MQTT、HTTP、文件 │ │
│ │ 4. 灵活配置:支持不同输出目标 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

22.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
┌─────────────────────────────────────────────────────────────┐
│ 输出需求分析 │
├─────────────────────────────────────────────────────────────┤
│ │
1. CAN 总线输出 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 格式:二进制帧(最多 8 字节) │ │
│ │ 频率:10-100 Hz │ │
│ │ 要求:实时性高,数据量小 │ │
│ │ │ │
│ │ 示例: │ │
│ │ - 0x500: 疲劳等级(1 字节) │ │
│ │ - 0x501: 分心区域(1 字节) │ │
│ │ - 0x502: 告警状态(1 字节) │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
2. 日志输出 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 格式:JSON 文本 │ │
│ │ 频率:1-10 Hz │ │
│ │ 要求:可读性好,易于分析 │ │
│ │ │ │
│ │ 示例: │ │
│ │ { │ │
│ │ "timestamp": 1710123456789, │ │
│ │ "fatigue_score": 0.35, │ │
│ │ "alert": false │ │
│ │ } │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
3. 可视化输出 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 格式:Protobuf 二进制 │ │
│ │ 频率:30 FPS │ │
│ │ 要求:高性能,支持复杂结构 │ │
│ │ │ │
│ │ 包含:检测框、关键点、热力图 │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
4. 云平台输出 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 格式:JSON over MQTT │ │
│ │ 频率:0.1-1 Hz │ │
│ │ 要求:支持断线重连,低带宽 │ │
│ │ │ │
│ │ 主题:ims/vehicle/{id}/dms │ │
│ │ QoS: 1(至少送达一次) │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

二十三、标准化数据结构

23.1 Protobuf 定义

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
// ========== ims_output.proto ==========
syntax = "proto3";

package ims;

// ========== IMS 统一输出 ==========
message IMSOutput {
// ========== 时间信息 ==========
int64 timestamp_ms = 1;
int64 frame_id = 2;

// ========== DMS 结果 ==========
DMSResult dms = 3;

// ========== OMS 结果 ==========
OMSResult oms = 4;

// ========== 车辆状态 ==========
VehicleState vehicle = 5;

// ========== 系统状态 ==========
SystemStatus system = 6;
}

// ========== DMS 结果 ==========
message DMSResult {
// ========== 疲劳检测 ==========
float fatigue_score = 1; // 疲劳分数 [0-1]
int32 fatigue_level = 2; // 疲劳等级 0-3
float perclos = 3; // PERCLOS 值 [0-100]
float blink_rate = 4; // 眨眼频率(次/分钟)

// ========== 分心检测 ==========
float distraction_score = 5; // 分心分数 [0-1]
int32 gaze_zone = 6; // 视线区域 0-9
GazeDirection gaze_direction = 7;

// ========== 眼动追踪 ==========
EyeState eye_state = 8;
repeated Landmark eye_landmarks = 9;

// ========== 头部姿态 ==========
HeadPose head_pose = 10;

// ========== 打哈欠 ==========
bool is_yawning = 11;
float yaw_duration = 12;

// ========== 告警状态 ==========
bool alert_fatigue = 13;
bool alert_distraction = 14;
bool alert_no_driver = 15;
bool alert_invalid_face = 16;
int32 alert_level = 17; // 0=无, 1=轻度, 2=中度, 3=重度
string alert_message = 18;

// ========== 统计信息 ==========
int32 frames_processed = 20;
float avg_fatigue_score = 21;
}

// ========== OMS 结果 ==========
message OMSResult {
// ========== 乘员检测 ==========
int32 num_occupants = 1; // 乘员数量
repeated Occupant occupants = 2;

// ========== 安全带检测 ==========
repeated SeatbeltStatus seatbelts = 3;
int32 num_unbuckled = 4;

// ========== 儿童座椅检测 ==========
repeated ChildSeatStatus child_seats = 5;

// ========== CPD 检测 ==========
bool child_presence_detected = 6;
float child_confidence = 7;
}

// ========== 乘员信息 ==========
message Occupant {
int32 seat_position = 1; // 座椅位置 0-7
float age_estimate = 2; // 年龄估计
OccupantType type = 3;
BoundingBox bbox = 4;
}

// ========== 安全带状态 ==========
message SeatbeltStatus {
int32 seat_position = 1;
bool is_fastened = 2;
float confidence = 3;
}

// ========== 儿童座椅状态 ==========
message ChildSeatStatus {
int32 seat_position = 1;
bool is_present = 2;
ChildSeatType type = 3;
}

// ========== 眼睛状态 ==========
message EyeState {
float left_eye_open = 1; // 左眼开合 [0-1]
float right_eye_open = 2; // 右眼开合 [0-1]
float left_ear = 3;
float right_ear = 4;
bool left_eye_closed = 5;
bool right_eye_closed = 6;
bool both_eyes_closed = 7;
}

// ========== 头部姿态 ==========
message HeadPose {
float yaw = 1; // 偏航角(度)
float pitch = 2; // 俯仰角(度)
float roll = 3; // 翻滚角(度)
}

// ========== 车辆状态 ==========
message VehicleState {
float speed = 1; // km/h
float steering_angle = 2; // 度
bool turn_signal_left = 3;
bool turn_signal_right = 4;
int32 gear = 5;
float engine_rpm = 6;
bool brake_pressed = 7;
}

// ========== 系统状态 ==========
message SystemStatus {
float cpu_usage = 1;
float memory_usage = 2;
float fps = 3;
int32 frame_id = 4;
string device_id = 5;
}

// ========== 枚举类型 ==========
enum OccupantType {
UNKNOWN = 0;
ADULT = 1;
CHILD = 2;
INFANT = 3;
}

enum ChildSeatType {
NONE = 0;
REAR_FACING = 1;
FORWARD_FACING = 2;
BOOSTER = 3;
}

enum GazeDirection {
FORWARD = 0;
LEFT = 1;
RIGHT = 2;
UP = 3;
DOWN = 4;
LEFT_UP = 5;
LEFT_DOWN = 6;
RIGHT_UP = 7;
RIGHT_DOWN = 8;
}

// ========== 辅助消息 ==========
message Landmark {
float x = 1;
float y = 2;
float z = 3;
int32 id = 4;
}

message BoundingBox {
float xmin = 1;
float ymin = 2;
float xmax = 3;
float ymax = 4;
float score = 5;
}

二十四、JSON 输出 Calculator

24.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
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
192
193
194
195
196
197
// json_output_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_OUTPUT_JSON_OUTPUT_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_OUTPUT_JSON_OUTPUT_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "ims_output.pb.h"
#include <sstream>
#include <iomanip>

namespace mediapipe {

// ========== JSON 输出 Calculator ==========
class JSONOutputCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMS_OUTPUT").Set<ims::IMSOutput>();
cc->Outputs().Tag("JSON").Set<std::string>();

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

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

pretty_print_ = options.pretty_print();
include_system_status_ = options.include_system_status();
precision_ = options.precision();

return absl::OkStatus();
}

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

const ims::IMSOutput& output = cc->Inputs().Tag("IMS_OUTPUT").Get<ims::IMSOutput>();

// 转换为 JSON
std::string json = IMSOutputToJSON(output);

cc->Outputs().Tag("JSON").AddPacket(
MakePacket<std::string>(json).At(cc->InputTimestamp()));

return absl::OkStatus();
}

private:
bool pretty_print_ = false;
bool include_system_status_ = false;
int precision_ = 2;

std::string IMSOutputToJSON(const ims::IMSOutput& output) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(precision_);

ss << "{";

// ========== 根级别 ==========
ss << "\"timestamp_ms\":" << output.timestamp_ms() << ",";
ss << "\"frame_id\":" << output.frame_id() << ",";

// ========== DMS 结果 ==========
ss << "\"dms\":";
DMSResultToJSON(ss, output.dms());
ss << ",";

// ========== OMS 结果 ==========
ss << "\"oms\":";
OMSResultToJSON(ss, output.oms());
ss << ",";

// ========== 车辆状态 ==========
ss << "\"vehicle\":";
VehicleStateToJSON(ss, output.vehicle());

// ========== 系统状态(可选)==========
if (include_system_status_) {
ss << ",";
ss << "\"system\":";
SystemStatusToJSON(ss, output.system());
}

ss << "}";

return ss.str();
}

void DMSResultToJSON(std::ostringstream& ss, const ims::DMSResult& dms) {
ss << "{";

// 疲劳检测
ss << "\"fatigue_score\":" << dms.fatigue_score() << ",";
ss << "\"fatigue_level\":" << dms.fatigue_level() << ",";
ss << "\"perclos\":" << dms.perclos() << ",";
ss << "\"blink_rate\":" << dms.blink_rate() << ",";

// 分心检测
ss << "\"distraction_score\":" << dms.distraction_score() << ",";
ss << "\"gaze_zone\":" << dms.gaze_zone() << ",";
ss << "\"gaze_direction\":" << static_cast<int>(dms.gaze_direction()) << ",";

// 头部姿态
ss << "\"head_pose\":{";
ss << "\"yaw\":" << dms.head_pose().yaw() << ",";
ss << "\"pitch\":" << dms.head_pose().pitch() << ",";
ss << "\"roll\":" << dms.head_pose().roll();
ss << "},";

// 眼睛状态
ss << "\"eye_state\":{";
ss << "\"left_eye_open\":" << dms.eye_state().left_eye_open() << ",";
ss << "\"right_eye_open\":" << dms.eye_state().right_eye_open();
ss << "},";

// 打哈欠
ss << "\"is_yawning\":" << (dms.is_yawning() ? "true" : "false") << ",";

// 告警
ss << "\"alert_fatigue\":" << (dms.alert_fatigue() ? "true" : "false") << ",";
ss << "\"alert_distraction\":" << (dms.alert_distraction() ? "true" : "false") << ",";
ss << "\"alert_no_driver\":" << (dms.alert_no_driver() ? "true" : "false") << ",";
ss << "\"alert_level\":" << dms.alert_level() << ",";
ss << "\"alert_message\":\"" << EscapeJSON(dms.alert_message()) << "\"";

ss << "}";
}

void OMSResultToJSON(std::ostringstream& ss, const ims::OMSResult& oms) {
ss << "{";

// 乘员检测
ss << "\"num_occupants\":" << oms.num_occupants() << ",";

// 安全带
ss << "\"num_unbuckled\":" << oms.num_unbuckled() << ",";

// CPD
ss << "\"child_presence_detected\":" <<
(oms.child_presence_detected() ? "true" : "false") << ",";
ss << "\"child_confidence\":" << oms.child_confidence();

ss << "}";
}

void VehicleStateToJSON(std::ostringstream& ss, const ims::VehicleState& vehicle) {
ss << "{";

ss << "\"speed\":" << vehicle.speed() << ",";
ss << "\"steering_angle\":" << vehicle.steering_angle() << ",";
ss << "\"turn_signal_left\":" << (vehicle.turn_signal_left() ? "true" : "false") << ",";
ss << "\"turn_signal_right\":" << (vehicle.turn_signal_right() ? "true" : "false") << ",";
ss << "\"gear\":" << vehicle.gear();

ss << "}";
}

void SystemStatusToJSON(std::ostringstream& ss, const ims::SystemStatus& system) {
ss << "{";

ss << "\"cpu_usage\":" << system.cpu_usage() << ",";
ss << "\"memory_usage\":" << system.memory_usage() << ",";
ss << "\"fps\":" << system.fps() << ",";
ss << "\"device_id\":\"" << system.device_id() << "\"";

ss << "}";
}

std::string EscapeJSON(const std::string& s) {
std::string result;
for (char c : s) {
switch (c) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default:
if (c >= 32 && c < 127) {
result += c;
} else {
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", c);
result += buf;
}
}
}
return result;
}
};

REGISTER_CALCULATOR(JSONOutputCalculator);

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_OUTPUT_JSON_OUTPUT_CALCULATOR_H_

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
34
35
36
37
38
39
40
41
{
"timestamp_ms": 1710123456789,
"frame_id": 123456,
"dms": {
"fatigue_score": 0.35,
"fatigue_level": 1,
"perclos": 28.30,
"blink_rate": 12.50,
"distraction_score": 0.15,
"gaze_zone": 0,
"gaze_direction": 0,
"head_pose": {
"yaw": -5.20,
"pitch": 2.10,
"roll": 0.80
},
"eye_state": {
"left_eye_open": 0.85,
"right_eye_open": 0.87
},
"is_yawning": false,
"alert_fatigue": false,
"alert_distraction": false,
"alert_no_driver": false,
"alert_level": 0,
"alert_message": ""
},
"oms": {
"num_occupants": 2,
"num_unbuckled": 0,
"child_presence_detected": false,
"child_confidence": 0.00
},
"vehicle": {
"speed": 65.5,
"steering_angle": -2.3,
"turn_signal_left": false,
"turn_signal_right": false,
"gear": 3
}
}

二十五、CAN 消息输出 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
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
// can_output_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_OUTPUT_CAN_OUTPUT_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_OUTPUT_CAN_OUTPUT_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "ims_output.pb.h"
#include <linux/can.h>

namespace mediapipe {

// ========== CAN ID 定义 ==========
namespace can_id {
// DMS 相关
constexpr uint32_t DMS_FATIGUE = 0x500;
constexpr uint32_t DMS_DISTRACTION = 0x501;
constexpr uint32_t DMS_GAZE = 0x502;
constexpr uint32_t DMS_ALERT = 0x503;
constexpr uint32_t DMS_STATUS = 0x504;

// OMS 相关
constexpr uint32_t OMS_OCCUPANTS = 0x510;
constexpr uint32_t OMS_SEATBELTS = 0x511;
constexpr uint32_t OMS_CPD = 0x512;

// 系统状态
constexpr uint32_t SYSTEM_STATUS = 0x5F0;
}

// ========== CAN 消息封装 ==========
class CANMessageBuilder {
public:
static can_frame BuildFatigueFrame(const ims::DMSResult& dms) {
can_frame frame;
memset(&frame, 0, sizeof(frame));

frame.can_id = can_id::DMS_FATIGUE;
frame.can_dlc = 8;

// 字节 0: 疲劳等级(0-3)
frame.data[0] = static_cast<uint8_t>(dms.fatigue_level() & 0x0F);

// 字节 1: 疲劳分数(0-100)
frame.data[1] = static_cast<uint8_t>(dms.fatigue_score() * 100);

// 字节 2: PERCLOS(0-100)
frame.data[2] = static_cast<uint8_t>(std::clamp(dms.perclos(), 0.0f, 100.0f));

// 字节 3: 眨眼频率(0-50)
frame.data[3] = static_cast<uint8_t>(std::clamp(dms.blink_rate(), 0.0f, 50.0f));

// 字节 4-5: 保留
frame.data[4] = 0;
frame.data[5] = 0;

// 字节 6: 标志位
uint8_t flags = 0;
if (dms.alert_fatigue()) flags |= 0x01;
if (dms.is_yawning()) flags |= 0x02;
frame.data[6] = flags;

// 字节 7: 告警等级(0-3)
frame.data[7] = static_cast<uint8_t>(dms.alert_level() & 0x0F);

return frame;
}

static can_frame BuildDistractionFrame(const ims::DMSResult& dms) {
can_frame frame;
memset(&frame, 0, sizeof(frame));

frame.can_id = can_id::DMS_DISTRACTION;
frame.can_dlc = 8;

// 字节 0: 视线区域(0-9)
frame.data[0] = static_cast<uint8_t>(dms.gaze_zone() & 0x0F);

// 字节 1: 视线方向(0-8)
frame.data[1] = static_cast<uint8_t>(static_cast<int>(dms.gaze_direction()) & 0x0F);

// 字节 2: 分心分数(0-100)
frame.data[2] = static_cast<uint8_t>(dms.distraction_score() * 100);

// 字节 3: 头部姿态 Yaw(-180 到 +180)
int16_t yaw = static_cast<int16_t>(dms.head_pose().yaw() * 10 + 1800);
frame.data[3] = yaw >> 8;
frame.data[4] = yaw & 0xFF;

// 字节 5: 标志位
uint8_t flags = 0;
if (dms.alert_distraction()) flags |= 0x01;
if (dms.alert_no_driver()) flags |= 0x02;
frame.data[5] = flags;

// 字节 6-7: 保留
frame.data[6] = 0;
frame.data[7] = 0;

return frame;
}

static can_frame BuildAlertFrame(const ims::DMSResult& dms,
const ims::OMSResult& oms) {
can_frame frame;
memset(&frame, 0, sizeof(frame));

frame.can_id = can_id::DMS_ALERT;
frame.can_dlc = 8;

// 字节 0: 告警类型
uint8_t alert_type = 0;
if (dms.alert_fatigue()) alert_type |= 0x01;
if (dms.alert_distraction()) alert_type |= 0x02;
if (dms.alert_no_driver()) alert_type |= 0x04;
if (dms.alert_invalid_face()) alert_type |= 0x08;
if (oms.child_presence_detected()) alert_type |= 0x10;
frame.data[0] = alert_type;

// 字节 1: 告警等级(0-3)
frame.data[1] = static_cast<uint8_t>(dms.alert_level() & 0x0F);

// 字节 2-7: 告警计数器
static uint32_t alert_counter = 0;
alert_counter++;
frame.data[2] = (alert_counter >> 24) & 0xFF;
frame.data[3] = (alert_counter >> 16) & 0xFF;
frame.data[4] = (alert_counter >> 8) & 0xFF;
frame.data[5] = alert_counter & 0xFF;
frame.data[6] = 0;
frame.data[7] = 0;

return frame;
}
};

// ========== CAN 输出 Calculator ==========
class CANOutputCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("IMS_OUTPUT").Set<ims::IMSOutput>();
cc->Outputs().Tag("CAN_FRAMES").Set<std::vector<can_frame>>();

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

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

const ims::IMSOutput& output = cc->Inputs().Tag("IMS_OUTPUT").Get<ims::IMSOutput>();

// ========== 构造 CAN 帧列表 ==========
std::vector<can_frame> frames;

// 疲劳告警帧
frames.push_back(CANMessageBuilder::BuildFatigueFrame(output.dms()));

// 分心告警帧
frames.push_back(CANMessageBuilder::BuildDistractionFrame(output.dms()));

// 综合告警帧
frames.push_back(CANMessageBuilder::BuildAlertFrame(output.dms(), output.oms()));

// ========== 输出 ==========
cc->Outputs().Tag("CAN_FRAMES").AddPacket(
MakePacket<std::vector<can_frame>>(frames).At(cc->InputTimestamp()));

return absl::OkStatus();
}
};

REGISTER_CALCULATOR(CANOutputCalculator);

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_OUTPUT_CAN_OUTPUT_CALCULATOR_H_

二十六、日志记录 Calculator

26.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
// file_logger_calculator.h
class FileLoggerCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("JSON").Set<std::string>();

cc->Outputs().Tag("LOGGED").Set<bool>();

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

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

log_path_ = options.log_path();
max_file_size_mb_ = options.max_file_size_mb();
backup_count_ = options.backup_count();

// 打开日志文件
log_file_.open(log_path_, std::ios::app);
RET_CHECK(log_file_.is_open()) << "Failed to open log file: " << log_path_;

LOG(INFO) << "FileLoggerCalculator opened: " << log_path_;

return absl::OkStatus();
}

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

const std::string& json = cc->Inputs().Tag("JSON").Get<std::string>();

// ========== 检查文件大小 ==========
CheckFileSize();

// ========== 写入日志 ==========
log_file_ << json << std::endl;
log_file_.flush();

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

return absl::OkStatus();
}

absl::Status Close(CalculatorContext* cc) override {
if (log_file_.is_open()) {
log_file_.close();
}
return absl::OkStatus();
}

private:
std::string log_path_;
int max_file_size_mb_ = 100;
int backup_count_ = 5;
std::ofstream log_file_;

void CheckFileSize() {
// 获取文件大小
log_file_.flush();
std::ifstream in(log_path_, std::ios::ate);
if (!in.good()) return;

std::streampos size = in.tellg();
in.close();

// 检查是否超过最大大小
if (size > max_file_size_mb_ * 1024 * 1024) {
RotateLogFile();
}
}

void RotateLogFile() {
log_file_.close();

// 删除最老的备份
std::string old_backup = log_path_ + "." + std::to_string(backup_count_);
std::remove(old_backup.c_str());

// 重命名备份文件
for (int i = backup_count_ - 1; i >= 1; --i) {
std::string old_name = log_path_ + "." + std::to_string(i);
std::string new_name = log_path_ + "." + std::to_string(i + 1);
std::rename(old_name.c_str(), new_name.c_str());
}

// 重命名当前日志
std::rename(log_path_.c_str(), (log_path_ + ".1").c_str());

// 重新打开日志文件
log_file_.open(log_path_, std::ios::app);
}
};

REGISTER_CALCULATOR(FileLoggerCalculator);

26.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# ims_output_graph.pbtxt

# ========== 汇总所有结果 ==========
node {
calculator: "IMSResultAggregator"
input_stream: "DMS:dms_result"
input_stream: "OMS:oms_result"
input_stream: "VEHICLE:vehicle_state"
input_stream: "SYSTEM:system_status"
output_stream: "OUTPUT:ims_output"
}

# ========== JSON 输出 ==========
node {
calculator: "JSONOutputCalculator"
input_stream: "IMS_OUTPUT:ims_output"
output_stream: "JSON:json_string"
options {
[mediapipe.JSONOutputOptions.ext] {
pretty_print: true
include_system_status: false
precision: 2
}
}
}

# ========== CAN 输出 ==========
node {
calculator: "CANOutputCalculator"
input_stream: "IMS_OUTPUT:ims_output"
output_stream: "CAN_FRAMES:can_frames"
}

# ========== 文件日志 ==========
node {
calculator: "FileLoggerCalculator"
input_stream: "JSON:json_string"
output_stream: "LOGGED:logged"
options {
[mediapipe.FileLoggerOptions.ext] {
log_path: "/var/log/ims/dms.log"
max_file_size_mb: 100
backup_count: 5
}
}
}

# ========== CAN 发送 ==========
node {
calculator: "CANPublisher"
input_stream: "FRAMES:can_frames"
}

二十七、总结

输出格式 Calculator 用途
JSON JSONOutputCalculator 日志、调试
CAN 帧 CANOutputCalculator 车身控制
文件日志 FileLoggerCalculator 数据记录

下篇预告

MediaPipe 系列 23:调试 Calculator——可视化与日志

深入讲解如何调试 Graph:可视化工具、日志系统、性能分析。


参考资料

  1. Google AI Edge. Output Formats
  2. JSON.org. JSON Specification
  3. Linux Socket CAN. Documentation

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


MediaPipe 系列 22:结果输出 Calculator——标准化输出格式完整指南
https://dapalm.com/2026/03/13/MediaPipe系列22-结果输出Calculator:标准化输出格式/
作者
Mars
发布于
2026年3月13日
许可协议