Gráficos

Gráfico

Um proto CalculatorGraphConfig especifica a topologia e a funcionalidade de Gráfico do MediaPipe. Cada node no gráfico representa uma calculadora ou e especifica as configurações necessárias, como registros calculadora/subgráfico, entradas, saídas e campos opcionais, como opções específicas de nó, política de entrada e executor, discutidas em Sincronização.

CalculatorGraphConfig tem vários outros campos para configurar o nível do gráfico global configurações, por exemplo, configurações do executor do gráfico, número de linhas de execução e tamanho máximo da fila de fluxos de entrada. Várias configurações no nível do gráfico são úteis para ajustar desempenho do gráfico em diferentes plataformas (por exemplo, computador x dispositivo móvel). Para em dispositivos móveis, anexar uma calculadora pesada de inferência de modelo executor pode melhorar o desempenho de um aplicativo em tempo real, ativa a localidade da linha de execução.

Confira abaixo um exemplo trivial de CalculatorGraphConfig, em que temos uma série de calculadoras de passagem dupla :

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

O MediaPipe oferece uma representação alternativa de C++ para gráficos complexos, como pipelines de ML, gerenciamento de metadados de modelo, nós opcionais etc. O gráfico acima pode ter esta aparência:

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

Veja mais detalhes em Como criar gráficos em C++.

Subgráfico

Modularizar um CalculatorGraphConfig em submódulos e ajudar na reutilização de soluções de percepção, um gráfico do MediaPipe pode ser definido como um Subgraph. A A interface pública de um subgráfico consiste em um conjunto de streams de entrada e saída semelhante à interface pública de uma calculadora. Assim, o subgráfico pode ser incluído uma CalculatorGraphConfig como se fosse uma calculadora. Quando um gráfico do MediaPipe carregado de um CalculatorGraphConfig, cada nó do subgráfico é substituído pelo gráfico correspondente de calculadoras. Como resultado, a semântica e o desempenho do subgráfico é idêntico ao gráfico de calculadoras correspondente.

Confira abaixo um exemplo de como criar um subgráfico chamado TwoPassThroughSubgraph.

  1. Definindo o subgráfico.

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

    A interface pública para o subgráfico consiste em:

    • Criar gráficos de streams de entrada
    • Criar gráficos de streams de saída
    • Representar graficamente pacotes laterais de entrada
    • Representar graficamente pacotes laterais de saída
  2. Registre o subgráfico usando a regra BUILD mediapipe_simple_subgraph. A O parâmetro register_as define o nome do componente para o novo subgráfico.

    # 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. Use o subgráfico no gráfico principal.

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

Opções de gráfico

É possível especificar "opções do gráfico" protobuf para um gráfico do MediaPipe semelhante ao Calculator Options protobuf especificado para uma calculadora do MediaPipe. Essas "opções de gráfico" podem ser especificado onde um gráfico é invocado e usado para preencher opções de calculadora e as opções do subgráfico dentro do gráfico.

Em um CalculatorGraphConfig, é possível especificar opções de gráfico para um subgráfico exatamente como as opções de calculadora, como mostrado abaixo:

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

Em um CalculatorGraphConfig, as opções de gráfico podem ser aceitas e usadas para preencher calculadora, como mostrado abaixo:

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

Neste exemplo, o FaceDetectionSubgraph aceita o protobuf da opção de gráfico. FaceDetectionOptions. O FaceDetectionOptions é usado para definir algum campo nas opções da calculadora ImageToTensorCalculatorOptions e em algum campo nas opções de subgráfico InferenceCalculatorOptions. Os valores dos campos são definidas usando a sintaxe option_value:.

No protobuf CalculatorGraphConfig::Node, os campos node_options: e A função option_value: define os valores das opções para uma calculadora, como ImageToTensorCalculator. O campo node_options: define um conjunto de valores valores constantes usando a sintaxe protobuf de texto. Cada campo option_value: define o valor de um campo protobuf usando as informações do campo gráfico, especificamente de valores de campo das opções gráficas do objeto gráfico. No exemplo acima, o option_value: "output_tensor_width:options/tensor_width" define o campo ImageToTensorCalculatorOptions.output_tensor_width usando o valor de FaceDetectionOptions.tensor_width.

