Rechner

Jeder Rechner ist ein Knoten in einem Graphen. Wir beschreiben, wie Sie ein neues wie Sie einen Rechner initialisieren, wie Sie seine Berechnungen durchführen, Eingabe- und Ausgabestreams, Zeitstempel und Optionen. Jeder Knoten im Diagramm als Calculator implementiert. Der Großteil der Ausführung von Graphen erfolgt innerhalb der Rechnern. Ein Rechner kann null oder mehr Eingabestreams und/oder Seiten und erzeugt null oder mehr Ausgabestreams und/oder Seitenpakete.

CalculatorBase

Ein Rechner wird erstellt, indem eine neue Unterklasse der CalculatorBase -Klasse, implementiert eine Reihe von Methoden und registrieren die neue Unterklasse mit Mediapipe Ein neuer Rechner muss mindestens die folgenden vier Methoden implementieren

  • GetContract()
    • Rechner:innen können die erwarteten Typen von Ein- und Ausgaben angeben eines Rechners in GetContract(). Wenn eine Grafik initialisiert wird, ruft das Framework eine statische Methode auf, um zu prüfen, ob die Pakettypen der verbundenen Ein- und Ausgaben mit den Informationen in diesem Spezifikation zu ändern.
  • Open()
    • Nachdem ein Diagramm gestartet wurde, ruft das Framework Open() auf. Eingabeseite stehen dem Rechner jetzt Pakete zur Verfügung. Open() Interpretiert die Knotenkonfigurationsvorgänge (siehe Grafiken) und bereitet den Rechner für die Ausführung pro Graph vor. Diese Funktion kann auch Pakete in die Rechnerausgaben. Ein Fehler während Open() kann die Ausführung des Graphen beenden.
  • Process()
    • Bei einem Rechner mit Eingaben ruft das Framework Process() wiederholt auf wenn mindestens ein Eingabestream ein Paket verfügbar hat. Das Framework garantiert standardmäßig, dass alle Eingaben den gleichen Zeitstempel haben (siehe Weitere Informationen zur Synchronisierung. Mehrere Process()-Aufrufe können bei paralleler Ausführung gleichzeitig aufgerufen werden aktiviert ist. Wenn während Process() ein Fehler auftritt, ruft das Framework Close() und die Graphausführung wird beendet.
  • Close()
    • Wenn alle Aufrufe an Process() abgeschlossen sind oder wenn alle Eingabestreams geschlossen wurden, das Framework Close(). Diese Funktion wird immer aufgerufen, Open() wurde aufgerufen und erfolgreich, selbst wenn die Graphausführung beendet wurde weil ein Fehler aufgetreten ist. Über Eingabestreams sind keine Eingaben verfügbar während Close(), hat aber immer noch Zugriff auf eingabeseitige Pakete und und daher Ausgaben schreiben kann. Nachdem Close() zurückgegeben wurde, als toter Knoten betrachtet. Das Rechnerobjekt wird zerstört als sobald die Grafik ausgeführt wurde.

Die folgenden Code-Snippets stammen aus 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();
  }

  ...
};

Das Leben eines Taschenrechners

Während der Initialisierung eines MediaPipe-Diagramms ruft das Framework eine GetContract(), um zu bestimmen, welche Arten von Paketen erwartet werden.

Das Framework erstellt und zerstört den gesamten Rechner für jeden Graphen. (z.B. einmal pro Video oder einmal pro Bild). Teure oder große Objekte, die verborgen bleiben Konstante für Graphausführungen sollte als Eingabeseitenpakete bereitgestellt werden, Berechnungen bei nachfolgenden Durchläufen nicht wiederholt.

Nach der Initialisierung findet bei jeder Ausführung des Graphen die folgende Reihenfolge statt:

  • Open()
  • Process() (wiederholt)
  • Close()

Das Framework ruft Open() auf, um den Rechner zu initialisieren. Open() sollte Interpretieren Sie alle Optionen und legen Sie den Status pro Diagrammausführung fest. Open() kann Eingabeseitenpakete abrufen und Pakete in die Rechnerausgaben schreiben. Wenn geeignet, sollte SetOffset() aufgerufen werden, um die potenzielle Paketzwischenspeicherung zu reduzieren von Eingabestreams.

Wenn während Open() oder Process() ein Fehler auftritt (wie durch einen von diesen angegeben) und einen Nicht-Ok-Status zurückgibt, wird die Graphausführung ohne weitere Aufrufe beendet. und der Rechner wird zerstört.

