ক্যালকুলেটর

প্রতিটি ক্যালকুলেটর একটি গ্রাফের একটি নোড। আমরা কীভাবে একটি নতুন ক্যালকুলেটর তৈরি করতে হয়, কীভাবে একটি ক্যালকুলেটর শুরু করতে হয়, কীভাবে এটির গণনাগুলি সম্পাদন করতে হয়, ইনপুট এবং আউটপুট স্ট্রীম, টাইমস্ট্যাম্প এবং বিকল্পগুলি বর্ণনা করি। গ্রাফের প্রতিটি নোড একটি Calculator হিসাবে প্রয়োগ করা হয়। গ্রাফ এক্সিকিউশনের বেশিরভাগই এর ক্যালকুলেটরগুলির মধ্যে ঘটে। একটি ক্যালকুলেটর শূন্য বা তার বেশি ইনপুট স্ট্রীম এবং/অথবা সাইড প্যাকেট পেতে পারে এবং শূন্য বা তার বেশি আউটপুট স্ট্রীম এবং/অথবা সাইড প্যাকেট তৈরি করে।

ক্যালকুলেটর বেস

একটি ক্যালকুলেটর তৈরি করা হয় CalculatorBase ক্লাসের একটি নতুন সাব-ক্লাস সংজ্ঞায়িত করে, বেশ কয়েকটি পদ্ধতি প্রয়োগ করে এবং মিডিয়াপাইপের সাথে নতুন সাব-ক্লাস নিবন্ধন করে। ন্যূনতম, একটি নতুন ক্যালকুলেটরকে অবশ্যই নিচের চারটি পদ্ধতি প্রয়োগ করতে হবে

  • GetContract()
    • ক্যালকুলেটর লেখকরা GetContract() এ ক্যালকুলেটরের প্রত্যাশিত ধরনের ইনপুট এবং আউটপুট নির্দিষ্ট করতে পারেন। যখন একটি গ্রাফ শুরু করা হয়, তখন ফ্রেমওয়ার্কটি একটি স্ট্যাটিক পদ্ধতিতে কল করে যা এই স্পেসিফিকেশনের তথ্যের সাথে সংযুক্ত ইনপুট এবং আউটপুটগুলির প্যাকেটের প্রকারগুলি মেলে কিনা তা যাচাই করতে।
  • Open()
    • একটি গ্রাফ শুরু হওয়ার পরে, ফ্রেমওয়ার্কটি Open() কল করে। ইনপুট সাইড প্যাকেটগুলি এই সময়ে ক্যালকুলেটরের কাছে উপলব্ধ। Open() নোড কনফিগারেশন ক্রিয়াকলাপ ব্যাখ্যা করে ( গ্রাফ দেখুন) এবং ক্যালকুলেটরের প্রতি-গ্রাফ-চালিত অবস্থা প্রস্তুত করে। এই ফাংশনটি ক্যালকুলেটর আউটপুটগুলিতে প্যাকেটগুলিও লিখতে পারে। Open() চলাকালীন একটি ত্রুটি গ্রাফ রান বন্ধ করতে পারে।
  • Process()
    • ইনপুট সহ একটি ক্যালকুলেটরের জন্য, যখনই অন্তত একটি ইনপুট স্ট্রীমে একটি প্যাকেট উপলব্ধ থাকে তখনই ফ্রেমওয়ার্ক Process() বারবার কল করে। ডিফল্টরূপে ফ্রেমওয়ার্ক গ্যারান্টি দেয় যে সমস্ত ইনপুট একই টাইমস্ট্যাম্প রয়েছে (আরো তথ্যের জন্য সিঙ্ক্রোনাইজেশন দেখুন)। সমান্তরাল এক্সিকিউশন চালু থাকলে একাধিক 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() স্ট্যাটিক পদ্ধতিকে কল করে যা নির্ধারণ করতে কি ধরনের প্যাকেট প্রত্যাশিত।

ফ্রেমওয়ার্ক প্রতিটি গ্রাফ রানের জন্য সম্পূর্ণ ক্যালকুলেটর তৈরি করে এবং ধ্বংস করে (যেমন প্রতি ভিডিওতে একবার বা প্রতি ছবিতে একবার)। ব্যয়বহুল বা বড় বস্তু যা গ্রাফ রান জুড়ে স্থির থাকে ইনপুট সাইড প্যাকেট হিসাবে সরবরাহ করা উচিত যাতে পরবর্তী রানগুলিতে গণনা পুনরাবৃত্তি না হয়।

আরম্ভ করার পরে, গ্রাফের প্রতিটি রানের জন্য, নিম্নলিখিত ক্রমটি ঘটে:

  • Open()
  • Process() (বারবার)
  • Close()

