آلات حاسبة

كل حاسبة هي جزء من الرسم البياني. نوضح كيفية إنشاء حاسبة جديدة، وكيفية إعداد حاسبة، وكيفية إجراء العمليات الحسابية، ومصادر الإدخال والإخراج، والطوابع الزمنية والخيارات. يتم تنفيذ كل عقدة في الرسم البياني على أنّها Calculator. الجزء الأكبر من تنفيذ الرسم البياني يحدث داخل الحاسبات الخاصة به. قد لا تتلقى الآلة الحاسبة أي مصادر إدخال و/أو حزم جانبية أو أكثر، ولا ينتج عنها أي عمليات بث للمخرجات و/أو حِزم جانبية أو أكثر.

CalculatorBase

يتم إنشاء الآلة الحاسبة من خلال تحديد فئة فرعية جديدة من الفئة CalculatorBase، وتنفيذ عدد من الطرق، وتسجيل الفئة الفرعية الجديدة باستخدام MediaMedia. كحد أدنى، يجب أن تنفذ الآلة الحاسبة الجديدة الطرق الأربع التالية

  • 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(). يبدو أن الإدخال الذي تفتقد حزمته ينتج حزمة فارغة (بدون طابع زمني).

يستدعي إطار العمل Close() بعد كل عمليات الاستدعاء إلى Process(). سيتم استنفاد جميع الإدخالات، ولكن بإمكان "Close()" الوصول إلى حِزم البيانات الجانبية للإدخال، ويمكنه كتابة مخرجات. بعد إغلاق الحساب، سيتم التخلص من الآلة الحاسبة.

يُشار إلى الحاسبات التي لا تحتوي على مُدخلات باسم المصادر. تستمر حاسبة المصدر في طلب Process() طالما أنّها تعرض الحالة Ok. وتشير أداة حساب المصدر إلى نفاده من خلال عرض حالة توقف (أي mediaPipe::tool::StatusStop().).

تحديد المدخلات والمخرجات

تتكون الواجهة العامة للآلة الحاسبة من مجموعة من ساحات مشاركات الإدخال وتدفقات الإخراج. في الآلة الحاسبة الرسومية، يتم ربط مخرجات بعض الحاسبات بمدخلات الحاسبات الأخرى باستخدام التدفقات المسماة. عادةً ما تكون أسماء ساحات المشاركات بأحرف صغيرة، بينما تكون علامات الإدخال والإخراج أحرف كبيرة عادةً. في المثال أدناه، يتم ربط الناتج الذي يحمل اسم العلامة 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"
      }
    }
  }

لا تقبل كل الحاسبات خيارات الآلة الحاسبة. لقبول الخيارات، ستحدِّد الآلة الحاسبة عادةً نوعًا جديدًا من رسالة النموذج الأوّلي لتمثيل خياراته، مثل PacketClonerCalculatorOptions. بعد ذلك، تقرأ الآلة الحاسبة رسالة البروتوكول الأوّلي بطريقة 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 مفيدًا عندما لا تتم محاذاة الطوابع الزمنية لحزم البيانات عند الوصول بشكل مثالي. لنفترض أن لدينا غرفة بها ميكروفون وجهاز استشعار للضوء وكاميرا فيديو تجمع البيانات الحسية. يعمل كل جهاز من أدوات الاستشعار بشكل مستقل ويجمع البيانات بشكل متقطع. افترض أن ناتج كل أداة استشعار هو:

  • الميكروفون = ارتفاع الصوت بالديسيبل في الغرفة (عدد صحيح)
  • أداة استشعار الضوء = سطوع الغرفة (عدد صحيح)
  • كاميرا الفيديو = إطار صورة بنموذج أحمر أخضر أزرق (ImageFrame)

صُمم مسار التصور البسيط الخاص بنا لمعالجة البيانات الحسية من أدوات الاستشعار الثلاثة هذه في أي وقت عندما تكون لدينا بيانات إطار صورة من الكاميرا تتم مزامنتها مع آخر بيانات تم جمعها من ارتفاع صوت الميكروفون وبيانات سطوع جهاز استشعار الضوء. للقيام بذلك باستخدام 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، لأن Mediapeيق يستخدم التسجيل لجعل الحاسبات معروفة له. بعد تحديد فئة الآلة الحاسبة، سجّلها باستخدام استدعاء ماكرو REGISTER_CALCULATOR(Parameter_class_name).

يوجد أدناه رسم بياني بسيط لـ MediaPipe يحتوي على 3 ساحات مشاركات للإدخال، وعقدة واحدة (PacketCloner Cash) و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، تُخرج أداة الحزم الحديثة أحدث حزمة من كل مصدر بيانات للإدخال. ويتم تحديد تسلسل حِزم الإخراج (أسفل) حسب تسلسل حِزم الإدخال (في الجزء العلوي) والطوابع الزمنية لها. تظهر الطوابع الزمنية على الجانب الأيسر من المخطّط البياني.