Grafici

Grafico

Un protocollo CalculatorGraphConfig specifica la topologia e la funzionalità di un grafico MediaPipe. Ogni node nel grafico rappresenta una calcolatrice o un sottografo particolare e specifica le configurazioni necessarie, ad esempio il tipo di calcolatrice/sottografo registrato, gli input, gli output e i campi facoltativi, ad esempio opzioni specifiche per nodo, criteri di input ed esecutore, descritti nella sezione Sincronizzazione.

CalculatorGraphConfig ha diversi altri campi per configurare impostazioni globali a livello di grafico, ad esempio configurazioni di esecutori di grafici, numero di thread e dimensione massima della coda dei flussi di input. Diverse impostazioni a livello di grafico sono utili per ottimizzare il rendimento del grafico su diverse piattaforme (ad es. desktop o dispositivi mobili). Ad esempio, sui dispositivi mobili, collegare una calcolatrice pesante di inferenza dei modelli a un esecutore separato può migliorare le prestazioni di un'applicazione in tempo reale poiché ciò consente la località dei thread.

Di seguito è riportato un banale esempio di CalculatorGraphConfig in cui è disponibile una serie di calcolatori passthrough :

# 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 offre una rappresentazione C++ alternativa per grafici complessi (ad es. pipeline ML, gestione dei metadati del modello, nodi facoltativi e così via). Il grafico riportato sopra potrebbe avere il seguente aspetto:

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

Per ulteriori dettagli, consulta Creazione di grafici in C++.

Sottografo

Per modularizzare un CalculatorGraphConfig in sottomoduli e facilitare il riutilizzo delle soluzioni di percezione, un grafico MediaPipe può essere definito come un Subgraph. L'interfaccia pubblica di un sottografo è composta da un insieme di flussi di input e di output simili all'interfaccia pubblica di una calcolatrice. Il sottografo può quindi essere incluso in un elemento CalculatorGraphConfig come se fosse una calcolatrice. Quando un grafico MediaPipe viene caricato da un CalculatorGraphConfig, ogni nodo del sottografico viene sostituito dal grafico corrispondente delle calcolatrici. Di conseguenza, la semantica e le prestazioni del sottografico sono identiche a quelle del grafico corrispondente.

Di seguito è riportato un esempio di come creare un sottografico denominato TwoPassThroughSubgraph.

  1. Definizione del sottografico.

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

    L'interfaccia pubblica per il sottografo è costituita da:

    • Tracciare il grafico dei flussi di input
    • Tracciare il grafico dei flussi di output
    • Tracciare grafici dei pacchetti laterali di input
    • Tracciare grafici dei pacchetti laterali di output
  2. Registra il grafico secondario utilizzando la regola Build mediapipe_simple_subgraph. Il parametro register_as definisce il nome del componente del nuovo sottografico.

    # 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. Utilizza il grafico secondario nel grafico principale.

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

Opzioni grafico

È possibile specificare un protobuf "opzioni del grafico" per un grafico MediaPipe simile al protobuf Calculator Options specificato per una calcolatrice MediaPipe. Queste "opzioni del grafico" possono essere specificate dove viene richiamato un grafico e utilizzate per compilare le opzioni della calcolatrice e le opzioni dei sottografi all'interno del grafico.

In un CalculatorGraphConfig, è possibile specificare le opzioni del grafico per un sottografico esattamente come le opzioni della calcolatrice, come mostrato di seguito:

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 un CalculatorGraphConfig, le opzioni del grafico possono essere accettate e utilizzate per completare le opzioni della calcolatrice, come mostrato di seguito:

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 questo esempio, l'elemento FaceDetectionSubgraph accetta l'opzione del grafico protobuf FaceDetectionOptions. FaceDetectionOptions viene utilizzato per definire alcuni valori di campo nelle opzioni della Calcolatrice ImageToTensorCalculatorOptions e alcuni valori di campo nelle opzioni del sottografico InferenceCalculatorOptions. I valori dei campi vengono definiti utilizzando la sintassi option_value:.

Nel protobuf CalculatorGraphConfig::Node, i campi node_options: e option_value: definiscono insieme i valori delle opzioni per una calcolatrice come ImageToTensorCalculator. Il campo node_options: definisce un insieme di valori costanti letterali utilizzando la sintassi protobuf del testo. Ogni campo option_value: definisce il valore di un campo protobuf utilizzando le informazioni del grafico che lo include, in particolare i valori dei campi delle opzioni del grafico del grafico che lo include. Nell'esempio precedente, "output_tensor_width:options/tensor_width" option_value: definisce il campo ImageToTensorCalculatorOptions.output_tensor_width utilizzando il valore FaceDetectionOptions.tensor_width.

