Wykresy

Wykres

Proto CalculatorGraphConfig określa topologię i funkcjonalność wykresu MediaPipe. Każdy element node na wykresie odpowiada konkretnemu kalkulatorowi lub podpunktowi i określa niezbędne konfiguracje, np. zarejestrowany typ kalkulatora/podgrafu, dane wejściowe, dane wyjściowe i pola opcjonalne, takie jak opcje związane z węzłem, zasady wprowadzania i wykonawca, opisane w sekcji Synchronizacja.

CalculatorGraphConfig ma kilka innych pól do konfigurowania globalnych ustawień na poziomie wykresu, np. konfigurację wykonawcy wykresu, liczbę wątków i maksymalny rozmiar kolejki strumieni wejściowych. Istnieje kilka ustawień na poziomie wykresu, które przydają się do dostrajania wykresu na różnych platformach (np. na komputerach i urządzeniach mobilnych). Na przykład w przypadku urządzenia mobilnego załączenie kalkulatora wnioskowania opartego na modelu na potrzeby oddzielnego wykonawcy może poprawić wydajność aplikacji w czasie rzeczywistym, ponieważ umożliwia to lokalizowanie wątków.

Poniżej znajduje się prosty przykład funkcji CalculatorGraphConfig z serią kalkulatorów przekazywania :

# 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 oferuje alternatywną reprezentację właściwości C++ dla złożonych wykresów (np. potoki ML, obsługę metadanych modelu, opcjonalne węzły itp.). Powyższy wykres może wyglądać tak:

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

Więcej informacji znajdziesz w artykule Tworzenie wykresów w C++.

Podtytuł

Aby połączyć moduł CalculatorGraphConfig w moduły podrzędne i pomóc w ponownym użyciu rozwiązań percepcyjnych, wykres MediaPipe można zdefiniować jako Subgraph. Publiczny interfejs podgrafu składa się z zestawu strumieni danych wejściowych i wyjściowych, który jest podobny do publicznego interfejsu kalkulatora. Podtytuł można umieścić w elemencie CalculatorGraphConfig tak, jakby był kalkulatorem. Gdy wykres MediaPipe jest ładowany z CalculatorGraphConfig, każdy węzeł podrzędny jest zastępowany odpowiednim wykresem kalkulatora. W rezultacie semantyka i skuteczność podtytułu są takie same jak odpowiadające mu wykresy kalkulatorów.

Poniżej znajdziesz przykład tworzenia podgrafu o nazwie TwoPassThroughSubgraph.

  1. Zdefiniowanie podtytułu.

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

    Publiczny interfejs podtytułu składa się z tych elementów:

    • Wykresy strumieni danych wejściowych
    • Wykresy strumieni danych wyjściowych
    • Wykresy dodatkowych pakietów danych wejściowych
    • Wykres przedstawiający dodatkowe pakiety wyjściowe
  2. Zarejestruj podpunkt za pomocą reguły BUILD mediapipe_simple_subgraph. Parametr register_as określa nazwę komponentu nowego podgrafu.

    # 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. Skorzystaj z podpunktu na wykresie głównym.

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

Opcje wykresu

W przypadku wykresu MediaPipe można określić protobuf opcji wykresu, podobny do protokołu Calculator Options określonego na potrzeby kalkulatora MediaPipe. Te „opcje wykresu” można określić, gdzie wywoływany jest wykres. Umożliwiają one wypełnianie opcji kalkulatora i podpisów na wykresie.

W przypadku funkcji CalculatorGraphConfig opcje wykresu można określić w podtytułie – tak jak opcje kalkulatora:

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

W obiekcie CalculatorGraphConfig opcje wykresu mogą być akceptowane i używane do wypełniania opcji kalkulatora, jak pokazano poniżej:

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

W tym przykładzie FaceDetectionSubgraph akceptuje opcję wykresu protobuf FaceDetectionOptions. Parametr FaceDetectionOptions służy do definiowania wartości pól w opcjach kalkulatora ImageToTensorCalculatorOptions oraz niektórych wartości pól w opcjach podtytułu InferenceCalculatorOptions. Wartości pól są definiowane za pomocą składni option_value:.

W protobfku CalculatorGraphConfig::Node pola node_options: i option_value: określają wartości opcji kalkulatora, np. ImageToTensorCalculator. Pole node_options: definiuje zestaw wartości stałych literałów przy użyciu składni protokołu tekstowego. Każde pole option_value: określa wartość 1 pola protobufa na podstawie informacji z wykresu otaczającego, a zwłaszcza wartości pól opcji wykresu nadrzędnego. W powyższym przykładzie option_value: "output_tensor_width:options/tensor_width" określa pole ImageToTensorCalculatorOptions.output_tensor_width za pomocą wartości FaceDetectionOptions.tensor_width.

