‏‫الرسوم البيانية

رسم بياني للدالة

يحدد النموذج الأوّلي CalculatorGraphConfig طوبولوجيا ووظائف الرسم البياني MediaPipe. ويمثّل كل node في الرسم البياني آلة حاسبة أو رسم بياني فرعي معيّن، كما يحدِّد الإعدادات اللازمة، مثل الآلة الحاسبة/نوع الرسم البياني الفرعي المسجّلة، والمدخلات والمخرجات والحقول الاختيارية، مثل الخيارات الخاصة بالعُقد وسياسة الإدخال والتنفيذ، والتي تمت مناقشتها في المزامنة.

يتضمن CalculatorGraphConfig عدة حقول أخرى لضبط الإعدادات العامة على مستوى الرسم البياني، مثل إعدادات منفّذ الرسم البياني، وعدد سلاسل الرسائل، والحد الأقصى لحجم قائمة الانتظار لمصادر الإدخال. هناك عدة إعدادات على مستوى الرسم البياني مفيدة لضبط أداء الرسم البياني على الأنظمة الأساسية المختلفة (على سبيل المثال، كمبيوتر سطح المكتب مقابل الأجهزة الجوّالة). على سبيل المثال، على الأجهزة الجوّالة، يمكن أن يؤدي إرفاق حاسبة كبيرة لاستنتاج النموذج إلى منفّذ منفصل إلى تحسين أداء التطبيق في الوقت الفعلي لأنّ ذلك يتيح المنطقة المحلية للسلاسل.

في ما يلي مثال بسيط على CalculatorGraphConfig حيث لدينا سلسلة من آلات حساب التمرير :

# This graph named main_pass_throughcals_nosubgraph.pbtxt contains 4
# passthrough calculators.
input_stream: "in"
output_stream: "out"
node {
    calculator: "PassThroughCalculator"
    input_stream: "in"
    output_stream: "out1"
}
node {
    calculator: "PassThroughCalculator"
    input_stream: "out1"
    output_stream: "out2"
}
node {
    calculator: "PassThroughCalculator"
    input_stream: "out2"
    output_stream: "out3"
}
node {
    calculator: "PassThroughCalculator"
    input_stream: "out3"
    output_stream: "out"
}

توفّر MediaPipe تمثيل C++ بديلاً للرسومات البيانية المعقّدة (مثل مسارات تعلُّم الآلة ومعالجة البيانات الوصفية للنموذج والعُقد الاختيارية وما إلى ذلك). قد يبدو الرسم البياني أعلاه كما يلي:

CalculatorGraphConfig BuildGraphConfig() {
  Graph graph;

  // Graph inputs
  Stream<AnyType> in = graph.In(0).SetName("in");

  auto pass_through_fn = [](Stream<AnyType> in,
                            Graph& graph) -> Stream<AnyType> {
    auto& node = graph.AddNode("PassThroughCalculator");
    in.ConnectTo(node.In(0));
    return node.Out(0);
  };

  Stream<AnyType> out1 = pass_through_fn(in, graph);
  Stream<AnyType> out2 = pass_through_fn(out1, graph);
  Stream<AnyType> out3 = pass_through_fn(out2, graph);
  Stream<AnyType> out4 = pass_through_fn(out3, graph);

  // Graph outputs
  out4.SetName("out").ConnectTo(graph.Out(0));

  return graph.GetConfig();
}

اطّلع على مزيد من التفاصيل في إنشاء الرسوم البيانية في C++.

إشارة فرعية

لتقسيم CalculatorGraphConfig إلى وحدات فرعية والمساعدة في إعادة استخدام حلول التصوير، يمكن تعريف رسم MediaPipe البياني على أنّه Subgraph. تتكون الواجهة العامة للرسم البياني الفرعي من مجموعة من تدفقات المدخلات والمخرجات التي تشبه الواجهة العامة للآلة الحاسبة. ويمكن بعد ذلك تضمين الرسم البياني الفرعي في CalculatorGraphConfig كما لو كانت آلة حاسبة. عند تحميل رسم MediaPipe من CalculatorGraphConfig، يتم استبدال كل عُقدة رسم بياني فرعي بالرسم البياني المقابل للآلات الحاسبة. ونتيجة لذلك، فإن دلالات وأداء الرسم البياني الفرعي متطابقان مع الرسم البياني المقابل للآلات الحاسبة.

