Grafiken

Grafik

Ein CalculatorGraphConfig-Proto gibt die Topologie und Funktionalität einer MediaPipe-Grafik an. Jede node in der Grafik stellt einen bestimmten Rechner oder Teildiagramm dar und gibt erforderliche Konfigurationen wie den registrierten Taschenrechner-/Teilgrafiktyp, Eingaben, Ausgaben und optionale Felder wie knotenspezifische Optionen, Eingaberichtlinie und Executor an, wie unter Synchronisierung erläutert.

CalculatorGraphConfig enthält einige weitere Felder zum Konfigurieren globaler Einstellungen auf Grafikebene, z.B. Konfigurationen für Executor-Grafiken, Anzahl der Threads und maximale Warteschlangengröße von Eingabestreams. Verschiedene Einstellungen auf Grafikebene sind nützlich, um die Leistung der Grafik auf verschiedenen Plattformen (z. B. Computer und Mobilgeräte) zu optimieren. Auf Mobilgeräten kann beispielsweise die Leistung einer Echtzeitanwendung verbessert werden, wenn ein separater Executor mit einem Rechner für hohe Modellinferenzen verbunden ist, da dies den Thread-Lokalität ermöglicht.

Unten sehen Sie ein einfaches CalculatorGraphConfig-Beispiel mit einer Reihe von Durchlaufrechnern :

# 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 bietet eine alternative C++-Darstellung für komplexe Grafiken (z.B. ML-Pipelines, Verarbeitung von Modellmetadaten, optionale Knoten usw.). Das Diagramm oben könnte so aussehen:

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();
}

Weitere Informationen finden Sie unter Diagramme in C++ erstellen.

Teilgrafik

Um ein CalculatorGraphConfig in Untermodule zu modularisieren und die Wiederverwendung von Wahrnehmungslösungen zu unterstützen, kann eine MediaPipe-Grafik als Subgraph definiert werden. Die öffentliche Schnittstelle einer Teilgrafik besteht aus einer Reihe von Eingabe- und Ausgabestreams, ähnlich wie bei der öffentlichen Schnittstelle eines Taschenrechners. Die Teilgrafik kann dann so in eine CalculatorGraphConfig aufgenommen werden, als wäre es ein Taschenrechner. Wenn eine MediaPipe-Grafik aus einem CalculatorGraphConfig geladen wird, wird jeder Teilgrafikknoten durch die entsprechende Grafik von Rechnern ersetzt. Daher sind die Semantik und Leistung der Teilgrafik mit der entsprechenden Grafik von Taschenrechnern identisch.

Im Folgenden finden Sie ein Beispiel für die Erstellung einer Teilgrafik mit dem Namen TwoPassThroughSubgraph.

  1. Teilgrafik definieren

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

    Die öffentliche Schnittstelle zum Teildiagramm besteht aus:

    • Grafikeingabestreams
    • Ausgabestreams grafisch darstellen
    • Pakete der Eingabeseite grafisch darstellen
    • Pakete der Ausgabeseite grafisch darstellen
  2. Registrieren Sie die Teilgrafik mit der build-Regel mediapipe_simple_subgraph. Der Parameter register_as definiert den Komponentennamen für die neue Teilgrafik.

    # 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. Verwenden Sie die Teilgrafik in der Hauptgrafik.

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

Grafikoptionen

Es ist möglich, einen Protokollpuffer vom Typ „Grafikoptionen“ für eine MediaPipe-Grafik anzugeben, ähnlich wie beim Calculator Options-Protokollpuffer, der für einen MediaPipe-Rechner angegeben wurde. Diese „Grafikoptionen“ können beim Aufrufen einer Grafik angegeben und zum Ausfüllen von Taschenrechner- und Teilgrafikoptionen in der Grafik verwendet werden.

In einer CalculatorGraphConfig können Diagrammoptionen für eine Teilgrafik genau wie Taschenrechneroptionen angegeben werden, wie unten dargestellt:

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

In einem CalculatorGraphConfig können Diagrammoptionen akzeptiert und zum Ausfüllen von Taschenrechneroptionen verwendet werden, wie unten dargestellt:

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

In diesem Beispiel akzeptiert der FaceDetectionSubgraph die Grafikoption protobuf FaceDetectionOptions. Mit FaceDetectionOptions werden einige Feldwerte in den Rechneroptionen ImageToTensorCalculatorOptions und einige Feldwerte in den Teilgrafikoptionen InferenceCalculatorOptions definiert. Die Feldwerte werden mit der Syntax option_value: definiert.

Im Protokollzwischenspeicher CalculatorGraphConfig::Node definieren die Felder node_options: und option_value: zusammen die Optionswerte für einen Rechner wie ImageToTensorCalculator. Das Feld node_options: definiert mithilfe der Text-protobuf-Syntax eine Reihe von konstanten Literalwerten. Jedes option_value:-Feld definiert den Wert für ein protobuf-Feld anhand von Informationen aus der einschließenden Grafik, insbesondere anhand der Feldwerte der Grafikoptionen des einschließenden Diagramms. Im Beispiel oben definiert option_value: "output_tensor_width:options/tensor_width" das Feld ImageToTensorCalculatorOptions.output_tensor_width mithilfe des Werts von FaceDetectionOptions.tensor_width.