Składnia instrukcji option_value: jest podobna do składni input_stream:. Składnia: option_value: "LHS:RHS". LHS wskazuje pole opcji kalkulatora, a po prawej – pole opcji wykresu. Dokładniej rzecz ujmując, LHS i RHS składają się z serii pól protokołu, identyfikujących zagnieżdżone wiadomości protokołu i pola rozdzielone znakiem „/”. Jest to tzw. składnia „ProtoPath”. Zagnieżdżone wiadomości, do których odwołuje się plik LHS lub RHS, muszą być już zdefiniowane w protobufach zamykających, aby można było poruszać się po nich za pomocą funkcji option_value:.

Cykle

Domyślnie MediaPipe wymaga, aby wykresy kalkulatora były acykliczne i traktują cykle na wykresie jako błędy. Jeśli wykres ma zawierać cykle, musisz dodać do nich adnotacje w konfiguracji wykresu. Z tej strony dowiesz się, jak to zrobić.

UWAGA: obecne podejście ma charakter eksperymentalny i może ulec zmianie. Chętnie poznamy Twoją opinię.

Jako przykładowego kodu użyj testu jednostkowego CalculatorGraphTest.Cycle w mediapipe/framework/calculator_graph_test.cc. Poniżej widać wykres cykliczny w teście. Dane wyjściowe funkcji sum dodawane przez sumera to suma liczb całkowitych wygenerowanych przez kalkulator źródeł całkowitych.

graf cykliczny, który dodaje strumień liczb całkowitych

Ten prosty wykres ilustruje wszystkie problemy związane z obsługą wykresów cyklicznych.

Adnotacja na tylnej krawędzi

Wymagamy, aby krawędź w każdym cyklu była opatrzona adnotacjami jako tylną krawędzią. Umożliwia to działanie sortowania topologicznego MediaPipe po usunięciu wszystkich tylnych krawędzi.

Tylne krawędzie można zwykle wybrać na kilka sposobów. To, które krawędzie są oznaczone jako tylne, wpływa na to, które węzły są uznawane za nadrzędne, a które są uważane za podrzędne, co z kolei wpływa na priorytety przypisane przez MediaPipe do węzłów.

Na przykład test CalculatorGraphTest.Cycle oznacza krawędź old_sum jako tylną krawędź, więc węzeł opóźniony jest uznawany za węzeł od węzła adderowego i ma wyższy priorytet. Możemy też oznaczyć dane wejściowe sum węzła opóźnionego jako tylną krawędź. W takim przypadku węzeł opóźniony zostanie uznany za węzeł nadrzędny węzła dodawania i otrzymuje niższy priorytet.

Pakiet początkowy

Aby kalkulator sumera mógł zostać uruchomiony po nadejściu pierwszej liczby całkowitej ze źródła liczby całkowitej, w strumieniu wejściowym old_sum do sumera potrzebny jest początkowy pakiet o wartości 0 i tej samej sygnaturze czasowej. Ten początkowy pakiet powinien zostać zwrócony przez kalkulator opóźnienia w metodzie Open().

Opóźnienie w pętli

Każda pętla powinna powodować opóźnienie, aby dopasować poprzednie dane wyjściowe sum do następnych całkowitych danych wejściowych. Robi to również przez węzeł opóźniający. Zatem węzeł opóźnienia musi znać te informacje o sygnaturach czasowych kalkulatora źródła liczby całkowitej:

  • Sygnatura czasowa pierwszych danych wyjściowych.

  • Różnica sygnatur czasowych między kolejnymi danymi wyjściowymi.

Planujemy dodać alternatywną zasadę harmonogramu, która dotyczy tylko kolejności pakietów i ignoruje sygnatury czasowe pakietów, co wyeliminuje te niedogodności.

Wcześniejsze zakończenie działania kalkulatora w przypadku realizacji jednego strumienia danych wejściowych

Domyślnie MediaPipe wywołuje metodę Close() z kalkulatora innego niż źródłowy, gdy gotowe są wszystkie jego strumienie wejściowe. Na przykładowym wykresie chcemy zatrzymać węzeł dodawania natychmiast po wykorzystaniu źródła liczby całkowitej. Jest to możliwe przez skonfigurowanie węzła dodającego z alternatywnym modułem obsługi strumienia wejściowego EarlyCloseInputStreamHandler.

Odpowiedni kod źródłowy

Kalkulator opóźnień

Zwróć uwagę na kod w Open(), który generuje pakiet początkowy, i kod w Process(), który dodaje opóźnienie (jednostkowe) do pakietów wejściowych. Jak wspomnieliśmy powyżej, ten węzeł opóźnienia zakłada, że jego strumień wyjściowy jest używany razem ze strumieniem wejściowym o sygnaturach czasowych pakietu 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();
  }
};

Konfiguracja wykresu

Zwróć uwagę na adnotację back_edge i alternatywną treść 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'
}