Gráficos

Gráfico

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

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

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

# 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 C++ alternativa para gráficos complexos, como pipelines de ML, gerenciamento de metadados do 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

Para 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 interface pública de um subgráfico consiste em um conjunto de streams de entrada e saída semelhantes à interface pública de uma calculadora. O subgráfico pode ser incluído em um CalculatorGraphConfig como se fosse uma calculadora. Quando um gráfico do MediaPipe é carregado de um CalculatorGraphConfig, cada nó de subgráfico é substituído pelo gráfico correspondente de calculadoras. Como resultado, a semântica e o desempenho do subgráfico são idênticos ao gráfico de calculadoras correspondente.

Veja abaixo um exemplo de como criar um subgráfico com o nome TwoPassThroughSubgraph.

  1. Definir 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
    • Criar gráficos com pacotes laterais de entrada
    • Representar graficamente pacotes secundários de saída
  2. Registre o subgráfico usando a regra BUILD mediapipe_simple_subgraph. 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 um protobuf "graph options" para um gráfico do MediaPipe semelhante ao buffer de protocolo Calculator Options especificado para uma calculadora do MediaPipe. Essas "opções de gráfico" podem ser especificadas quando um gráfico é invocado e usadas para preencher as opções da calculadora e do subgráfico.

Em um CalculatorGraphConfig, as opções de gráfico podem ser especificadas para um subgráfico, assim como as opções de calculadora, conforme 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 as opções da calculadora, conforme 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 a opção de gráfico protobuf FaceDetectionOptions. O FaceDetectionOptions é usado para definir alguns valores de campo nas opções da calculadora ImageToTensorCalculatorOptions e alguns valores nas opções do subgráfico InferenceCalculatorOptions. Os valores dos campos são definidos usando a sintaxe option_value:.

No protobuf CalculatorGraphConfig::Node, os campos node_options: e option_value: juntos definem os valores de opção para uma calculadora como ImageToTensorCalculator. O campo node_options: define um conjunto de valores constantes literais usando a sintaxe protobuf de texto. Cada campo option_value: define o valor de um campo protobuf usando informações do gráfico delimitador, especificamente com base nos valores de campo das opções do gráfico contínuo. 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 sintaxe é option_value: "LHS:RHS". O LHS identifica um campo de opções de calculadora, e o RHS identifica um campo de opção de gráfico. Mais especificamente, o LHS e o RHS consistem em uma série de nomes de campos protobuf que identificam mensagens protobuf aninhadas e campos separados por "/". Isso é conhecido como a sintaxe "ProtoPath". As mensagens aninhadas que são referenciadas no LHS ou RHS já precisam estar definidas no protobuf delimitado para serem transferidas usando option_value:.

Ciclos

Por padrão, o MediaPipe exige que os gráficos da calculadora sejam acíclicos e trata os ciclos em um gráfico como erros. Se você pretende que um gráfico tenha ciclos, eles precisam ser anotados na configuração do gráfico. Veja nesta página como fazer isso.

OBSERVAÇÃO: a abordagem atual é experimental e está sujeita a alterações. Queremos saber sua opinião.

Use o teste de unidade de CalculatorGraphTest.Cycle em mediapipe/framework/calculator_graph_test.cc como exemplo de código. Veja abaixo o gráfico cíclico do teste. A saída sum do somador é a soma dos 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 na compatibilidade com gráficos cíclicos.

Anotação de borda traseira

Exigimos que uma borda em cada ciclo seja anotada como uma borda traseira. Isso permite que a classificação topológica do MediaPipe funcione, depois de remover todas as bordas traseiras.

Geralmente, há várias maneiras de selecionar as bordas traseiras. Quais bordas são marcadas como borda de fundo afeta quais nós são considerados upstream e quais nós são considerados como downstream, o que, por sua vez, afeta as prioridades que o MediaPipe atribui aos nós.

Por exemplo, o teste CalculatorGraphTest.Cycle marca a borda old_sum como uma borda de trás. Portanto, o nó de atraso é considerado um nó downstream do nó de adição e recebe uma prioridade mais alta. Como alternativa, podemos marcar a entrada sum no nó de atraso como a borda de trás. Nesse caso, o nó de atraso seria considerado como um nó upstream do nó de adição e recebe uma prioridade mais baixa.

Pacote inicial

Para que a calculadora de adição possa ser executada quando o primeiro número inteiro da origem de números inteiros chegar, precisamos de um pacote inicial, com valor 0 e com o mesmo carimbo de data/hora, no fluxo de entrada old_sum para o somador. Esse pacote inicial precisa ser enviado pela calculadora de atraso no método Open().

Atraso em um loop

Cada loop pode gerar um atraso para alinhar a saída sum anterior com a próxima entrada de números inteiros. 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 origem de números inteiros:

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

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

Planejamos adicionar uma política de programação alternativa que se preocupe apenas com a ordenação de pacotes e ignora os carimbos de data/hora dos 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 é fonte quando todos os streams de entrada são concluídos. No gráfico de exemplo, queremos interromper o nó adicional assim que a origem do número inteiro for concluída. Isso é feito configurando o nó de adição com um gerenciador de stream de entrada alternativo, EarlyCloseInputStreamHandler.

Código-fonte relevante

Calculadora de atraso

Observe o código em Open() que gera o pacote inicial e o código em Process() que adiciona um atraso (unidade) aos pacotes de entrada. Como mencionado acima, esse nó de atraso supõe que o stream de saída seja usado com um stream 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 de gráfico

Observe a anotação back_edge e a 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'
}