La sintassi di option_value: è simile a quella di input_stream:. La sintassi è option_value: "LHS:RHS". Il lato sinistro indica un campo opzione Calcolatrice, mentre il lato destro identifica un campo opzione Grafico. Più precisamente, i codici LHS e RHS sono costituiti ciascuno da una serie di nomi di campi protobuf che identificano messaggi e campi protobuf nidificati separati da "/". Questa è nota come sintassi "ProtoPath". I messaggi nidificati a cui viene fatto riferimento nel testo LHS o RHS devono essere già definiti nel protobuf che li contiene per poter essere attraversati utilizzando option_value:.

Momenti

Per impostazione predefinita, MediaPipe richiede che i grafici della calcolatrice siano aciclici e tratta i cicli di un grafico come errori. Se un grafico è destinato ad avere cicli, questi devono essere annotati nella relativa configurazione. In questa pagina viene spiegato come fare.

NOTA: l'approccio attuale è sperimentale e soggetto a modifiche. Saremmo felici di ricevere il tuo feedback.

Utilizza il test delle unità CalculatorGraphTest.Cycle in mediapipe/framework/calculator_graph_test.cc come codice di esempio. Di seguito è riportato il grafico ciclico nel test. L'output sum del sommatore è la somma dei numeri interi generati dal calcolatore di origine dei numeri interi.

un grafico ciclico che aggiunge un flusso di numeri interi

Questo semplice grafico illustra tutti i problemi nel supporto dei grafi ciclici.

Annotazione back-edge

È necessario che un bordo in ogni ciclo sia annotato come bordo posteriore. Ciò consente all'ordinamento topologico di MediaPipe di funzionare, dopo la rimozione di tutti i bordi posteriori.

Solitamente, esistono diversi modi per selezionare i bordi posteriori. Quali bordi sono contrassegnati come bordi posteriori, influiscono quali nodi sono considerati upstream e quali nodi sono considerati come downstream, il che a sua volta influenza le priorità che MediaPipe assegna ai nodi.

Ad esempio, il test CalculatorGraphTest.Cycle contrassegna il bordo old_sum come bordo posteriore, quindi il nodo Delay viene considerato come un nodo downstream del nodo adder a cui viene assegnata una priorità più alta. In alternativa, potremmo contrassegnare l'input sum per il nodo di ritardo come bordo posteriore, nel qual caso il nodo di ritardo verrebbe considerato come un nodo a monte del nodo dell'adder e gli viene assegnata una priorità inferiore.

Pacchetto iniziale

Affinché il calcolatore del sommatore sia eseguibile quando arriva il primo numero intero dell'origine, è necessario un pacchetto iniziale, con valore 0 e con lo stesso timestamp, nel flusso di input old_sum verso il sommatore. Questo pacchetto iniziale deve essere output dal calcolatore del ritardo nel metodo Open().

Ritardo in un loop

Ogni loop dovrebbe presentare un ritardo per allineare l'output sum precedente all'input intero successivo. Questa operazione viene eseguita anche dal nodo ritardo. Di conseguenza, il nodo ritardo deve conoscere quanto segue sui timestamp del calcolatore di origine dei numeri interi:

  • Il timestamp del primo output.

  • Il delta del timestamp tra gli output successivi.

Prevediamo di aggiungere una norma di pianificazione alternativa che si preoccupi solo dell'ordine dei pacchetti e ignori i timestamp dei pacchetti, eliminando così questo inconveniente.

Interruzione anticipata di una calcolatrice al termine di un unico flusso di input

Per impostazione predefinita, MediaPipe chiama il metodo Close() di una calcolatrice non di origine quando tutti i suoi flussi di input sono stati completati. Nel grafico di esempio, vogliamo arrestare il nodo adder appena termina l'origine del numero intero. A questo scopo, configura il nodo adder con un gestore del flusso di input alternativo, EarlyCloseInputStreamHandler.

Codice sorgente pertinente

Calcolatore del ritardo

Nota il codice in Open() che restituisce il pacchetto iniziale e il codice in Process() che aggiunge un ritardo di (unità) ai pacchetti di input. Come notato sopra, questo nodo di ritardo presuppone che il suo flusso di output venga utilizzato insieme a un flusso di input con timestamp dei pacchetti 0, 1, 2, 3 e così via

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

Configurazione grafico

Prendi nota dell'annotazione back_edge e dell'input_stream_handler alternativa.

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