فيما يلي مثال على كيفية إنشاء رسم بياني فرعي يسمى TwoPassThroughSubgraph.

  1. تعريف الرسم البياني الفرعي.

    # This subgraph is defined in two_pass_through_subgraph.pbtxt
    # and is registered as "TwoPassThroughSubgraph"
    
    type: "TwoPassThroughSubgraph"
    input_stream: "out1"
    output_stream: "out3"
    
    node {
        calculator: "PassThroughCalculator"
        input_stream: "out1"
        output_stream: "out2"
    }
    node {
        calculator: "PassThroughCalculator"
        input_stream: "out2"
        output_stream: "out3"
    }
    

    تتكون الواجهة العامة للرسم البياني الفرعي مما يلي:

    • إنشاء رسم بياني لمصادر الإدخال
    • إنشاء رسم بياني لمجموعات الإخراج
    • الرسم البياني لحِزم البيانات الجانبية لإدخالها
    • إنشاء رسم بياني لحِزم الإخراج الجانبية
  2. سجّل الرسم البياني الفرعي باستخدام قاعدة الإنشاء mediapipe_simple_subgraph. تحدد المعلمة register_as اسم المكون للرسم البياني الفرعي الجديد.

    # Small section of BUILD file for registering the "TwoPassThroughSubgraph"
    # subgraph for use by main graph main_pass_throughcals.pbtxt
    
    mediapipe_simple_subgraph(
        name = "twopassthrough_subgraph",
        graph = "twopassthrough_subgraph.pbtxt",
        register_as = "TwoPassThroughSubgraph",
        deps = [
                "//mediapipe/calculators/core:pass_through_calculator",
                "//mediapipe/framework:calculator_graph",
        ],
    )
    
  3. استخدِم الرسم البياني الفرعي في الرسم البياني الرئيسي.

    # This main graph is defined in main_pass_throughcals.pbtxt
    # using subgraph called "TwoPassThroughSubgraph"
    
    input_stream: "in"
    node {
        calculator: "PassThroughCalculator"
        input_stream: "in"
        output_stream: "out1"
    }
    node {
        calculator: "TwoPassThroughSubgraph"
        input_stream: "out1"
        output_stream: "out3"
    }
    node {
        calculator: "PassThroughCalculator"
        input_stream: "out3"
        output_stream: "out4"
    }
    

خيارات الرسم البياني

من الممكن تحديد نموذج أوّلي لـ "خيارات الرسم البياني" للرسم البياني MediaPipe على غرار النموذج الأوّلي لـ Calculator Options المحدد لآلة حاسبة MediaPipe. يمكن تحديد "خيارات الرسم البياني" هذه في المكان الذي يتم فيه استدعاء الرسم البياني، واستخدامها لتعبئة خيارات الآلة الحاسبة وخيارات الرسم البياني الفرعية داخل الرسم البياني.

في CalculatorGraphConfig، يمكن تحديد خيارات الرسم البياني للرسم البياني الفرعي تمامًا مثل خيارات الآلة الحاسبة، كما هو موضّح أدناه:

node {
  calculator: "FlowLimiterCalculator"
  input_stream: "image"
  output_stream: "throttled_image"
  node_options: {
    [type.googleapis.com/mediapipe.FlowLimiterCalculatorOptions] {
      max_in_flight: 1
    }
  }
}

node {
  calculator: "FaceDetectionSubgraph"
  input_stream: "IMAGE:throttled_image"
  node_options: {
    [type.googleapis.com/mediapipe.FaceDetectionOptions] {
      tensor_width: 192
      tensor_height: 192
    }
  }
}

في CalculatorGraphConfig، يمكن قبول خيارات الرسم البياني واستخدامها لتعبئة خيارات الآلة الحاسبة، كما هو موضّح أدناه:

graph_options: {
  [type.googleapis.com/mediapipe.FaceDetectionOptions] {}
}

