Grafici

Grafico

Un protocollo CalculatorGraphConfig specifica la topologia e la funzionalità di una Grafico MediaPipe. Ciascun node nel grafico rappresenta una determinata calcolatrice o grafico secondario e specifica le configurazioni necessarie, come la registrazione Tipo di calcolatrice/sottografo, input, output e campi facoltativi, ad esempio le opzioni specifiche per i nodi, il criterio di input e l'esecutore, Sincronizzazione.

In CalculatorGraphConfig sono disponibili diversi altri campi per la configurazione globale a livello di grafico impostazioni, ad esempio configurazioni degli esecutori grafici, numero di thread e dimensione massima della coda di flussi di input. Diverse impostazioni a livello di grafico sono utili per il rendimento del grafico su diverse piattaforme (ad es. computer o dispositivo mobile). Per Ad esempio, sui dispositivi mobili, il collegamento di un calcolatore pesante di inferenza di modelli a un un esecutore può migliorare le prestazioni di un'applicazione in tempo reale poiché abilita la località dei thread.

Di seguito è riportato un banale esempio di CalculatorGraphConfig in cui sono presenti 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 di metadati del modello, nodi facoltativi e così via). Il grafico sopra può 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 maggiori dettagli, consulta l'articolo Creare grafici in C++.

Grafico secondario

Per modularizzare un CalculatorGraphConfig in sottomoduli e facilitare il riutilizzo delle soluzioni di percezione, un grafico MediaPipe può essere definito come Subgraph. La l'interfaccia pubblica di un grafico secondario è composta da un insieme di flussi di input e output in modo simile all'interfaccia pubblica di una calcolatrice. Il grafico secondario può quindi essere incluso un CalculatorGraphConfig come se fosse una calcolatrice. Quando un grafico MediaPipe caricati da un CalculatorGraphConfig, ogni nodo del sottografo viene sostituito grafico corrispondente delle calcolatrici. Di conseguenza, la semantica e il rendimento del grafico secondario è identico al corrispondente grafico delle calcolatrici.

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

  1. Definizione del grafico secondario.

    # 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 verso il grafico secondario è composta da:

    • Tracciare un grafico dei flussi di input
    • Tracciare grafici dei flussi di output
    • Tracciare il grafico dei pacchetti lato di input
    • Tracciare il grafico dei pacchetti lato di output
  2. Registra il grafico secondario utilizzando la regola di creazione mediapipe_simple_subgraph. La Il parametro register_as definisce il nome del componente del nuovo grafico secondario.

    # 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 grafo 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 "opzioni grafico" protobuf per un grafico MediaPipe simile a Calculator Options protobuf specificato per un calcolatore MediaPipe. Queste "opzioni di grafico" può essere specificato dove viene richiamato un grafico e utilizzato per compilare le opzioni della calcolatrice le opzioni dei grafi secondari all'interno del grafico.

In un CalculatorGraphConfig, è possibile specificare le opzioni di grafico per un grafico secondario 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 compilare 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, FaceDetectionSubgraph accetta l'opzione grafico protobuf FaceDetectionOptions. FaceDetectionOptions viene utilizzato per definire alcuni campi valori nelle opzioni della calcolatrice ImageToTensorCalculatorOptions e in alcuni campi nelle opzioni del grafico secondario InferenceCalculatorOptions. I valori dei campi vengono definiti con 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 letterali di valori costanti usando la sintassi di testo protobuf. Ogni campo option_value: definisce il valore di un campo protobuf utilizzando le informazioni del e in particolare dai valori dei campi delle opzioni di grafico del grafico. Nell'esempio precedente, il valore option_value: "output_tensor_width:options/tensor_width" definisce il campo ImageToTensorCalculatorOptions.output_tensor_width utilizzando il valore di FaceDetectionOptions.tensor_width.