Die Syntax von option_value: ähnelt der Syntax von input_stream:. Die Syntax lautet option_value: "LHS:RHS". Auf der linken Seite steht ein Taschenrechner-Optionsfeld, auf der rechten Seite ein Diagrammoptionsfeld. Genauer gesagt, bestehen die LHS- und die RHS jeweils aus einer Reihe von protobuf-Feldnamen, die verschachtelte Protokollpuffer-Nachrichten und -Felder identifizieren, die durch „/“ getrennt sind. Dies wird als „ProtoPath“-Syntax bezeichnet. Verschachtelte Nachrichten, auf die auf der linken oder rechten Seite verwiesen wird, müssen bereits im einschließenden Protokollzwischenspeicher definiert sein, damit sie mit option_value: durchlaufen werden können.

Tages- & Jahreszeiten

Standardmäßig erfordert MediaPipe, dass Taschenrechnerdiagramme azyklisch sind, und behandelt Zyklen in einer Grafik als Fehler. Wenn eine Grafik Zyklen enthalten soll, müssen diese in der Grafikkonfiguration annotiert werden. Auf dieser Seite wird beschrieben, wie Sie das tun können.

HINWEIS: Der aktuelle Ansatz ist experimentell und kann sich ändern. Wir freuen uns auf Ihr Feedback.

Verwenden Sie den CalculatorGraphTest.Cycle-Einheitentest in mediapipe/framework/calculator_graph_test.cc als Beispielcode. Unten sehen Sie den zyklischen Graphen im Test. Die sum-Ausgabe des Addierers ist die Summe der vom Integer-Source-Rechner generierten Ganzzahlen.

Ein zyklischer Graph, der einen Strom von Ganzzahlen addiert

Diese einfache Grafik veranschaulicht alle Probleme in unterstützenden zyklischen Graphen.

Anmerkung zum hinteren Rand

Eine Kante in jedem Zyklus muss als hintere Kante annotiert sein. Dadurch funktioniert die topologische Sortierung von MediaPipe, nachdem alle hinteren Ränder entfernt wurden.

Es gibt normalerweise mehrere Möglichkeiten, die hinteren Ränder auszuwählen. Welche Kanten als hintere Kanten markiert sind, wirkt sich darauf aus, welche Knoten als vorgelagert und welche als nachgelagert gelten, was wiederum die Prioritäten beeinflusst, die MediaPipe den Knoten zuweist.

Beispielsweise markiert der CalculatorGraphTest.Cycle-Test den old_sum-Kanten als Back-Edge. Daher wird der Verzögerungsknoten als Downstream-Knoten des Adder-Knotens betrachtet und erhält eine höhere Priorität. Alternativ könnten wir die sum-Eingabe für den Verzögerungsknoten als Back-Edge markieren. In diesem Fall würde der Verzögerungsknoten als vorgelagerter Knoten des Adder-Knotens betrachtet und eine niedrigere Priorität erhalten.

Erstes Paket

Damit der Adder-Rechner ausgeführt werden kann, wenn die erste Ganzzahl aus der Ganzzahlquelle eintrifft, benötigen wir ein Anfangspaket mit dem Wert 0 und demselben Zeitstempel im old_sum-Eingabestream zum Adder. Dieses erste Paket sollte vom Verzögerungsrechner in der Methode Open() ausgegeben werden.

Verzögerung in einer Schleife

Für jede Schleife sollte eine Verzögerung auftreten, um die vorherige sum-Ausgabe auf die nächste Ganzzahleingabe auszurichten. Dies geschieht auch durch den Verzögerungsknoten. Daher muss der Verzögerungsknoten folgende Informationen über die Zeitstempel des Integer-Source-Rechners haben:

  • Der Zeitstempel der ersten Ausgabe.

  • Die Zeitstempeldifferenz zwischen aufeinanderfolgenden Ausgaben.

Wir planen, eine alternative Planungsrichtlinie hinzuzufügen, die nur die Paketreihenfolge berücksichtigt und Paketzeitstempel ignoriert, wodurch diese Unannehmlichkeiten beseitigt werden.

Vorzeitiges Beenden eines Taschenrechners, wenn ein Eingabestream abgeschlossen ist

Standardmäßig ruft MediaPipe die Methode Close() eines Nicht-Quell-Rechners auf, wenn alle Eingabestreams abgeschlossen sind. In der Beispielgrafik soll der Adder-Knoten beendet werden, sobald die Ganzzahlquelle fertig ist. Dazu wird der Adder-Knoten mit dem alternativen Eingabestream-Handler EarlyCloseInputStreamHandler konfiguriert.

Relevanter Quellcode

Verzögerungsrechner

Beachten Sie den Code in Open(), der das erste Paket ausgibt, und den Code in Process(), der den Eingabepaketen eine Verzögerung (Einheit) hinzufügt. Wie oben erwähnt, geht dieser Verzögerungsknoten davon aus, dass sein Ausgabestream neben einem Eingabestream mit den Paketzeitstempeln 0, 1, 2, 3, ... verwendet wird.

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();
  }
};

Grafikkonfiguration

Beachten Sie die Annotation back_edge und die alternative 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'
}