Bei einem Rechner mit Eingaben ruft das Framework Process() immer dann auf, wenn mindestens für eine Eingabe ein Paket verfügbar ist. Das Framework garantiert, dass alle Eingaben gleichen Zeitstempel, dass sich die Zeitstempel mit jedem Aufruf von Process() erhöhen und dass alle Pakete zugestellt werden. Daher kann es sein, dass einige Eingaben Pakete, wenn Process() aufgerufen wird. Eine Eingabe, deren Paket fehlt, scheint ein leeres Paket (ohne Zeitstempel) erzeugen.

Das Framework ruft Close() nach allen Aufrufen von Process() auf. Alle Eingaben sind erschöpft, aber Close() hat Zugriff auf eingabeseitige Pakete und kann Ausgaben schreiben. Nachdem Close zurückgegeben wurde, wird der Rechner zerstört.

Rechner ohne Eingaben werden als Quellen bezeichnet. Ein Quellenrechner Process() wird weiterhin aufgerufen, solange der Status Ok zurückgegeben wird. A Der Quellenrechner gibt an, dass er aufgebraucht ist, indem ein Haltestellenstatus zurückgegeben wird. (z. B. mediaPipe::tool::StatusStop()).

Ein- und Ausgaben identifizieren

Die öffentliche Schnittstelle zu einem Rechner besteht aus einer Reihe von Eingabestreams und Ausgabestreams. In einer CalculatorGraphConfiguration sind Rechner mit den Eingaben anderer Rechner verbunden, die Streams. Streamnamen werden normalerweise in Kleinbuchstaben geschrieben, während Eingabe- und Ausgabe-Tags normalerweise in GROSSBUCHSTABEN. Im folgenden Beispiel lautet die Ausgabe mit dem Tag-Namen VIDEO: die mit dem Eingang mit dem Tag-Namen VIDEO_IN über den Stream namens 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"
}

Eingabe- und Ausgabestreams können anhand der Indexnummer, des Tag-Namens oder Kombination aus Tag-Name und Indexnummer. Sie sehen einige Beispiele für Eingabe- und Ausgabekennungen im Beispiel unten. SomeAudioVideoCalculator identifiziert Videoausgabe nach Tag und Audioausgaben durch die Kombination aus Tag und -Index. Der Eingang mit dem Tag VIDEO ist mit dem Stream namens video_stream. Die Ausgaben mit dem Tag AUDIO und den Indexen 0 und 1 sind die mit den Streams audio_left und audio_right verbunden sind. SomeAudioCalculator identifiziert Audioeingaben nur anhand des Index (kein Tag erforderlich).

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

In der Rechnerimplementierung werden Ein- und Ausgaben auch durch Tags gekennzeichnet. mit dem Namen und der Indexnummer. In der folgenden Funktion werden Ein- und Ausgabe identifiziert:

  • Nach Indexnummer: Der kombinierte Eingabestream wird einfach durch den Index identifiziert. 0
  • Nach Tag-Name: Der Videoausgabestream wird durch den Tag-Namen "VIDEO" identifiziert.
  • Nach Tag-Name und Indexnummer: Die ausgegebenen Audiostreams werden anhand des Kombination aus Tag-Name AUDIO und den Indexnummern 0 und 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();
  }

In Bearbeitung

Process(), der auf einem Nicht-Quellknoten aufgerufen wird, muss absl::OkStatus() zurückgeben an um anzuzeigen, dass alles funktioniert hat, oder einen anderen Statuscode, um einen Fehler zu signalisieren

Wenn ein externer Rechner tool::StatusStop() zurückgibt, signalisiert dies den Diagramm vorzeitig abgebrochen. In diesem Fall werden alle Quellenrechner und der Graphen verwendet, Eingabestreams werden geschlossen (und verbleibende Pakete werden durch den Diagramm).

Auf einen Quellknoten in einem Diagramm wird Process() so lange wie möglich aufgerufen da sie absl::OkStatus( zurückgibt. Um anzuzeigen, dass keine weiteren Daten vorliegen, generierte Rückgabe tool::StatusStop(). Jeder andere Status gibt an, dass ein Fehler aufgetreten.

Close() gibt absl::OkStatus() zurück, wenn der Vorgang erfolgreich war. Sonstiger Status zeigt einen Fehler an.

Hier ist die grundlegende Process()-Funktion. Dabei wird die Methode Input() verwendet, nur verwendet werden, wenn der Rechner nur über eine einzige Eingabe verfügt), um die Eingabedaten anzufordern. Es verwendet dann std::unique_ptr, um den für das Ausgabepaket erforderlichen Arbeitsspeicher zuzuweisen. und führt die Berechnungen durch. Wenn der Vorgang abgeschlossen ist, wird der Zeiger losgelassen, wenn er des Ausgabestreams.

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

