เครื่องคิดเลข

เครื่องคำนวณแต่ละแบบคือโหนดของกราฟ เราได้อธิบายวิธีสร้างเครื่องคิดเลขใหม่ วิธีเริ่มต้นเครื่องคำนวณ วิธีคำนวณ สตรีมอินพุตและเอาต์พุต การประทับเวลา และตัวเลือกต่างๆ แต่ละโหนดในกราฟ จะมีการใช้งานเป็น Calculator การดำเนินการกราฟจำนวนมากจะเกิดขึ้นในเครื่องคำนวณ เครื่องคำนวณอาจได้รับสตรีมอินพุตและ/หรือแพ็กเก็ตข้างเคียงเป็นศูนย์หรือมากกว่า และสร้างสตรีมเอาต์พุตและ/หรือแพ็กเก็ตด้านข้างเป็น 0 ขึ้นไป

CalculatorBase

เครื่องคำนวณสร้างขึ้นโดยการกำหนดคลาสย่อยใหม่ของคลาส CalculatorBase โดยใช้เมธอดต่างๆ และการลงทะเบียนคลาสย่อยใหม่ด้วย Mediapipe เครื่องคำนวณใหม่ต้องใช้ 4 วิธีต่อไปนี้เป็นอย่างน้อย

  • GetContract()
    • ผู้เขียนเครื่องคิดเลขสามารถระบุประเภทอินพุตและเอาต์พุตที่คาดไว้ของเครื่องคำนวณใน GetContract() เมื่อกราฟเริ่มต้น เฟรมเวิร์กจะเรียกเมธอดแบบคงที่เพื่อยืนยันว่าประเภทแพ็กเก็ตของอินพุตและเอาต์พุตที่เชื่อมต่อตรงกับข้อมูลในข้อกำหนดดังกล่าวหรือไม่
  • Open()
    • หลังจากกราฟเริ่มต้น เฟรมเวิร์กจะเรียก Open() ณ จุดนี้ แพ็กเก็ตด้านข้างของอินพุตจะพร้อมใช้งานสำหรับเครื่องคำนวณ Open() ตีความการดำเนินการกำหนดค่าโหนด (ดูกราฟ) และเตรียมสถานะการเรียกใช้ต่อกราฟของเครื่องคำนวณ ฟังก์ชันนี้อาจเขียนแพ็กเก็ตไปยังเอาต์พุตเครื่องคิดเลขด้วย ข้อผิดพลาดระหว่าง Open() อาจทำให้กราฟทำงานสิ้นสุดได้
  • Process()
    • สำหรับเครื่องคิดเลขที่มีอินพุต เฟรมเวิร์กจะเรียก Process() ซ้ำๆ เมื่อใดก็ตามที่สตรีมอินพุตอย่างน้อย 1 รายการมีแพ็กเก็ตที่พร้อมใช้งาน โดยค่าเริ่มต้น เฟรมเวิร์กจะรับประกันว่าอินพุตทั้งหมดมีการประทับเวลาเดียวกัน (ดูข้อมูลเพิ่มเติมในการซิงค์ข้อมูล) คุณเรียกใช้ Process() หลายรายการพร้อมกันได้เมื่อเปิดใช้การดำเนินการพร้อมกัน หากเกิดข้อผิดพลาดระหว่าง Process() การเรียกใช้เฟรมเวิร์ก Close() และการเรียกใช้กราฟจะยุติลง
  • Close()
    • หลังจากการเรียก Process() ทั้งหมดเสร็จสิ้นหรือเมื่อสตรีมอินพุตทั้งหมดปิดลงแล้ว เฟรมเวิร์กจะเรียก Close() ระบบจะเรียกฟังก์ชันนี้เสมอหากมีการเรียกใช้ Open() และทำสำเร็จ แม้ว่าการเรียกใช้กราฟจะสิ้นสุดลงเนื่องจากมีข้อผิดพลาดก็ตาม ไม่มีอินพุตพร้อมใช้งานผ่านสตรีมอินพุตในช่วง Close() แต่ยังคงเข้าถึงแพ็กเกตด้านอินพุตได้และอาจเขียนเอาต์พุต หลังจากที่ Close() แสดงผลแล้ว เครื่องคำนวณจะถือว่าโหนดที่ไม่ทำงาน วัตถุเครื่องคิดเลขจะถูกทำลายในไม่ช้า เมื่อกราฟทำงานเสร็จ

ต่อไปนี้เป็นข้อมูลโค้ดจาก CalculatorBase.h

