前言:为什么需要条件分支? 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 ┌─────────────────────────────────────────────────────────────────────────┐ │ 条件执行的重要性 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 问题:如何根据条件选择性地执行处理流程? │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ IMS DMS 场景: │ │ │ │ │ │ │ │ • 没有检测到人脸 → 不执行关键点检测 │ │ │ │ • 白天场景 → 使用标准检测器 │ │ │ │ • 夜间场景 → 使用红外增强检测器 │ │ │ │ • 逆光场景 → 使用 HDR 处理 │ │ │ │ • 帧率低于 15 FPS → 跳过部分处理 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ 解决方案:条件分支 Calculator │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 1. Gate Calculator │ │ │ │ • 允许/阻塞数据流 │ │ │ │ • 条件为 true 通过,false 阻塞 │ │ │ │ │ │ │ │ 2. Switch Calculator │ │ │ │ • 多分支选择 │ │ │ │ • 根据索引选择输出 │ │ │ │ │ │ │ │ 3. Mux/Demux Calculator │ │ │ │ • 多路复用/解复用 │ │ │ │ • 动态路由 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
19.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 ┌─────────────────────────────────────────────────────────────┐ │ 条件分支类型 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1 . Gate (门控) │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ INPUT ──▶ [Gate] ──▶ OUTPUT (allow=true) │ │ │ │ │ │ │ │ │ └──▶ 阻塞 (allow=false) │ │ │ │ │ │ │ │ 用途:条件执行、性能优化 │ │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 2 . Switch (选择) │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ INPUT ──▶ [Switch] ──▶ Branch A (sel=0 ) │ │ │ │ │──▶ Branch B (sel=1 ) │ │ │ │ └──▶ Branch C (sel=2 ) │ │ │ │ │ │ │ │ 用途:多分支选择、动态算法切换 │ │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 3 . Mux (多路复用) │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ Input A ──┐ │ │ │ │ Input B ──┼──▶ [Mux] ──▶ Output │ │ │ │ Input C ──┘ │ │ │ │ │ │ │ │ 用途:合并多个流、选择输出 │ │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 4 . Demux (解复用) │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ ┌──▶ Output A │ │ │ │ Input ──▶ [Demux] ──┼──▶ Output B │ │ │ │ └──▶ Output C │ │ │ │ │ │ │ │ 用途:分发数据到多个分支 │ │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
二十、Gate Calculator 20.1 内置 GateCalculator 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 # 基本 Gate 用法 node { calculator: "GateCalculator" input_stream: "INPUT:data" input_stream: "ALLOW:condition" # bool 类型 output_stream: "OUTPUT:gated_data" options { [mediapipe.GateCalculatorOptions.ext] { initial_state: UNINITIALIZED allow_empty_condition: false } } } # 多输入 Gate node { calculator: "GateCalculator" input_stream: "INPUT:0:data_a" input_stream: "INPUT:1:data_b" input_stream: "ALLOW:condition" output_stream: "OUTPUT:0:gated_a" output_stream: "OUTPUT:1:gated_b" } # 反向 Gate(条件为 false 时通过) node { calculator: "GateCalculator" input_stream: "INPUT:data" input_stream: "DISALLOW:condition" # 注意:使用 DISALLOW 标签 output_stream: "OUTPUT:gated_data" }
20.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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #ifndef MEDIAPIPE_CALCULATORS_CORE_CONDITION_GENERATOR_CALCULATOR_H_ #define MEDIAPIPE_CALCULATORS_CORE_CONDITION_GENERATOR_CALCULATOR_H_ #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/detection.pb.h" namespace mediapipe {class ConditionGeneratorCalculator : public CalculatorBase { public : static absl::Status GetContract (CalculatorContract* cc) { cc->Inputs ().Tag ("DETECTIONS" ).Set<std::vector<Detection>>().Optional (); cc->Inputs ().Tag ("POSE" ).Set <HeadPose>().Optional (); cc->Inputs ().Tag ("EAR" ).Set <float >().Optional (); cc->Outputs ().Tag ("HAS_FACE" ).Set <bool >(); cc->Outputs ().Tag ("EYES_VISIBLE" ).Set <bool >(); cc->Outputs ().Tag ("HEAD_FRONT" ).Set <bool >(); cc->Outputs ().Tag ("QUALITY_OK" ).Set <bool >(); cc->Options <ConditionGeneratorOptions>(); return absl::OkStatus (); } absl::Status Open (CalculatorContext* cc) override { const auto & options = cc->Options <ConditionGeneratorOptions>(); min_face_size_ = options.min_face_size (); max_head_yaw_ = options.max_head_yaw (); min_ear_ = options.min_ear (); min_quality_ = options.min_quality (); return absl::OkStatus (); } absl::Status Process (CalculatorContext* cc) override { bool has_face = false ; if (!cc->Inputs ().Tag ("DETECTIONS" ).IsEmpty ()) { const auto & detections = cc->Inputs ().Tag ("DETECTIONS" ).Get<std::vector<Detection>>(); for (const auto & det : detections) { float width = det.xmax () - det.xmin (); float height = det.ymax () - det.ymin (); if (width > min_face_size_ && height > min_face_size_) { has_face = true ; break ; } } } bool eyes_visible = false ; if (!cc->Inputs ().Tag ("EAR" ).IsEmpty ()) { float ear = cc->Inputs ().Tag ("EAR" ).Get <float >(); eyes_visible = (ear > min_ear_); } bool head_front = false ; if (!cc->Inputs ().Tag ("POSE" ).IsEmpty ()) { const HeadPose& pose = cc->Inputs ().Tag ("POSE" ).Get <HeadPose>(); head_front = (std::abs (pose.yaw ()) < max_head_yaw_); } bool quality_ok = has_face && eyes_visible; cc->Outputs ().Tag ("HAS_FACE" ).AddPacket ( MakePacket <bool >(has_face).At (cc->InputTimestamp ())); cc->Outputs ().Tag ("EYES_VISIBLE" ).AddPacket ( MakePacket <bool >(eyes_visible).At (cc->InputTimestamp ())); cc->Outputs ().Tag ("HEAD_FRONT" ).AddPacket ( MakePacket <bool >(head_front).At (cc->InputTimestamp ())); cc->Outputs ().Tag ("QUALITY_OK" ).AddPacket ( MakePacket <bool >(quality_ok).At (cc->InputTimestamp ())); return absl::OkStatus (); } private : float min_face_size_ = 0.1f ; float max_head_yaw_ = 30.0f ; float min_ear_ = 0.15f ; float min_quality_ = 0.5f ; };REGISTER_CALCULATOR (ConditionGeneratorCalculator); } #endif
二十一、Switch Calculator 21.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 #ifndef MEDIAPIPE_CALCULATORS_CORE_SWITCH_CALCULATOR_H_ #define MEDIAPIPE_CALCULATORS_CORE_SWITCH_CALCULATOR_H_ #include "mediapipe/framework/calculator_framework.h" namespace mediapipe {template <typename T>class SwitchCalculator : public CalculatorBase { public : static absl::Status GetContract (CalculatorContract* cc) { cc->Inputs ().Tag ("SELECT" ).Set <int >(); for (int i = 0 ; i < cc->Inputs ().NumEntries ("INPUT" ); ++i) { cc->Inputs ().Get ("INPUT" , i).Set <T>(); } cc->Outputs ().Tag ("OUTPUT" ).Set <T>(); cc->Options <SwitchCalculatorOptions>(); return absl::OkStatus (); } absl::Status Open (CalculatorContext* cc) override { const auto & options = cc->Options <SwitchCalculatorOptions>(); output_mode_ = options.output_mode (); num_inputs_ = cc->Inputs ().NumEntries ("INPUT" ); LOG (INFO) << "SwitchCalculator initialized: num_inputs=" << num_inputs_; return absl::OkStatus (); } absl::Status Process (CalculatorContext* cc) override { if (cc->Inputs ().Tag ("SELECT" ).IsEmpty ()) { return absl::OkStatus (); } int select = cc->Inputs ().Tag ("SELECT" ).Get <int >(); if (select < 0 || select >= num_inputs_) { LOG (WARNING) << "Invalid switch index: " << select << ", valid range: [0, " << num_inputs_ - 1 << "]" ; return absl::OkStatus (); } if (cc->Inputs ().Get ("INPUT" , select).IsEmpty ()) { return absl::OkStatus (); } const T& selected = cc->Inputs ().Get ("INPUT" , select).Get <T>(); cc->Outputs ().Tag ("OUTPUT" ).AddPacket ( MakePacket <T>(selected).At (cc->InputTimestamp ())); return absl::OkStatus (); } private : int num_inputs_ = 0 ; SwitchCalculatorOptions::OutputMode output_mode_ = SwitchCalculatorOptions::SELECTED_ONLY; };REGISTER_CALCULATOR (SwitchCalculator);extern template class SwitchCalculator <std::vector<Detection>>;extern template class SwitchCalculator <float >; } #endif
21.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 # switch_graph.pbtxt # 场景分类 node { calculator: "SceneClassifierCalculator" input_stream: "IMAGE:image" output_stream: "SCENE:scene_id" # 0 =白天, 1 =夜晚, 2 =逆光 } # 多种算法并行执行 node { calculator: "DaytimeFaceDetector" input_stream: "IMAGE:image" output_stream: "DETECTIONS:detections_day" } node { calculator: "NightFaceDetector" input_stream: "IMAGE:image" output_stream: "DETECTIONS:detections_night" } node { calculator: "BacklightFaceDetector" input_stream: "IMAGE:image" output_stream: "DETECTIONS:detections_backlight" } # 动态选择 node { calculator: "SwitchCalculator<std::vector<Detection>>" input_stream: "SELECT:scene_id" input_stream: "INPUT:0:detections_day" input_stream: "INPUT:1:detections_night" input_stream: "INPUT:2:detections_backlight" output_stream: "OUTPUT:final_detections" }
二十二、Mux Calculator 22.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 #ifndef MEDIAPIPE_CALCULATORS_CORE_MUX_CALCULATOR_H_ #define MEDIAPIPE_CALCULATORS_CORE_MUX_CALCULATOR_H_ #include "mediapipe/framework/calculator_framework.h" namespace mediapipe {template <typename T>class MuxCalculator : public CalculatorBase { public : static absl::Status GetContract (CalculatorContract* cc) { for (int i = 0 ; i < cc->Inputs ().NumEntries ("INPUT" ); ++i) { cc->Inputs ().Get ("INPUT" , i).Set <T>(); } cc->Outputs ().Tag ("OUTPUT" ).Set <T>(); cc->Options <MuxCalculatorOptions>(); return absl::OkStatus (); } absl::Status Open (CalculatorContext* cc) override { num_inputs_ = cc->Inputs ().NumEntries ("INPUT" ); return absl::OkStatus (); } absl::Status Process (CalculatorContext* cc) override { for (int i = 0 ; i < num_inputs_; ++i) { if (!cc->Inputs ().Get ("INPUT" , i).IsEmpty ()) { const T& data = cc->Inputs ().Get ("INPUT" , i).Get <T>(); cc->Outputs ().Tag ("OUTPUT" ).AddPacket ( MakePacket <T>(data).At (cc->InputTimestamp ())); return absl::OkStatus (); } } return absl::OkStatus (); } private : int num_inputs_ = 0 ; };REGISTER_CALCULATOR (MuxCalculator); } #endif
二十三、IMS 实战:动态算法选择 23.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 # ims_adaptive_detection_graph.pbtxt input_stream: "IR_IMAGE:ir_image" output_stream: "DETECTIONS:detections" output_stream: "SCENE:scene_info" # ========== 步骤 1 :场景分析 ========== node { calculator: "SceneAnalyzerCalculator" input_stream: "IMAGE:ir_image" output_stream: "SCENE_ID:scene_id" output_stream: "SCENE_INFO:scene_info" output_stream: "BRIGHTNESS:brightness" output_stream: "CONTRAST:contrast" } # ========== 步骤 2 :多种检测器并行 ========== # 白天模式(标准检测) node { calculator: "StandardFaceDetector" input_stream: "IMAGE:ir_image" output_stream: "DETECTIONS:detections_standard" executor: "gpu_executor" } # 夜间模式(增强检测) node { calculator: "EnhancedFaceDetector" input_stream: "IMAGE:ir_image" output_stream: "DETECTIONS:detections_enhanced" options { [mediapipe.FaceDetectorOptions.ext] { enhance_contrast: true adaptive_threshold: true } } executor: "gpu_executor" } # 极端模式(HDR 检测) node { calculator: "HDRFaceDetector" input_stream: "IMAGE:ir_image" output_stream: "DETECTIONS:detections_hdr" options { [mediapipe.FaceDetectorOptions.ext] { hdr_mode: true multi_scale: true } } executor: "gpu_executor" } # ========== 步骤 3 :动态选择 ========== node { calculator: "SwitchCalculator<std::vector<Detection>>" input_stream: "SELECT:scene_id" input_stream: "INPUT:0:detections_standard" input_stream: "INPUT:1:detections_enhanced" input_stream: "INPUT:2:detections_hdr" output_stream: "OUTPUT:detections" } # ========== 步骤 4 :Gate 控制 ========== # 只有检测到人脸才执行后续处理 node { calculator: "ConditionGeneratorCalculator" input_stream: "DETECTIONS:detections" output_stream: "HAS_FACE:has_face" } node { calculator: "GateCalculator" input_stream: "INPUT:detections" input_stream: "ALLOW:has_face" output_stream: "OUTPUT:valid_detections" } # ========== 步骤 5 :后续处理 ========== # 关键点检测(仅当有人脸时) node { calculator: "LandmarkDetectorCalculator" input_stream: "IMAGE:ir_image" input_stream: "DETECTIONS:valid_detections" output_stream: "LANDMARKS:landmarks" }
23.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 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 #ifndef MEDIAPIPE_CALCULATORS_IMS_SCENE_ANALYZER_CALCULATOR_H_ #define MEDIAPIPE_CALCULATORS_IMS_SCENE_ANALYZER_CALCULATOR_H_ #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/image_frame.h" #include "mediapipe/framework/formats/image_frame_opencv.h" namespace mediapipe { message SceneInfo { int32 scene_id = 1 ; float brightness = 2 ; float contrast = 3 ; string scene_name = 4 ; }class SceneAnalyzerCalculator : public CalculatorBase { public : static absl::Status GetContract (CalculatorContract* cc) { cc->Inputs ().Tag ("IMAGE" ).Set <ImageFrame>(); cc->Outputs ().Tag ("SCENE_ID" ).Set <int >(); cc->Outputs ().Tag ("SCENE_INFO" ).Set <SceneInfo>(); cc->Outputs ().Tag ("BRIGHTNESS" ).Set <float >(); cc->Outputs ().Tag ("CONTRAST" ).Set <float >(); cc->Options <SceneAnalyzerOptions>(); return absl::OkStatus (); } absl::Status Open (CalculatorContext* cc) override { const auto & options = cc->Options <SceneAnalyzerOptions>(); dark_threshold_ = options.dark_threshold (); bright_threshold_ = options.bright_threshold (); low_contrast_threshold_ = options.low_contrast_threshold (); high_contrast_threshold_ = options.high_contrast_threshold (); 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 mat = formats::MatView (&image); cv::Mat gray; if (mat.channels () == 3 ) { cv::cvtColor (mat, gray, cv::COLOR_RGB2GRAY); } else { gray = mat; } double brightness = cv::mean (gray)[0 ]; cv::Mat std_dev; cv::meanStdDev (gray, cv::noArray (), std_dev); double contrast = std_dev.at <double >(0 ); int scene_id = 0 ; std::string scene_name = "daytime" ; if (brightness < dark_threshold_) { scene_id = 1 ; scene_name = "night" ; } else if (brightness > bright_threshold_) { scene_id = 2 ; scene_name = "backlight" ; } else if (contrast < low_contrast_threshold_) { scene_id = 3 ; scene_name = "low_contrast" ; } else if (contrast > high_contrast_threshold_) { scene_id = 4 ; scene_name = "high_contrast" ; } SceneInfo scene_info; scene_info.set_scene_id (scene_id); scene_info.set_brightness (static_cast <float >(brightness)); scene_info.set_contrast (static_cast <float >(contrast)); scene_info.set_scene_name (scene_name); cc->Outputs ().Tag ("SCENE_ID" ).AddPacket ( MakePacket <int >(scene_id).At (cc->InputTimestamp ())); cc->Outputs ().Tag ("SCENE_INFO" ).AddPacket ( MakePacket <SceneInfo>(scene_info).At (cc->InputTimestamp ())); cc->Outputs ().Tag ("BRIGHTNESS" ).AddPacket ( MakePacket <float >(static_cast <float >(brightness)).At (cc->InputTimestamp ())); cc->Outputs ().Tag ("CONTRAST" ).AddPacket ( MakePacket <float >(static_cast <float >(contrast)).At (cc->InputTimestamp ())); return absl::OkStatus (); } private : float dark_threshold_ = 50.0f ; float bright_threshold_ = 200.0f ; float low_contrast_threshold_ = 30.0f ; float high_contrast_threshold_ = 100.0f ; };REGISTER_CALCULATOR (SceneAnalyzerCalculator); } #endif
二十四、性能优化 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 ┌─────────────────────────────────────────────────────────────┐ │ 懒执行优化 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 问题:上游 Calculator 仍会执行,浪费资源 │ │ │ │ 传统 Gate: │ │ ┌─────────────────────────────────────────────┐ │ │ │ [Detector] ──▶ [Gate] ──▶ [Process] │ │ │ │ ↑ │ │ │ │ 仍会执行(浪费) │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 解决方案:FlowLimiter + 反向边 │ │ ┌─────────────────────────────────────────────┐ │ │ │ [FlowLimiter] ──▶ [Detector] ──▶ [Gate] │ │ │ │ ↑ │ │ │ │ │ └─────────────────────────┘ │ │ │ │ (back edge) │ │ │ │ │ │ │ │ Gate 阻塞时,FlowLimiter 也停止输出 │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
24.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 # 懒执行 Graph # FlowLimiter + Gate 实现懒执行 node { calculator: "FlowLimiterCalculator" input_stream: "image" input_stream: "processed" input_stream_info: { tag_index: "processed" back_edge: true } output_stream: "throttled_image" } # 条件判断 node { calculator: "ConditionGeneratorCalculator" input_stream: "DETECTIONS:detections" output_stream: "HAS_FACE:has_face" } # 检测器(受 FlowLimiter 控制) node { calculator: "FaceDetector" input_stream: "IMAGE:throttled_image" output_stream: "DETECTIONS:detections" } # Gate node { calculator: "GateCalculator" input_stream: "INPUT:detections" input_stream: "ALLOW:has_face" output_stream: "OUTPUT:gated_detections" } # 后处理 node { calculator: "LandmarkDetector" input_stream: "IMAGE:throttled_image" input_stream: "DETECTIONS:gated_detections" output_stream: "LANDMARKS:landmarks" output_stream: "PROCESSED:processed" }
二十五、总结
Calculator
功能
用途
GateCalculator
允许/阻塞
条件执行
SwitchCalculator
多分支选择
动态算法
MuxCalculator
多路复用
输入选择
ConditionGenerator
条件生成
状态判断
FlowLimiter
限流
懒执行
下篇预告 MediaPipe 系列 20:渲染 Calculator——可视化输出
深入讲解如何在 Calculator 中渲染检测结果、关键点、热力图。
参考资料
Google AI Edge. Gate Calculator
Google AI Edge. Flow Control
Google AI Edge. Conditional Execution
系列进度: 19/55更新时间: 2026-03-12