Rechneroptionen

Rechner akzeptieren Verarbeitungsparameter über (1) Eingabestreampakete (2) Eingabeseitenpakete und (3) Rechneroptionen. Rechneroptionen, wenn angegeben, als Literalwerte im Feld node_options der CalculatorGraphConfiguration.Node-Nachricht.

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

Das Feld node_options akzeptiert die Proto3-Syntax. Alternativ können Sie mit dem Rechner Optionen können im Feld options mithilfe der Proto2-Syntax angegeben werden.

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

Nicht alle Rechner akzeptieren Rechneroptionen. Um Optionen anzunehmen, muss ein normalerweise einen neuen protobuf-Nachrichtentyp definiert, Optionen wie PacketClonerCalculatorOptions. Der Rechner ermittelt dann diese protobuf-Nachricht in ihrer CalculatorBase::Open-Methode lesen und möglicherweise ist auch in seiner CalculatorBase::GetContract-Funktion CalculatorBase::Process-Methode. Normalerweise wird der neue Nachrichtentyp protobuf als protobuf-Schema mit einer „.proto“-Datei definiert Datei und ein mediapipe_proto_library()-Build-Regel.

  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",
      ],
  )

Beispielrechner

In diesem Abschnitt wird die Implementierung von PacketClonerCalculator erläutert, die und wird in vielen Rechnergrafiken verwendet. PacketClonerCalculator erstellt einfach eine Kopie der letzten Eingabepakete on demand.

PacketClonerCalculator ist nützlich, wenn die Zeitstempel eingehender Datenpakete nicht perfekt aufeinander abgestimmt. Angenommen, wir haben einen Raum mit Mikrofon, Licht und einer Videokamera, die Sinnesdaten sammelt. Jeder der Sensoren arbeitet unabhängig und erhebt unregelmäßig Daten. Angenommen, die Ausgabe jedes Sensors:

  • Mikrofon = Lautstärke in Dezibel im Raum (Ganzzahl)
  • Lichtsensor = Helligkeit des Raums (Ganzzahl)
  • Videokamera = RGB-Bildframe des Raums (ImageFrame)

Unsere einfache Wahrnehmungs-Pipeline wurde entwickelt, um Sinnesdaten aus diesen drei Sensoren, sodass jedes Mal, wenn Frame-Daten von der Kamera vorliegen, wird mit den zuletzt erfassten Daten zur Mikrofonlautstärke und dem Licht synchronisiert Helligkeitsdaten des Sensors. Um dies mit MediaPipe zu erreichen, verfügt unsere Perception-Pipeline über drei Eingabestreams:

  • Room_mic_signal – Jedes Datenpaket in diesem Eingabestream ist eine Ganzzahl die die Lautstärke der Geräusche in einem Raum mit Zeitstempel darstellt.
  • Room_lightening_sensor – Jedes Datenpaket in diesem Eingabestream ist eine Ganzzahl Daten, die angeben, wie hell der Raum beleuchtet ist, mit Zeitstempel.
  • Room_video_tick_signal – Jedes Datenpaket in diesem Eingabestream Bildframe aus Videodaten, die das von der Kamera aufgenommene Video darstellen Raum mit Zeitstempel.

Unten siehst du die Implementierung von PacketClonerCalculator. Sie können die GetContract()-, Open()- und Process()-Methoden sowie die Instanz. Variable current_, die die neuesten Eingabepakete enthält.

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

Normalerweise verfügt ein Rechner nur über eine .cc-Datei. .h ist nicht erforderlich, da Mediapipe nutzt die Registrierung, um Rechner bekannt zu machen. Nachdem Sie Rechnerklasse definiert haben, registrieren Sie sie mit einem Makroaufruf REGISTER_CALCULATOR(calculator_class_name).

Unten sehen Sie ein einfaches MediaPipe-Diagramm mit 3 Eingabestreams, 1 Knoten (PacketClonerCalculator) und zwei Ausgabestreams.

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

Das folgende Diagramm zeigt, wie PacketClonerCalculator seine Ausgabe definiert Pakete (unten) basierend auf der Reihe von Eingabepaketen (oben).

Grafik mit PacketClonerCalculator
Bei jedem Empfang eines Pakets in seinem TICK-Eingabestream gibt PacketKlonrCalculator das neueste Paket aus jedem seiner Eingabestreams aus. Die Reihenfolge der Ausgabepakete (unten) wird durch die Sequenz der Eingabepakete (oben) und deren Zeitstempel bestimmt. Die Zeitstempel werden rechts im Diagramm angezeigt.