node: {
  calculator: "ImageToTensorCalculator"
  input_stream: "IMAGE:image"
  node_options: {
    [type.googleapis.com/mediapipe.ImageToTensorCalculatorOptions] {
        keep_aspect_ratio: true
        border_mode: BORDER_ZERO
    }
  }
  option_value: "output_tensor_width:options/tensor_width"
  option_value: "output_tensor_height:options/tensor_height"
}

node {
  calculator: "InferenceCalculator"
  node_options: {
    [type.googleapis.com/mediapipe.InferenceCalculatorOptions] {}
  }
  option_value: "delegate:options/delegate"
  option_value: "model_path:options/model_path"
}

في هذا المثال، يقبل FaceDetectionSubgraph النموذج الأوّلي لخيار الرسم البياني FaceDetectionOptions. يتم استخدام FaceDetectionOptions لتحديد بعض قيم الحقول في خيارات الآلة الحاسبة ImageToTensorCalculatorOptions وبعض قيم الحقول في خيارات الرسم البياني الفرعي InferenceCalculatorOptions. يتم تحديد قيم الحقول باستخدام بنية option_value:.

في النموذج الأوّلي CalculatorGraphConfig::Node، يحدّد الحقلان node_options: وoption_value: معًا قيم الخيارات للآلة الحاسبة مثل ImageToTensorCalculator. يحدد الحقل node_options: مجموعة من القيم الثابتة الحرفية باستخدام بنية النص الأوّلي. يحدد كل حقل option_value: قيمة حقل نموذج أولي واحد باستخدام معلومات من الرسم البياني للإرفاق، وتحديدًا من قيم الحقول لخيارات الرسم البياني للإرفاق. في المثال أعلاه، تُحدد السمة option_value: "output_tensor_width:options/tensor_width" الحقل ImageToTensorCalculatorOptions.output_tensor_width باستخدام القيمة FaceDetectionOptions.tensor_width.

تعتبر بنية option_value: مشابهة لبنية input_stream:. بناء الجملة هو option_value: "LHS:RHS". تحدد LHS حقل الآلة الحاسبة وتحدد RHS حقل خيار الرسم البياني. وبشكل أكثر تحديدًا، يتكون كل من LHS وRHS من سلسلة من أسماء حقول النموذج الأوّلي التي تحدد رسائل Protobuf المتداخلة والحقول المفصولة بـ "/". وهذا ما يُعرف باسم بناء الجملة "ProtoPath". يجب تحديد الرسائل المتداخلة التي تمت الإشارة إليها في LHS أو RHS من قبل في النموذج الأولي للتضمين حتى يتم اجتيازها باستخدام option_value:.

فترات زمنية

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

ملاحظة: إنّ الأسلوب الحالي تجريبي وخاضع للتغيير. نرحب بملاحظاتك.

يُرجى استخدام اختبار الوحدات CalculatorGraphTest.Cycle في mediapipe/framework/calculator_graph_test.cc كرمز نموذجي. يظهر أدناه الرسم البياني الدوري في الاختبار. ناتج sum في الإضافة هو مجموع الأعداد الصحيحة التي يتم إنشاؤها بواسطة الآلة الحاسبة لمصدر الأعداد الصحيحة.

رسم بياني دائري يضيف مجموعة من الأعداد الصحيحة

يوضح هذا الرسم البياني البسيط جميع المشكلات في دعم الرسوم البيانية الدورية.

التعليق التوضيحي على الحافة الخلفية

نشترط إضافة تعليق توضيحي على حافة في كل دورة كحافة خلفية. يسمح ذلك باستخدام الترتيب الطوبولوجي لـ MediaPipe، بعد إزالة جميع الحواف الخلفية.

هناك عادةً عدة طرق لتحديد الحواف الخلفية. الحواف المميزة على أنها حواف خلفية تؤثر على العُقد التي تعتبر من الأعلى والعُقد التي تعتبر حواف، ما يؤثر بدوره على الأولويات التي يعيّنها MediaPipe للعُقد.

