Wykresy

Wykres

Protokół CalculatorGraphConfig określa topologię i funkcjonalność Wykres MediaPipe. Każda node na wykresie odpowiada konkretnemu kalkulatorowi lub i określa niezbędne konfiguracje, takie jak zarejestrowane typ kalkulatora/podgrafu, dane wejściowe, dane wyjściowe i pola opcjonalne, takie jak opcji dla konkretnego węzła, zasady wejściowej oraz wykonawcy, omówione w Synchronizacja.

Funkcja CalculatorGraphConfig ma kilka innych pól do konfigurowania globalnego wykresu ustawienia, np. konfiguracje wykonawcy grafu, liczba wątków oraz maksymalny rozmiar kolejki strumieni wejściowych. Kilka ustawień na poziomie wykresu pomaga dostosować skuteczności wykresu na różnych platformach (np. komputer i telefon komórkowy). Dla: przez umieszczenie ciężkiego kalkulatora wnioskowania dotyczącego modelu w osobnym może zwiększyć wydajność aplikacji w czasie rzeczywistym, ponieważ włącza lokalność wątku.

Poniżej znajduje się trywialny przykład reguły CalculatorGraphConfig, w której mamy serię kalkulatory przejścia :

# 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ę C++ dla złożonych wykresów (np. potoki ML, obsługa metadanych modelu, węzły opcjonalne 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ł

Możliwość modularyzacji modułu CalculatorGraphConfig w moduły podrzędne i pomoc w ich ponownym używaniu. rozwiązań percepcyjnych, wykres MediaPipe można zdefiniować jako Subgraph. publiczny interfejs podgrafu składa się ze zbioru strumieni wejściowych i wyjściowych podobny do publicznego interfejsu kalkulatora. Podgraf można następnie umieścić w CalculatorGraphConfig, jakby to był kalkulator. Gdy wykres MediaPipe jest wczytany z interfejsu CalculatorGraphConfig, każdy węzeł podgrafu jest zastępowany przez odpowiedni wykres kalkulatorów. W rezultacie semantyka i skuteczność podgrafu jest taki sam jak odpowiedni wykres kalkulatorów.

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

  1. Definiowanie podgrafu

    # 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 podgrafu składa się z:

    • Strumienie danych wejściowych wykresu
    • Wykres strumieni danych wyjściowych
    • Wykres po stronie pakietów wejściowych
    • Narysuj wykres pakietów wyjściowych po stronie
  2. Zarejestruj podgraf przy użyciu reguły BUILD mediapipe_simple_subgraph. 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 podgrafu 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

Można określić „opcje wykresu”. protobuf dla grafu MediaPipe podobnie jak Calculator Options protobuf określony dla kalkulatora MediaPipe. Te „opcje wykresu” może być określone miejsce wywoływania grafu i używane do wypełniania opcji kalkulatora oraz opcje podgrafu.

W CalculatorGraphConfig opcje wykresu można określić dla podgrafu dokładnie tak jak w przypadku opcji kalkulatora, jak poniżej:

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 elemencie CalculatorGraphConfig opcje wykresu mogą być akceptowane i używane do wypełniania 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 protobuf opcji grafu FaceDetectionOptions Pole FaceDetectionOptions jest używane do zdefiniowania pola wartości w opcjach kalkulatora ImageToTensorCalculatorOptions i w niektórych polach w opcjach podgrafu InferenceCalculatorOptions. Wartości pól są zdefiniowane za pomocą składni option_value:.

W protobufie CalculatorGraphConfig::Node pola node_options: i option_value: razem definiują wartości opcji kalkulatora, takie jak ImageToTensorCalculator Pole node_options: definiuje zbiór literału stałych wartości za pomocą składni protokołu tekstowego. W każdym polu option_value: określa wartość jednego pola protobuf przy użyciu informacji z zamykającego zwłaszcza z wartości pól opcji wykresu wykres. W przykładzie powyżej option_value: "output_tensor_width:options/tensor_width" definiuje pole ImageToTensorCalculatorOptions.output_tensor_width z użyciem wartości FaceDetectionOptions.tensor_width

