آلات حاسبة

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

CalculatorBase

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

  • GetContract()
    • يمكن لمؤلفي الآلة الحاسبة تحديد الأنواع المتوقّعة من الإدخالات والمخرجات للآلة الحاسبة في GetRect(). عند تهيئة رسم بياني، أسلوبًا ثابتًا للتحقق مما إذا كانت أنواع حزم البيانات تتطابق المدخلات والمخرجات المتصلة مع المعلومات في هذه المواصفات.
  • 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() الوصول إلى حزم البيانات الجانبية للإدخال وقد يكون وكتابة المخرجات. بعد رجوع 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"
      }
    }
  }

بعض الحاسبات لا تقبل خياراتها. لقبول الخيارات، يجب ستحدد الحاسبة عادةً نوع رسالة أولية جديدة لتمثيل الخيارات، مثل PacketClonerCalculatorOptions. ستقوم الآلة الحاسبة بعد ذلك قراءة رسالة Protobuf هذه بطريقة CalculatorBase::Open، وربما أيضًا في دالتها CalculatorBase::GetContract أو طريقة CalculatorBase::Process وعادةً ما يكون نوع رسالة النموذج الأوّلي الجديد على أنه مخطط 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)

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

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 للمُخرجات. الحزم (الأسفل) استنادًا إلى سلسلة حزم الإدخال (الأعلى).

الرسم البياني باستخدام حاسبة PacketCloner
في كل مرة يتلقى فيها الحزمة حزمة في تدفق إدخال TICK، تُخرج أداة PacketClonerCalculator أحدث حزمة من كل مصدر من مصادر الإدخال. يتم تحديد تسلسل حِزم الإخراج (الأسفل) من خلال تسلسل حِزم الإدخال (الأعلى) والطوابع الزمنية لها. تظهر الطوابع الزمنية في الجهة اليسرى من الرسم البياني.