class CalculatorBase {
 public:
  ...

  // The subclasses of CalculatorBase must implement GetContract.
  // ...
  static absl::Status GetContract(CalculatorContract* cc);

  // Open is called before any Process() calls, on a freshly constructed
  // calculator.  Subclasses may override this method to perform necessary
  // setup, and possibly output Packets and/or set output streams' headers.
  // ...
  virtual absl::Status Open(CalculatorContext* cc) {
    return absl::OkStatus();
  }

  // Processes the incoming inputs. May call the methods on cc to access
  // inputs and produce outputs.
  // ...
  virtual absl::Status Process(CalculatorContext* cc) = 0;

  // Is called if Open() was called and succeeded.  Is called either
  // immediately after processing is complete or after a graph run has ended
  // (if an error occurred in the graph).  ...
  virtual absl::Status Close(CalculatorContext* cc) {
    return absl::OkStatus();
  }

  ...
};

ชีวิตของเครื่องคิดเลข

ในระหว่างการเริ่มต้นกราฟ MediaPipe เฟรมเวิร์กจะเรียกเมธอดแบบคงที่ GetContract() เพื่อระบุประเภทของแพ็กเก็ตที่คาดไว้

เฟรมเวิร์กนี้จะสร้างและทำลายเครื่องคำนวณทั้งหมดสำหรับการเรียกใช้กราฟแต่ละครั้ง (เช่น 1 ครั้งต่อวิดีโอหรือ 1 ครั้งต่อรูปภาพ) คุณควรระบุออบเจ็กต์ที่มีราคาแพงหรือออบเจ็กต์ขนาดใหญ่ที่คงที่ตลอดการเรียกใช้กราฟเป็นแพ็กเก็ตด้านอินพุตเพื่อไม่ให้การคำนวณซ้ำในการเรียกใช้ครั้งต่อๆ ไป

หลังจากการเริ่มต้น สำหรับการเรียกใช้กราฟแต่ละครั้ง ลำดับต่อไปนี้จะเกิดขึ้น:

  • Open()
  • Process() (เกิดซ้ำ)
  • Close()

เฟรมเวิร์กจะเรียก Open() เพื่อเริ่มต้นเครื่องคำนวณ Open() ควรตีความตัวเลือกใดก็ได้และตั้งค่าสถานะการเรียกใช้กราฟต่อเครื่องคำนวณ Open() อาจรับแพ็กเก็ตด้านอินพุตและเขียนแพ็กเก็ตไปยังเอาต์พุตของเครื่องคำนวณ หากเหมาะสม ควรเรียกใช้ SetOffset() เพื่อลดการบัฟเฟอร์แพ็กเก็ตของสตรีมอินพุตที่อาจเกิดขึ้น

หากเกิดข้อผิดพลาดระหว่าง Open() หรือ Process() (ตามที่ระบุไว้โดยหนึ่งในนั้นแสดงสถานะที่ไม่ใช่ Ok) การเรียกใช้กราฟจะสิ้นสุดลงโดยไม่มีการเรียกเมธอดของเครื่องคำนวณอีกต่อไป และตัวคำนวณจะถูกทำลาย

สำหรับเครื่องคิดเลขที่มีอินพุต เฟรมเวิร์กจะเรียก Process() เมื่อใดก็ตามที่อินพุตอย่างน้อย 1 รายการมีแพ็กเก็ตที่พร้อมใช้งาน เฟรมเวิร์กนี้รับประกันว่าอินพุตทั้งหมดมีการประทับเวลาเดียวกัน ซึ่งทำให้การประทับเวลาเพิ่มขึ้นเมื่อมีการเรียกไปยัง Process() แต่ละครั้งและมีการนำส่งแพ็กเก็ตทั้งหมด ด้วยเหตุนี้ อินพุตบางรายการจึงอาจไม่มีแพ็กเกตเมื่อมีการเรียกใช้ Process() อินพุตที่แพ็กเก็ตหายไปดูเหมือนจะสร้างแพ็กเก็ตว่าง (ไม่มีการประทับเวลา)

เฟรมเวิร์กจะเรียก Close() หลังจากเรียกใช้ Process() ทุกครั้ง อินพุตทั้งหมดจะหมดแล้ว แต่ Close() มีสิทธิ์เข้าถึงแพ็กเก็ตฝั่งอินพุตและอาจเขียนเอาต์พุต หลังจากที่ปิดการทำงาน เครื่องคำนวณจะถูกทำลาย

