Gráficos

Gráfico

Un protocolo CalculatorGraphConfig especifica la topología y la funcionalidad de un gráfico de MediaPipe. Cada node del gráfico representa una calculadora o subgrafo en particular, y especifica las configuraciones necesarias, como el tipo de calculadora o subgrafo registrado, las entradas, las salidas y los campos opcionales, como las opciones específicas del nodo, la política de entrada y el ejecutor, que se analizan en Sincronización.

CalculatorGraphConfig tiene muchos otros campos para establecer la configuración global a nivel del gráfico, p.ej., los parámetros de configuración del ejecutor de grafos, la cantidad de subprocesos y el tamaño máximo de cola de los flujos de entrada. Varias opciones de configuración a nivel del gráfico son útiles para ajustar el rendimiento del gráfico en diferentes plataformas (p. ej., computadoras de escritorio en comparación con dispositivos móviles). Por ejemplo, en un dispositivo móvil, conectar una calculadora de inferencia de modelo pesada a un ejecutor independiente puede mejorar el rendimiento de una aplicación en tiempo real, ya que esto habilita la localidad de subprocesos.

A continuación, se muestra un ejemplo trivial de CalculatorGraphConfig en el que tenemos una serie de calculadoras de transferencia :

# 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 ofrece una representación alternativa de C++ para grafos complejos (p. ej., canalizaciones de AA, control de metadatos del modelo, nodos opcionales, etcétera). El gráfico anterior podría verse de la siguiente manera:

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

Consulta más detalles en Cómo compilar gráficos en C++.

Subgrafo

Para modularizar un CalculatorGraphConfig en submódulos y ayudar a reutilizar las soluciones de percepción, un gráfico de MediaPipe se puede definir como un Subgraph. La interfaz pública de un subgrafo consta de un conjunto de flujos de entrada y salida similares a la interfaz pública de una calculadora. Luego, se puede incluir el subgrafo en un CalculatorGraphConfig como si fuera una calculadora. Cuando se carga un gráfico de MediaPipe desde un CalculatorGraphConfig, cada nodo del subgrafo se reemplaza por el gráfico de calculadoras correspondiente. Como resultado, la semántica y el rendimiento del subgrafo son idénticos a los del grafo de calculadoras correspondiente.

A continuación, se muestra un ejemplo de cómo crear un subgrafo llamado TwoPassThroughSubgraph.

  1. Define el subgrafo.

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

    La interfaz pública del subgrafo consta de los siguientes elementos:

    • Representar gráficamente transmisiones de entrada
    • Representar gráficamente transmisiones de salida
    • Representar gráficamente paquetes laterales de entrada
    • Representar gráficamente paquetes de salida
  2. Registra el subgrafo con la regla BUILD mediapipe_simple_subgraph. El parámetro register_as define el nombre del componente del nuevo subgrafo.

    # 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. Usa el subgrafo del 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"
    }
    

Opciones gráficas

Es posible especificar un protobuf de “opciones de gráfico” para un gráfico de MediaPipe similar al protobuf de Calculator Options especificado para una calculadora de MediaPipe. Estas "opciones de gráfico" se pueden especificar dónde se invoca un gráfico y se usan para propagar las opciones de la calculadora y las opciones de subgrafos dentro del gráfico.

En un objeto CalculatorGraphConfig, las opciones de gráfico se pueden especificar para un subgrafo de la misma manera que las opciones de la calculadora, como se muestra a continuación:

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

En un objeto CalculatorGraphConfig, se pueden aceptar las opciones de gráfico y usarse para propagar las opciones de la calculadora, como se muestra a continuación:

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

En este ejemplo, FaceDetectionSubgraph acepta la opción de gráfico protobuf FaceDetectionOptions. FaceDetectionOptions se usa para definir algunos valores de campo en las opciones de la calculadora ImageToTensorCalculatorOptions y algunos valores de campo en las opciones del subgrafo InferenceCalculatorOptions. Los valores de campo se definen con la sintaxis option_value:.

En el protobuf de CalculatorGraphConfig::Node, los campos node_options: y option_value: definen juntos los valores de opción para una calculadora como ImageToTensorCalculator. El campo node_options: define un conjunto de valores literales constantes con la sintaxis de protobuf de texto. Cada campo option_value: define el valor para un campo de protobuf con información del gráfico contenedor, específicamente a partir de valores de campo de las opciones de gráfico del gráfico contenedor. En el ejemplo anterior, el option_value: "output_tensor_width:options/tensor_width" define el campo ImageToTensorCalculatorOptions.output_tensor_width con el valor de FaceDetectionOptions.tensor_width.

