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
.
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
Registre o subgráfico usando a regra BUILD
mediapipe_simple_subgraph
. O parâmetroregister_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", ], )
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.
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'
}