เครื่องคิดเลขที่ไม่มีอินพุตเรียกว่าแหล่งที่มา เครื่องคำนวณแหล่งที่มาจะยังคงมีการเรียก Process() ต่อไปตราบใดที่แสดงสถานะ Ok เครื่องคำนวณแหล่งที่มาจะระบุว่ามีการใช้งานหมดแล้วโดยการแสดงสถานะหยุด (เช่น mediaPipe::tool::StatusStop())

การระบุอินพุตและเอาต์พุต

อินเทอร์เฟซสาธารณะไปยังเครื่องคำนวณประกอบด้วยชุดสตรีมอินพุตและสตรีมเอาต์พุต ในเครื่อง CalculatorGraphConfiguration เอาต์พุตจากเครื่องคำนวณบางอย่างจะเชื่อมต่อกับอินพุตของเครื่องคำนวณอื่นๆ โดยใช้สตรีมที่มีชื่อ โดยปกติแล้ว ชื่อสตรีมจะเป็นตัวพิมพ์เล็ก ส่วนแท็กอินพุตและเอาต์พุตจะเป็นตัวพิมพ์ใหญ่ ในตัวอย่างด้านล่าง เอาต์พุตที่มีชื่อแท็ก VIDEO เชื่อมต่อกับอินพุตที่มีชื่อแท็ก VIDEO_IN โดยใช้สตรีมชื่อ video_stream

# Graph describing calculator SomeAudioVideoCalculator
node {
  calculator: "SomeAudioVideoCalculator"
  input_stream: "INPUT:combined_input"
  output_stream: "VIDEO:video_stream"
}
node {
  calculator: "SomeVideoCalculator"
  input_stream: "VIDEO_IN:video_stream"
  output_stream: "VIDEO_OUT:processed_video"
}

สามารถระบุสตรีมอินพุตและเอาต์พุตด้วยหมายเลขดัชนี ตามชื่อแท็ก หรือทั้งชื่อแท็กและหมายเลขดัชนีรวมกัน ดูตัวอย่างตัวระบุอินพุตและเอาต์พุตได้จากตัวอย่างด้านล่าง SomeAudioVideoCalculator ระบุเอาต์พุตวิดีโอตามแท็กและเอาต์พุตเสียงโดยใช้แท็กและดัชนีร่วมกัน อินพุตที่มีแท็ก VIDEO เชื่อมต่อกับสตรีมชื่อ video_stream เอาต์พุตที่มีแท็ก AUDIO และดัชนี 0 และ 1 เชื่อมต่อกับสตรีมชื่อ audio_left และ audio_right SomeAudioCalculator ระบุอินพุตเสียงตามดัชนีเท่านั้น (ไม่จำเป็นต้องติดแท็ก)

# Graph describing calculator SomeAudioVideoCalculator
node {
  calculator: "SomeAudioVideoCalculator"
  input_stream: "combined_input"
  output_stream: "VIDEO:video_stream"
  output_stream: "AUDIO:0:audio_left"
  output_stream: "AUDIO:1:audio_right"
}

node {
  calculator: "SomeAudioCalculator"
  input_stream: "audio_left"
  input_stream: "audio_right"
  output_stream: "audio_energy"
}

ในการใช้งานเครื่องคำนวณ อินพุตและเอาต์พุตจะระบุตามชื่อแท็กและหมายเลขดัชนีด้วย ในฟังก์ชันด้านล่างนี้มีการระบุอินพุตและเอาต์พุต

  • ตามหมายเลขดัชนี: สตรีมอินพุตแบบรวมระบุได้ง่ายๆ ด้วยดัชนี 0
  • ตามชื่อแท็ก: สตรีมเอาต์พุตวิดีโอระบุโดยใช้ชื่อแท็ก "VIDEO"
  • ตามชื่อแท็กและหมายเลขดัชนี: สตรีมเสียงเอาต์พุตจะระบุโดยใช้ชื่อแท็ก AUDIO ร่วมกับหมายเลขดัชนี 0 และ 1
