เครื่องคำนวณแต่ละแบบคือโหนดของกราฟ เราได้อธิบายวิธีสร้างเครื่องคิดเลขใหม่ วิธีเริ่มต้นเครื่องคำนวณ วิธีคำนวณ สตรีมอินพุตและเอาต์พุต การประทับเวลา และตัวเลือกต่างๆ แต่ละโหนดในกราฟ
จะมีการใช้งานเป็น 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
กำหนดแพ็กเก็ตเอาต์พุต (ด้านล่าง) โดยอิงตามชุดแพ็กเก็ตอินพุต (ด้านบน)
ทุกครั้งที่ได้รับแพ็กเก็ตในสตรีมอินพุต TICK แล้ว PacketClonerCalculator จะส่งแพ็กเก็ตล่าสุดจากสตรีมอินพุตแต่ละรายการออกมา ลำดับของแพ็กเก็ตเอาต์พุต (ด้านล่าง) กำหนดโดยลำดับของแพ็กเก็ตอินพุต (ด้านบน) และการประทับเวลา การประทับเวลาจะแสดงอยู่ทางด้านขวาของแผนภาพ |