Składnia instrukcji option_value: jest podobna do składni input_stream:. składnia to option_value: "LHS:RHS". LHS określa opcję kalkulatora , a RHS określa pole opcji wykresu. Mówiąc bardziej szczegółowo, i RHS, każda z nich składa się z serii nazw pól protobuf identyfikujących zagnieżdżone wiadomości i pola buforów protokołu rozdzielone znakiem „/”. Jest to tzw. „ProtoPath”. składni. Zagnieżdżone komunikaty, do których odwołuje się LHS lub RHS, muszą już być zdefiniowane w protobufie otaczającym, aby przejść do przemierzania za pomocą option_value:

Cykle

Domyślnie MediaPipe wymaga, aby wykresy kalkulatora były acykliczne i traktuje cykle na wykresie jako błędy. Jeśli wykres ma zawierać cykle, muszą one zostaną oznaczone adnotacjami w konfiguracji grafu. Na tej stronie dowiesz się, jak to zrobić.

UWAGA: obecne podejście ma charakter eksperymentalny i może ulec zmianie. Zapraszamy z Twoją opinią.

Użyj testu jednostkowego CalculatorGraphTest.Cycle w: mediapipe/framework/calculator_graph_test.cc jako przykładowy kod. Widoczne poniżej to wykresu cyklicznego w teście. Dane wyjściowe funkcji sum sumy liczb całkowitych wygenerowanych przez kalkulator źródłowy liczby całkowitej.

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

Ten prosty wykres ilustruje wszystkie problemy związane z grafami cyklicznymi.

Adnotacja na tylnej krawędzi

Wymagamy, aby krawędź w każdym cyklu była oznaczona jako tylna. Dzięki temu Sortowanie topologiczne w MediaPipe działa po usunięciu wszystkich tylnych krawędzi.

Tylne krawędzie można zwykle wybrać na kilka sposobów. Które krawędzie są oznaczone ponieważ tylne krawędzie mają wpływ na to, które węzły są uznawane za nadrzędne, a które jest uznawana za dalszą, 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ędzią, więc węzeł opóźnienia jest uważany za węzeł nadrzędny sumy i ma wyższy priorytet. Możemy też oznaczyć sum do węzła opóźnienia jako krawędzi tylnej. W takim przypadku węzeł opóźnienia będzie miał postać uważany za węzeł nadrzędny węzła dodatkowego i ma niższy priorytet.

Początkowy pakiet

Aby kalkulator sumy mógł zostać uruchomiony, gdy zostanie użyta pierwsza liczba całkowita z liczby całkowitej dociera do źródła, potrzebujemy pakietu początkowego o wartości 0 i o takiej samej wartości sygnatura czasowa w strumieniu wejściowym old_sum dodanym do dodatku. Ten początkowy pakiet powinien być zwracany przez kalkulator opóźnienia w metodzie Open().

Opóźnienie w pętli

Każda pętla powinna powodować opóźnienie wynikające z wyrównania poprzedniego wyjścia sum z następnym jest liczbą całkowitą. To samo robi węzeł opóźnienia. Węzeł opóźnienia musi więc zapoznaj się z poniższymi informacjami o sygnaturach czasowych kalkulatora źródła liczb całkowitych:

  • Sygnatura czasowa pierwszego wyniku.

  • Różnica sygnatury czasowej między kolejnymi danymi wyjściowymi.

Planujemy dodać alternatywne zasady planowania, które będą uwzględniać tylko pakiety kolejności i ignoruje sygnatury czasowe pakietów, co wyeliminuje ten problem.

Wczesne zakończenie kalkulatora po zakończeniu jednego strumienia wejściowego

Domyślnie MediaPipe wywołuje metodę Close() kalkulatora niebędącego źródłem, gdy: jego wszystkie strumienie wejściowe są gotowe. Na przykładowym wykresie chcemy zatrzymać po zakończeniu pracy źródła w postaci liczby całkowitej. Jest to możliwe dzięki konfigurując węzeł dodatkowy z alternatywnym modułem obsługi strumienia wejściowego, EarlyCloseInputStreamHandler

Odpowiedni kod źródłowy

Kalkulator opóźnienia

Zwróć uwagę na kod w kodzie Open(), który zwraca pakiet początkowy, oraz kod w Process(), który dodaje opóźnienie (jednostkowe) do pakietów wejściowych. Jak wspomnieliśmy powyżej, opóźniony węzeł zakłada, że jego strumień wyjściowy jest używany razem ze strumieniem wejściowym z sygnatury czasowe pakietów 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 grafu

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