ফ্রেমওয়ার্ক ক্যালকুলেটর আরম্ভ করার জন্য Open() কল করে। Open() যেকোন বিকল্পের ব্যাখ্যা করবে এবং ক্যালকুলেটরের প্রতি-গ্রাফ-চালিত অবস্থা সেট আপ করবে। Open() ইনপুট সাইড প্যাকেট পেতে পারে এবং ক্যালকুলেটর আউটপুটে প্যাকেট লিখতে পারে। উপযুক্ত হলে, ইনপুট স্ট্রীমের সম্ভাব্য প্যাকেট বাফারিং কমাতে SetOffset() কল করা উচিত।

যদি Open() বা Process() এর সময় একটি ত্রুটি ঘটে (যেমন তাদের মধ্যে একটি অ- Ok স্ট্যাটাস ফিরিয়ে দেয়), গ্রাফ রানটি ক্যালকুলেটরের পদ্ধতিতে আর কোন কল না করেই বন্ধ হয়ে যায় এবং ক্যালকুলেটরটি ধ্বংস হয়ে যায়।

ইনপুট সহ একটি ক্যালকুলেটরের জন্য, যখনই অন্তত একটি ইনপুটে একটি প্যাকেট উপলব্ধ থাকে তখনই ফ্রেমওয়ার্কটি Process() কল করে। ফ্রেমওয়ার্ক গ্যারান্টি দেয় যে ইনপুটগুলির সকলেরই একই টাইমস্ট্যাম্প থাকে, যে টাইমস্ট্যাম্পগুলি Process() এ প্রতিটি কলের সাথে বৃদ্ধি পায় এবং সমস্ত প্যাকেট বিতরণ করা হয়। ফলস্বরূপ, কিছু ইনপুটের কোনো প্যাকেট নাও থাকতে পারে যখন Process() বলা হয়। একটি ইনপুট যার প্যাকেট অনুপস্থিত একটি খালি প্যাকেট (কোন টাইমস্ট্যাম্প ছাড়াই) তৈরি করতে দেখা যাচ্ছে।

ফ্রেমওয়ার্কটি Process() এ সমস্ত কল করার পরে Close() কল করে। সমস্ত ইনপুট শেষ হয়ে যাবে, কিন্তু Close() ইনপুট সাইড প্যাকেটগুলিতে অ্যাক্সেস রয়েছে এবং আউটপুট লিখতে পারে। ক্লোজ রিটার্ন করার পরে, ক্যালকুলেটরটি নষ্ট হয়ে যায়।

কোনো ইনপুট ছাড়া ক্যালকুলেটরকে উৎস হিসেবে উল্লেখ করা হয়। একটি সোর্স ক্যালকুলেটর যতক্ষণ পর্যন্ত এটি একটি Ok স্ট্যাটাস প্রদান করে ততক্ষণ পর্যন্ত Process() কল করা থাকে। একটি উৎস ক্যালকুলেটর নির্দেশ করে যে এটি একটি স্টপ স্ট্যাটাস (যেমন mediaPipe::tool::StatusStop() .) ফেরত দিয়ে নিঃশেষ হয়ে গেছে।

ইনপুট এবং আউটপুট সনাক্তকরণ

একটি ক্যালকুলেটরের সর্বজনীন ইন্টারফেস ইনপুট স্ট্রীম এবং আউটপুট স্ট্রিমগুলির একটি সেট নিয়ে গঠিত। একটি ক্যালকুলেটর গ্রাফ কনফিগারেশনে, কিছু ক্যালকুলেটর থেকে আউটপুট নামযুক্ত স্ট্রীম ব্যবহার করে অন্যান্য ক্যালকুলেটরের ইনপুটগুলির সাথে সংযুক্ত থাকে। স্ট্রীমের নামগুলি সাধারণত ছোট হাতের হয়, যখন ইনপুট এবং আউটপুট ট্যাগগুলি সাধারণত বড় হাতের হয়৷ নীচের উদাহরণে, VIDEO ট্যাগ নামের আউটপুটটি video_stream নামের স্ট্রীমটি ব্যবহার করে VIDEO_IN ট্যাগ নামের ইনপুটের সাথে সংযুক্ত।

# 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 দ্বারা চিহ্নিত করা হয়।
  • ট্যাগ নাম দ্বারা: ভিডিও আউটপুট স্ট্রীম ট্যাগ নাম "ভিডিও" দ্বারা চিহ্নিত করা হয়.
  • ট্যাগ নাম এবং সূচক নম্বর দ্বারা: আউটপুট অডিও স্ট্রীমগুলি ট্যাগ নাম 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() ফেরত দেয়, তাহলে এটি ইঙ্গিত দেয় যে গ্রাফটি তাড়াতাড়ি বাতিল করা হচ্ছে। এই ক্ষেত্রে, সমস্ত উৎস ক্যালকুলেটর এবং গ্রাফ ইনপুট স্ট্রীম বন্ধ হয়ে যাবে (এবং অবশিষ্ট প্যাকেটগুলি গ্রাফের মাধ্যমে প্রচার করবে)।