// c++ Code snippet describing the SomeAudioVideoCalculator GetContract() method
class SomeAudioVideoCalculator : public CalculatorBase {
 public:
  static absl::Status GetContract(CalculatorContract* cc) {
    cc->Inputs().Index(0).SetAny();
    // SetAny() is used to specify that whatever the type of the
    // stream is, it's acceptable.  This does not mean that any
    // packet is acceptable.  Packets in the stream still have a
    // particular type.  SetAny() has the same effect as explicitly
    // setting the type to be the stream's type.
    cc->Outputs().Tag("VIDEO").Set<ImageFrame>();
    cc->Outputs().Get("AUDIO", 0).Set<Matrix>();
    cc->Outputs().Get("AUDIO", 1).Set<Matrix>();
    return absl::OkStatus();
  }

กำลังประมวลผล

Process() ที่เรียกใช้โหนดที่ไม่ใช่แหล่งที่มาต้องแสดงผล absl::OkStatus() เพื่อระบุว่าทั้งหมดเป็นไปด้วยดี หรือมีรหัสสถานะอื่นๆ ที่ส่งสัญญาณแจ้งข้อผิดพลาด

หากเครื่องคำนวณที่ไม่ใช่แหล่งที่มาแสดงค่า tool::StatusStop() แสดงว่ากราฟนี้ถูกยกเลิกก่อนกำหนด ในกรณีนี้ เครื่องคำนวณแหล่งที่มาและสตรีมอินพุตกราฟทั้งหมดจะปิดอยู่ (และแพ็กเก็ตที่เหลือจะเผยแพร่ผ่านกราฟ)

โหนดแหล่งที่มาในกราฟจะยังคงมีการเรียก Process() บนโหนดนี้ ตราบใดที่โหนดนั้นแสดงผลเป็น absl::OkStatus() เพื่อระบุว่าไม่มีข้อมูลที่จะสร้างผลลัพธ์ tool::StatusStop() เพิ่มเติม สถานะอื่นๆ แสดงว่า มีข้อผิดพลาดเกิดขึ้น

Close() แสดงผล absl::OkStatus() เพื่อแสดงความสำเร็จ สถานะอื่นๆ หมายถึงความล้มเหลว

นี่คือฟังก์ชัน Process() พื้นฐาน และจะใช้เมธอด Input() (ซึ่งใช้ได้ต่อเมื่อเครื่องคำนวณมีอินพุตเดียว) เพื่อขอข้อมูลอินพุต จากนั้นจะใช้ std::unique_ptr เพื่อจัดสรรหน่วยความจำที่จำเป็นสำหรับแพ็กเก็ตเอาต์พุตและทำการคำนวณ เมื่อเสร็จแล้ว จะปล่อยตัวชี้เมื่อเพิ่มลงในสตรีมเอาต์พุต

absl::Status MyCalculator::Process() {
  const Matrix& input = Input()->Get<Matrix>();
  std::unique_ptr<Matrix> output(new Matrix(input.rows(), input.cols()));
  // do your magic here....
  //    output->row(n) =  ...
  Output()->Add(output.release(), InputTimestamp());
  return absl::OkStatus();
}

ตัวเลือกเครื่องคิดเลข

เครื่องคิดเลขจะยอมรับพารามิเตอร์การประมวลผลผ่าน (1) แพ็กเก็ตสตรีมอินพุต (2) แพ็กเก็ตด้านอินพุต และ (3) ตัวเลือกเครื่องคำนวณ หากระบุไว้ ตัวเลือกเครื่องคิดเลขจะปรากฏเป็นค่าลิเทอรัลในช่อง node_options ของข้อความ CalculatorGraphConfiguration.Node

  node {
    calculator: "TfLiteInferenceCalculator"
    input_stream: "TENSORS:main_model_input"
    output_stream: "TENSORS:main_model_output"
    node_options: {
      [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] {
        model_path: "mediapipe/models/detection_model.tflite"
      }
    }
  }

ช่อง node_options ยอมรับไวยากรณ์ Proto3 หรือคุณจะระบุตัวเลือกเครื่องคำนวณในช่อง options โดยใช้ไวยากรณ์ Proto2 ก็ได้

  node {
    calculator: "TfLiteInferenceCalculator"
    input_stream: "TENSORS:main_model_input"
    output_stream: "TENSORS:main_model_output"
    node_options: {
      [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] {
        model_path: "mediapipe/models/detection_model.tflite"
      }
    }
  }

เครื่องคำนวณบางรุ่นไม่รองรับตัวเลือกเครื่องคิดเลข เพื่อยอมรับตัวเลือก ปกติแล้วเครื่องคำนวณจะกำหนดประเภทข้อความ Protobuf ใหม่เพื่อแสดงตัวเลือก เช่น PacketClonerCalculatorOptions เครื่องคำนวณจะอ่านข้อความ Protobuf นั้นในเมธอด CalculatorBase::Open และอาจอยู่ในฟังก์ชัน CalculatorBase::GetContract หรือเมธอด CalculatorBase::Process ด้วย โดยปกติแล้ว ประเภทข้อความ Protobuf ใหม่ จะได้รับการกำหนดเป็นสคีมา Protobuf โดยใช้ไฟล์ ".Proto" และ กฎบิลด์ mediapipe_proto_library()