A sintaxe de option_value: é semelhante à sintaxe de input_stream:. A a sintaxe é option_value: "LHS:RHS". O LHS identifica uma opção de calculadora e RHS identifica um campo de opção de gráfico. Mais especificamente, o LHS e RHS consiste em uma série de nomes de campo protobuf que identificam Mensagens e campos protobuf separados por "/". Isso é conhecido como "ProtoPath" . As mensagens aninhadas referenciadas no LHS ou RHS já devem ser definidos no protobuf incluído para ser processado usando option_value::

Ciclos

Por padrão, o MediaPipe exige que os gráficos da calculadora sejam acíclicos e trata ciclos. em um gráfico como erros. Se a intenção é que um gráfico tenha ciclos, eles precisam com anotações na configuração do gráfico. Esta página descreve como fazer isso.

OBSERVAÇÃO: a abordagem atual é experimental e está sujeita a mudanças. Aceitamos seu feedback.

Use o teste de unidade CalculatorGraphTest.Cycle em mediapipe/framework/calculator_graph_test.cc como exemplo de código. O exemplo abaixo o gráfico cíclico no teste. A saída sum do somador é a soma: números inteiros gerados pela calculadora de origem de números inteiros.

um gráfico cíclico que adiciona um fluxo de números inteiros

Este gráfico simples ilustra todos os problemas no suporte a gráficos cíclicos.

Anotação da borda traseira

É necessário que uma borda em cada ciclo seja anotada como uma borda de fundo. Isso permite Classificação topológica do MediaPipe para funcionar após a remoção de todas as bordas de trás.

Geralmente, há várias maneiras de selecionar as bordas traseiras. Quais bordas estão marcadas já que os back-ends afetam quais nós são considerados como upstream e quais considerados downstream, o que, por sua vez, afeta as prioridades atribuídas pelo MediaPipe aos nós.

Por exemplo, o teste CalculatorGraphTest.Cycle marca a borda old_sum como uma na borda de retorno, ou seja, o nó de atraso é considerado um nó downstream do nó e recebe uma prioridade mais alta. Como alternativa, podemos marcar o sum entrada para o nó de atraso como a borda de fundo. Nesse caso, o nó de atraso seria considerado um nó upstream do nó de somador e recebe uma prioridade mais baixa.

Pacote inicial

Para que a calculadora do somatório possa ser executada quando o primeiro número inteiro do número inteiro origem chegar, precisamos de um pacote inicial, com valor 0 e com a mesma carimbo de data/hora, no fluxo de entrada old_sum para o adicionador. O pacote inicial será gerado pela calculadora de atraso no método Open().

Atraso em um loop

Cada repetição precisa gerar um atraso para alinhar a saída sum anterior com a próxima a entrada de um número inteiro. Isso também é feito pelo nó de atraso. Portanto, o nó de atraso precisa saber o seguinte sobre os carimbos de data/hora da calculadora de fonte de números inteiros:

  • O carimbo de data/hora da primeira saída.

  • O delta de carimbo de data/hora entre saídas sucessivas.

Planejamos adicionar uma política alternativa de agendamento que se preocupa apenas com os pacotes e ignora os carimbos de data/hora de pacotes, o que eliminará esse inconveniente.

Encerramento antecipado de uma calculadora quando um stream de entrada é concluído

Por padrão, o MediaPipe chama o método Close() de uma calculadora que não é de origem quando todos os streams de entrada estão concluídos. No gráfico de exemplo, queremos parar do adicionador assim que a origem do número inteiro for concluída. Isso é alcançado ao a configuração do nó de adicionar com um gerenciador alternativo de fluxo de entrada EarlyCloseInputStreamHandler:

Código-fonte relevante

Calculadora de atraso

Observe o código em Open() que gera o pacote inicial e o código Process() que adiciona um atraso (unidade) aos pacotes de entrada. Como mencionado acima, nó de atraso presume que seu fluxo de saída é usado junto com um fluxo de entrada com carimbos de data/hora do pacote 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();
  }
};

Configuração do gráfico

Observe a anotação back_edge e o input_stream_handler alternativo.

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