একটি গ্রাফের একটি উত্স নোড যতক্ষণ পর্যন্ত এটি absl::OkStatus( ) ফেরত দেয় ততক্ষণ পর্যন্ত এটিকে Process() কল করতে থাকবে। ইঙ্গিত করার জন্য যে রিটার্ন 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) ক্যালকুলেটর বিকল্পগুলির মাধ্যমে প্রক্রিয়াকরণের পরামিতিগুলি গ্রহণ করে। ক্যালকুলেটর বিকল্পগুলি, নির্দিষ্ট করা থাকলে, CalculatorGraphConfiguration.Node নোড বার্তার node_options ক্ষেত্রে আক্ষরিক মান হিসাবে উপস্থিত হয়।

  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 সিনট্যাক্স গ্রহণ করে। বিকল্পভাবে, প্রোটো2 সিনট্যাক্স ব্যবহার করে options ক্ষেত্রে ক্যালকুলেটর বিকল্পগুলি নির্দিষ্ট করা যেতে পারে।

  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"
      }
    }
  }

সমস্ত ক্যালকুলেটর ক্যালকুলেটর বিকল্পগুলি গ্রহণ করে না। বিকল্পগুলি গ্রহণ করার জন্য, একটি ক্যালকুলেটর সাধারণত একটি নতুন প্রোটোবাফ বার্তার ধরন সংজ্ঞায়িত করে তার বিকল্পগুলিকে উপস্থাপন করতে, যেমন PacketClonerCalculatorOptions । ক্যালকুলেটর তারপর সেই প্রোটোবাফ বার্তাটি তার CalculatorBase::Open পদ্ধতিতে এবং সম্ভবত এটির CalculatorBase::GetContract ফাংশন বা এর CalculatorBase::Process পদ্ধতিতে পড়বে। সাধারণত, নতুন protobuf বার্তার ধরনটিকে একটি ".proto" ফাইল এবং একটি mediapipe_proto_library() বিল্ড নিয়ম ব্যবহার করে একটি protobuf স্কিমা হিসাবে সংজ্ঞায়িত করা হবে।

  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 উপযোগী হয় যখন আগত ডেটা প্যাকেটের টাইমস্ট্যাম্পগুলি পুরোপুরি সারিবদ্ধ না হয়। ধরুন আমাদের কাছে একটি মাইক্রোফোন, লাইট সেন্সর এবং একটি ভিডিও ক্যামেরা আছে যা সংবেদনশীল তথ্য সংগ্রহ করছে। প্রতিটি সেন্সর স্বাধীনভাবে কাজ করে এবং মাঝে মাঝে ডেটা সংগ্রহ করে। ধরুন প্রতিটি সেন্সরের আউটপুট হল:

  • মাইক্রোফোন = ঘরে শব্দের ডেসিবেলের উচ্চতা (পূর্ণসংখ্যা)
  • আলো সেন্সর = ঘরের উজ্জ্বলতা (পূর্ণসংখ্যা)
  • ভিডিও ক্যামেরা = ঘরের আরজিবি ছবির ফ্রেম (ইমেজ ফ্রেম)

আমাদের সাধারণ উপলব্ধি পাইপলাইনটি এই 3টি সেন্সর থেকে সংবেদনশীল ডেটা প্রক্রিয়া করার জন্য ডিজাইন করা হয়েছে যাতে যে কোনও সময় যখন আমাদের কাছে ক্যামেরা থেকে ইমেজ ফ্রেম ডেটা থাকে যা শেষ সংগৃহীত মাইক্রোফোন লাউডনেস ডেটা এবং লাইট সেন্সর উজ্জ্বলতা ডেটার সাথে সিঙ্ক্রোনাইজ করা হয়। MediaPipe এর সাথে এটি করতে, আমাদের উপলব্ধি পাইপলাইনে 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 এর প্রয়োজন নেই, কারণ মিডিয়াপাইপ রেজিস্ট্রেশন ব্যবহার করে ক্যালকুলেটরদের পরিচিত করতে। আপনি আপনার ক্যালকুলেটর ক্লাস সংজ্ঞায়িত করার পরে, এটি একটি ম্যাক্রো ইনভোকেশন 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 ইনপুট স্ট্রীমে একটি প্যাকেট পায়, প্যাকেটক্লোনার ক্যালকুলেটর তার প্রতিটি ইনপুট স্ট্রীম থেকে সাম্প্রতিক প্যাকেটটি আউটপুট করে। আউটপুট প্যাকেটের ক্রম (নীচে) ইনপুট প্যাকেট (শীর্ষ) এবং তাদের টাইমস্ট্যাম্পগুলির ক্রম দ্বারা নির্ধারিত হয়। টাইমস্ট্যাম্পগুলি ডায়াগ্রামের ডান পাশে দেখানো হয়েছে।