  mediapipe_proto_library(
      name = "packet_cloner_calculator_proto",
      srcs = ["packet_cloner_calculator.proto"],
      visibility = ["//visibility:public"],
      deps = [
          "//mediapipe/framework:calculator_options_proto",
          "//mediapipe/framework:calculator_proto",
      ],
  )

ตัวอย่างเครื่องคิดเลข

ส่วนนี้จะพูดถึงการใช้งาน PacketClonerCalculator ซึ่ง ทำงานค่อนข้างง่าย และใช้ในกราฟเครื่องคิดเลขหลายกราฟ PacketClonerCalculator จะสร้างสำเนาของแพ็กเก็ตอินพุตล่าสุดตามคำขอ

PacketClonerCalculator จะมีประโยชน์เมื่อการประทับเวลาของแพ็กเก็ตข้อมูลที่มาถึงไม่ตรงกันอย่างสมบูรณ์ สมมติว่าเรามีห้องที่มีไมโครโฟน เซ็นเซอร์แสง และกล้องวิดีโอที่กำลังเก็บข้อมูลประสาทสัมผัส เซ็นเซอร์แต่ละตัวจะทำงานเป็นอิสระต่อกันและเก็บรวบรวมข้อมูลเป็นระยะๆ สมมติว่าเอาต์พุตของเซ็นเซอร์ แต่ละตัวเป็นดังนี้

  • ไมโครโฟน = ความดังในหน่วยเดซิเบลของเสียงในห้อง (จำนวนเต็ม)
  • เซ็นเซอร์แสง = ความสว่างของห้อง (จำนวนเต็ม)
  • กล้องวิดีโอ = กรอบรูป RGB ของห้อง (ImageFrame)

ไปป์ไลน์การรับรู้ที่เรียบง่ายของเราออกแบบมาเพื่อประมวลผลข้อมูลประสาทสัมผัสจากเซ็นเซอร์ 3 ตัวนี้ เมื่อใดก็ตามที่เรามีข้อมูลเฟรมภาพจากกล้องที่ซิงค์กับข้อมูลความดังของไมโครโฟนและความสว่างของเซ็นเซอร์แสงที่รวบรวมล่าสุด หากต้องการใช้ MediaPipe ไปป์ไลน์ Perception จะมีสตรีมอินพุต 3 รายการ ได้แก่

  • Room_mic_signal - ข้อมูลแต่ละแพ็กเก็ตในสตรีมอินพุตนี้เป็นข้อมูลจำนวนเต็ม ซึ่งแสดงระดับความดังในห้องที่มีการประทับเวลา
  • Room_lightening_sensor - ข้อมูลแต่ละแพ็กเก็ตในสตรีมอินพุตนี้เป็นข้อมูลจำนวนเต็ม ซึ่งแสดงความสว่างของห้องสว่างด้วยการประทับเวลา
  • Room_video_tick_signal - ข้อมูลแต่ละแพ็กเก็ตในสตรีมอินพุตนี้คือเฟรมภาพของข้อมูลวิดีโอซึ่งแสดงวิดีโอที่รวบรวมจากกล้องในห้องที่มีการประทับเวลา

ด้านล่างนี้เป็นการใช้งาน PacketClonerCalculator คุณจะเห็นเมธอด GetContract(), Open() และ Process() รวมถึงตัวแปรอินสแตนซ์ current_ ซึ่งมีแพ็กเก็ตอินพุตล่าสุด

// This takes packets from N+1 streams, A_1, A_2, ..., A_N, B.
// For every packet that appears in B, outputs the most recent packet from each
// of the A_i on a separate stream.

#include <vector>

#include "absl/strings/str_cat.h"
#include "mediapipe/framework/calculator_framework.h"

namespace mediapipe {

// For every packet received on the last stream, output the latest packet
// obtained on all other streams. Therefore, if the last stream outputs at a
// higher rate than the others, this effectively clones the packets from the
// other streams to match the last.
//
// Example config:
// node {
//   calculator: "PacketClonerCalculator"
//   input_stream: "first_base_signal"
//   input_stream: "second_base_signal"
//   input_stream: "tick_signal"
//   output_stream: "cloned_first_base_signal"
//   output_stream: "cloned_second_base_signal"
// }
//
class PacketClonerCalculator : public CalculatorBase {
 public:
  static absl::Status GetContract(CalculatorContract* cc) {
    const int tick_signal_index = cc->Inputs().NumEntries() - 1;
    // cc->Inputs().NumEntries() returns the number of input streams
    // for the PacketClonerCalculator
    for (int i = 0; i < tick_signal_index; ++i) {
      cc->Inputs().Index(i).SetAny();
      // cc->Inputs().Index(i) returns the input stream pointer by index
      cc->Outputs().Index(i).SetSameAs(&cc->Inputs().Index(i));
    }
    cc->Inputs().Index(tick_signal_index).SetAny();
    return absl::OkStatus();
  }