على سبيل المثال، يحدد الاختبار CalculatorGraphTest.Cycle الحافة old_sum على أنّها حافة خلفية، وبالتالي تُعتبر عقدة التأخير عقدة متأخّرة من عقدة الوصلة، ويتم منحها أولوية أعلى. بدلاً من ذلك، يمكننا وضع علامة على الإدخال sum في عقدة التأخير على أنّه حافة خلفية، وفي هذه الحالة ستُعتبر عقدة التأخير عقدة أعلى لعقدة الإضافة وسيتم منحها أولوية أقل.

الحزمة الأولية

لإتاحة إمكانية تشغيل الآلة الحاسبة للإضافة عند وصول أول عدد صحيح من مصدر العدد الصحيح، نحتاج إلى حزمة أولية بالقيمة 0 والطابع الزمني نفسه في تدفق إدخال old_sum إلى الإضافة. يجب إخراج هذه الحزمة الأولية من خلال حاسبة التأخير في طريقة Open().

تأخير في حلقة التكرار

يجب أن تؤدي كل حلقة التكرار إلى تأخير لمحاذاة إخراج sum السابق مع إدخال العدد الصحيح التالي. يتم ذلك أيضًا بواسطة عقدة التأخير. لذا، تحتاج عقدة التأخير إلى معرفة ما يلي حول الطوابع الزمنية لحاسبة مصدر الأعداد الصحيحة:

  • الطابع الزمني للناتج الأول.

  • يشير ذلك المصطلح إلى دلتا الطابع الزمني بين المخرجات المتتالية.

نحن نخطط لإضافة سياسة جدولة بديلة تهتم فقط بترتيب حزم البيانات وتتجاهل الطوابع الزمنية للحزم، ما سيجنبك هذا الإزعاج.

الإنهاء المبكر للآلة الحاسبة عند اكتمال بث إدخال واحد

يستدعي MediaPipe تلقائيًا طريقة Close() للآلة الحاسبة غير المصدر عند انتهاء جميع عمليات بث الإدخال. في مثال الرسم البياني، نريد إيقاف العقدة الإضافية بمجرد الانتهاء من مصدر العدد الصحيح. ويمكن تحقيق ذلك من خلال ضبط عقدة الإضافة باستخدام معالج بث إدخال بديل، EarlyCloseInputStreamHandler.

رمز المصدر ذو الصلة

حاسبة التأخير

لاحِظ الرمز في Open() الذي يؤدي إلى إخراج الحزمة الأولية والرمز في Process() الذي يضيف تأخيرًا (وحدة) إلى حِزم الإدخال. كما هو موضّح أعلاه، تفترض عقدة التأخير هذه أنّه يتم استخدام بث الإخراج الخاص بها إلى جانب مجموعة بث ذات الطوابع الزمنية للحزمة 0، 1، 2، 3، ...

class UnitDelayCalculator : public Calculator {
 public:
  static absl::Status FillExpectations(
      const CalculatorOptions& extendable_options, PacketTypeSet* inputs,
      PacketTypeSet* outputs, PacketTypeSet* input_side_packets) {
    inputs->Index(0)->Set<int>("An integer.");
    outputs->Index(0)->Set<int>("The input delayed by one time unit.");
    return absl::OkStatus();
  }

  absl::Status Open() final {
    Output()->Add(new int(0), Timestamp(0));
    return absl::OkStatus();
  }

  absl::Status Process() final {
    const Packet& packet = Input()->Value();
    Output()->AddPacket(packet.At(packet.Timestamp().NextAllowedInStream()));
    return absl::OkStatus();
  }
};

إعداد الرسم البياني

عليك مراعاة التعليق التوضيحي back_edge والنوع input_stream_handler البديل.

node {
  calculator: 'GlobalCountSourceCalculator'
  input_side_packet: 'global_counter'
  output_stream: 'integers'
}
node {
  calculator: 'IntAdderCalculator'
  input_stream: 'integers'
  input_stream: 'old_sum'
  input_stream_info: {
    tag_index: ':1'  # 'old_sum'
    back_edge: true
  }
  output_stream: 'sum'
  input_stream_handler {
    input_stream_handler: 'EarlyCloseInputStreamHandler'
  }
}
node {
  calculator: 'UnitDelayCalculator'
  input_stream: 'sum'
  output_stream: 'old_sum'
}