La sintaxis de option_value: es similar a la de input_stream:. La sintaxis es option_value: "LHS:RHS". El LHS identifica un campo de opción de calculadora y el RHS identifica un campo de opciones de gráfico. Más específicamente, el LHS y el RHS consisten en una serie de nombres de campos protobuf que identifican mensajes protobuf anidados y campos separados por “/”, lo que se conoce como la sintaxis “ProtoPath”. Los mensajes anidados a los que se hace referencia en el LHS o el RHS ya deben estar definidos en el protobuf de cierre para poder desviarse con option_value:.

Ciclos

De forma predeterminada, MediaPipe requiere que los grafos de la calculadora sean acíclicos y trata los ciclos de un grafo como errores. Si un grafo está destinado a tener ciclos, estos deben anotarse en la configuración del grafo. En esta página, se describe cómo hacerlo.

NOTA: El enfoque actual es experimental y está sujeto a cambios. Tus comentarios son bienvenidos.

Usa la prueba de unidades CalculatorGraphTest.Cycle en mediapipe/framework/calculator_graph_test.cc como código de muestra. A continuación, se muestra el grafo cíclico de la prueba. La salida de sum del sumador es la suma de los números enteros generados por la calculadora de origen de números enteros.

un grafo cíclico que suma un flujo de números enteros

En este gráfico simple, se ilustran todos los problemas de compatibilidad con grafos cíclicos.

Anotación del borde posterior

Es necesario que se anote un borde de cada ciclo como un borde trasero. Esto permite que funcione la ordenación topológica de MediaPipe después de quitar todos los bordes traseros.

Por lo general, hay varias formas de seleccionar los bordes posteriores. Qué perímetros se marcan como perimetrales posteriores afectan qué nodos se consideran ascendentes y qué nodos se consideran descendentes, lo que, a su vez, afecta las prioridades que MediaPipe asigna a los nodos.

Por ejemplo, la prueba CalculatorGraphTest.Cycle marca el borde old_sum como un borde trasero, por lo que el nodo de retraso se considera como un nodo descendente del nodo sumador y recibe una prioridad más alta. Como alternativa, podríamos marcar la entrada sum en el nodo de retraso como el borde trasero, en cuyo caso el nodo de retraso se considerará como un nodo upstream del nodo sumador y tendrá una prioridad más baja.

Paquete inicial

Para que la calculadora de suma se pueda ejecutar cuando llega el primer número entero de la fuente de números enteros, necesitamos un paquete inicial, con valor 0 y la misma marca de tiempo, en el flujo de entrada old_sum al sumador. La calculadora de retrasos debe generar este paquete inicial en el método Open().

Demora en un bucle

Cada bucle debe generar un retraso para alinear el resultado anterior de sum con la siguiente entrada de número entero. Esto también lo hace el nodo de retraso. Por lo tanto, el nodo de retraso debe saber lo siguiente sobre las marcas de tiempo de la calculadora de fuentes de números enteros:

  • La marca de tiempo del primer resultado.

  • El delta de la marca de tiempo entre salidas sucesivas

Planeamos agregar una política de programación alternativa que solo se preocupe por el orden de paquetes y que ignore las marcas de tiempo del paquete, lo que eliminará este inconveniente.

Finalización anticipada de una calculadora cuando se completa un flujo de entrada

De forma predeterminada, MediaPipe llama al método Close() de una calculadora que no es de origen cuando todas sus transmisiones de entrada están listas. En el gráfico de ejemplo, queremos detener el nodo de suma en cuanto esté lista la fuente del número entero. Para ello, configura el nodo de suma con un controlador de flujo de entrada alternativo, EarlyCloseInputStreamHandler.

Código fuente relevante

Calculadora de retraso

Observa el código en Open() que genera el paquete inicial y el código en Process() que agrega un retraso (unidad) a los paquetes de entrada. Como se indicó antes, este nodo de demora supone que su transmisión de salida se usa junto con una transmisión de entrada con marcas de tiempo de paquetes 0, 1, 2, 3, etc.

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

Configuración del grafo

Observa la anotación back_edge y la 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'
}