  absl::Status Open(CalculatorContext* cc) final {
    tick_signal_index_ = cc->Inputs().NumEntries() - 1;
    current_.resize(tick_signal_index_);
    // Pass along the header for each stream if present.
    for (int i = 0; i < tick_signal_index_; ++i) {
      if (!cc->Inputs().Index(i).Header().IsEmpty()) {
        cc->Outputs().Index(i).SetHeader(cc->Inputs().Index(i).Header());
        // Sets the output stream of index i header to be the same as
        // the header for the input stream of index i
      }
    }
    return absl::OkStatus();
  }

  absl::Status Process(CalculatorContext* cc) final {
    // Store input signals.
    for (int i = 0; i < tick_signal_index_; ++i) {
      if (!cc->Inputs().Index(i).Value().IsEmpty()) {
        current_[i] = cc->Inputs().Index(i).Value();
      }
    }

    // Output if the tick signal is non-empty.
    if (!cc->Inputs().Index(tick_signal_index_).Value().IsEmpty()) {
      for (int i = 0; i < tick_signal_index_; ++i) {
        if (!current_[i].IsEmpty()) {
          cc->Outputs().Index(i).AddPacket(
              current_[i].At(cc->InputTimestamp()));
          // Add a packet to output stream of index i a packet from inputstream i
          // with timestamp common to all present inputs
        } else {
          cc->Outputs().Index(i).SetNextTimestampBound(
              cc->InputTimestamp().NextAllowedInStream());
          // if current_[i], 1 packet buffer for input stream i is empty, we will set
          // next allowed timestamp for input stream i to be current timestamp + 1
        }
      }
    }
    return absl::OkStatus();
  }

 private:
  std::vector<Packet> current_;
  int tick_signal_index_;
};

REGISTER_CALCULATOR(PacketClonerCalculator);
}  // namespace mediapipe

โดยปกติแล้ว เครื่องคำนวณจะมีเฉพาะไฟล์ .cc เท่านั้น ไม่จำเป็นต้องมี .h เพราะ Mediapipe ใช้การลงทะเบียนเพื่อทำให้เครื่องคิดเลขรู้จัก หลังจากกำหนดคลาสเครื่องคิดเลขแล้ว ให้ลงทะเบียนด้วยการเรียกใช้มาโคร REGISTER_CALCULATOR(calculator_class_name)

ด้านล่างคือกราฟ MediaPipe ธรรมดาที่มีสตรีมอินพุต 3 สตรีม 1 โหนด (PacketClonerCalculator) และสตรีมเอาต์พุต 2 รายการ

input_stream: "room_mic_signal"
input_stream: "room_lighting_sensor"
input_stream: "room_video_tick_signal"

node {
   calculator: "PacketClonerCalculator"
   input_stream: "room_mic_signal"
   input_stream: "room_lighting_sensor"
   input_stream: "room_video_tick_signal"
   output_stream: "cloned_room_mic_signal"
   output_stream: "cloned_lighting_sensor"
 }

แผนภาพด้านล่างแสดงวิธีที่ PacketClonerCalculator กำหนดแพ็กเก็ตเอาต์พุต (ด้านล่าง) โดยอิงตามชุดแพ็กเก็ตอินพุต (ด้านบน)

เขียนกราฟโดยใช้ PacketClonerCalculator
ทุกครั้งที่ได้รับแพ็กเก็ตในสตรีมอินพุต TICK แล้ว PacketClonerCalculator จะส่งแพ็กเก็ตล่าสุดจากสตรีมอินพุตแต่ละรายการออกมา ลำดับของแพ็กเก็ตเอาต์พุต (ด้านล่าง) กำหนดโดยลำดับของแพ็กเก็ตอินพุต (ด้านบน) และการประทับเวลา การประทับเวลาจะแสดงอยู่ทางด้านขวาของแผนภาพ