La sintassi di option_value: è simile a quella di input_stream:. La la sintassi è option_value: "LHS:RHS". Il riquadro LHS identifica l'opzione per la calcolatrice e il campo RHS identifica un campo opzione grafico. Più precisamente, la LHS e RHS ciascuna consiste in una serie di nomi di campi protobuf che identificano messaggi protobuf e campi separati da "/". Questo è chiamato "ProtoPath" a riga di comando. I messaggi nidificati a cui si fa riferimento in LHS o RHS devono essere già definita nel relativo protobuf in modo da essere attraversata utilizzando option_value:.

Momenti

Per impostazione predefinita, MediaPipe richiede che i grafici delle calcolatrici siano aciclici e tratta i cicli in un grafico come errori. Se un grafico deve avere dei cicli, questi ultimi devono annotati nella configurazione del grafico. In questa pagina viene descritto come fare.

NOTA: l'approccio attuale è sperimentale e soggetto a modifiche. Diamo il benvenuto il tuo feedback.

Usa il test delle unità CalculatorGraphTest.Cycle in mediapipe/framework/calculator_graph_test.cc come codice di esempio. La figura seguente mostra il grafo ciclico nel test. L'output sum del sommatore è la somma dei numeri interi generati dal calcolatore di origine.

un grafo ciclico che somma un flusso di numeri interi

Questo semplice grafico illustra tutti i problemi nei grafi ciclici di supporto.

Annotazione bordo posteriore

Richiediamo che un bordo in ogni ciclo sia annotato come bordo posteriore. Ciò consente L'ordinamento topologico di MediaPipe funzionerà dopo aver rimosso tutti i bordi posteriori.

Di solito sono disponibili diversi modi per selezionare i bordi posteriori. Quali bordi sono contrassegnati poiché i back edge influiscono sui nodi considerati upstream e quali vengono da considerare come downstream, il che a sua volta influisce sulle priorità assegnate da MediaPipe ai nodi.

Ad esempio, il test CalculatorGraphTest.Cycle contrassegna il bordo old_sum come back-edge, quindi il nodo Delay è considerato un nodo downstream del sommatore nodo, a cui viene assegnata una priorità più alta. In alternativa, potremmo contrassegnare sum al nodo di ritardo come bordo posteriore, nel qual caso il nodo di ritardo sarebbe considerato come un nodo a monte del nodo sommatore e a cui viene assegnata una priorità inferiore.

Pacchetto iniziale

Affinché il calcolatore sommatore sia eseguibile quando il primo numero intero all'origine, abbiamo bisogno di un pacchetto iniziale, con valore 0 e timestamp, sul flusso di input old_sum al sommatore. Questo pacchetto iniziale deve essere generato dalla calcolatrice del ritardo nel metodo Open().

Ritardo in un loop

Ogni ciclo dovrebbe subire un ritardo per allineare l'output sum precedente al successivo un valore intero. Questo viene fatto anche dal nodo di ritardo. Quindi il nodo di ritardo deve informazioni sui timestamp del calcolatore dell'origine dei numeri interi:

  • Timestamp del primo output.

  • Il delta del timestamp tra output successivi.

Prevediamo di aggiungere un criterio di pianificazione alternativo che interessa solo il pacchetto ed ignora i timestamp dei pacchetti, eliminando così l'inconveniente.

Chiusura anticipata di un calcolatore quando si esegue un unico flusso di input

Per impostazione predefinita, MediaPipe chiama il metodo Close() di un calcolatore non di origine quando tutti i suoi flussi di input. Nel grafico di esempio, vogliamo interrompere adder non appena viene completata l'origine del numero intero. Ciò si ottiene configurando il nodo adder con un gestore di flussi di input alternativo, EarlyCloseInputStreamHandler.

Codice sorgente pertinente

Calcolatore dei ritardi

Prendi nota del codice in Open() che restituisce il pacchetto iniziale e del codice Process() che aggiunge un ritardo (unità) ai pacchetti di input. Come osservato in precedenza, il 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, ...

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

Osserva l'annotazione back_edge e l'alternativa 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'
}