Gráficos

Gráfico

Un proto CalculatorGraphConfig especifica la topología y funcionalidad de una Gráfico de MediaPipe Cada node del gráfico representa una calculadora o subgrafo y especifica los parámetros de configuración necesarios, como registros tipo de calculadora o subgrafo, las entradas, las salidas y los campos opcionales, como opciones específicas del nodo, política de entrada y ejecutor, que se analizan en Sincronización.

CalculatorGraphConfig tiene muchos otros campos para configurar el nivel de gráfico global configuración, p.ej., configuración del ejecutor de grafos, cantidad de subprocesos y tamaño máximo de cola de flujos de entrada. Varias configuraciones a nivel de gráfico son útiles para ajustar la el rendimiento del gráfico en distintas plataformas (p. ej., computadoras de escritorio y dispositivos móviles). Para en un dispositivo móvil, adjuntar una calculadora de inferencia de modelo pesada a un modelo el ejecutor puede mejorar el rendimiento de una aplicación en tiempo real, ya que este 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 puede 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();
}

Para obtener más información, consulta Cómo compilar gráficos en C++.

Subgrafo

Para modularizar un CalculatorGraphConfig en submódulos y ayudar con la reutilización de soluciones de percepción, un gráfico de MediaPipe se puede definir como un Subgraph. El interfaz pública de un subgrafo consiste en un conjunto de flujos de entrada y salida similar a la interfaz pública de una calculadora. Luego, el subgrafo se puede incluir en un objeto CalculatorGraphConfig como si fuera una calculadora. Cuando un gráfico de MediaPipe se cargado desde un CalculatorGraphConfig, cada nodo del subgrafo se reemplaza por el gráfico correspondiente de las calculadoras. Como resultado, la semántica y el rendimiento del subgrafo es idéntico al gráfico correspondiente de calculadoras.

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

  1. Cómo definir 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 lo siguiente:

    • Representar gráficamente transmisiones de entrada
    • Representar gráficamente transmisiones de salida
    • Representar gráficamente paquetes laterales de entrada
    • Representar gráficamente los paquetes de salida
  2. Registra el subgrafo con la regla de COMPILACIÓN mediapipe_simple_subgraph. El El parámetro register_as define el nombre del componente para el 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 "opciones de grafo". protobuf para un gráfico MediaPipe de manera similar a Calculator Options protobuf especificado para una calculadora de MediaPipe. Estas “opciones de gráficos” puede ser especificado cuando se invoca un grafo y se usa para propagar las opciones de la calculadora y opciones de subgrafo dentro del gráfico.

En un CalculatorGraphConfig, se pueden especificar opciones de gráfico para un subgrafo. exactamente como 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 una CalculatorGraphConfig, se pueden aceptar opciones de gráfico y usarlas para propagar 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 algún campo en las opciones de la calculadora ImageToTensorCalculatorOptions y algún campo valores en las opciones de subgrafo InferenceCalculatorOptions. Los valores del campo se definen con la sintaxis option_value:.

En el protobuf CalculatorGraphConfig::Node, los campos node_options: y En conjunto, option_value: definen los valores de opción para una calculadora, como ImageToTensorCalculator El campo node_options: define un conjunto de valores valores constantes con la sintaxis de protobuf de texto. Cada campo option_value: define el valor de un campo de protobuf con información de la gráfico, específicamente a partir de los valores de campo de las opciones de gráfico de la gráfico. En el ejemplo anterior, 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:. El la sintaxis es option_value: "LHS:RHS". El LHS identifica una opción de calculadora y el RHS identifica un campo de opción de gráfico. Más concretamente, el LHS y el RHS cada uno consiste en una serie de nombres de campo protobuf que identifican Mensajes y campos protobuf separados por "/". Esto se conoce como la "ProtoPath". sintaxis. Los mensajes anidados a los que se hace referencia en el LHS o el RHS ya deben estar definidos en el protobuf adjunto para que se recorran con option_value:

Ciclos

De forma predeterminada, MediaPipe requiere que los grafos de la calculadora sean acíclicos y trate los ciclos. en un gráfico como errores. Si se pretende que un gráfico tenga ciclos, estos deben 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. Aceptamos tus comentarios.

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

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

En este grafo simple, se ilustran todos los problemas de la compatibilidad con grafos cíclicos.

Anotación del borde trasero

Exigimos que una arista en cada ciclo se anote como borde trasero. Esto permite la ordenación topológica de MediaPipe para que funcione después de quitar todos los bordes posteriores.

Por lo general, hay varias formas de seleccionar los bordes posteriores. ¿Qué bordes están marcados? ya que sus extremos afectan los nodos que se consideran ascendentes consideradas como downstream, 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 una al backend, por lo que el nodo de retraso se considera como un nodo downstream del sumador. y se le da una prioridad más alta. De manera alternativa, podríamos marcar el sum al nodo de retraso como borde posterior, en cuyo caso, el nodo de retraso se considera un nodo upstream del nodo sumador y se le da una prioridad más baja.

Paquete inicial

Para que la calculadora de sumas se pueda ejecutar cuando el primer número entero del número entero llega la fuente, necesitamos un paquete inicial con el valor 0 y el mismo marca de tiempo, en la transmisión de entrada old_sum al sumador. Este paquete inicial La calculadora de retrasos debe generar el resultado en el método Open().

Demora en bucle

Cada bucle debe generar un retraso para alinear la salida anterior de sum con la siguiente de números enteros. Esto también lo hace el nodo de retraso. Así que el nodo de retraso debe ten en cuenta lo siguiente sobre las marcas de tiempo de la calculadora de fuente de números enteros:

  • Es 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 los paquetes como el comando e ignora las marcas de tiempo de los paquetes, lo que eliminará este inconveniente.

Rescisión anticipada de una calculadora cuando finaliza un flujo de entrada

De forma predeterminada, MediaPipe llama al método Close() de una calculadora que no es de origen cuando estén listas todas sus transmisiones de entrada. En el gráfico de ejemplo, queremos detener la en el nodo de suma en cuanto finalice la fuente de número entero. Esto se logra mediante configurar el nodo de adder con un controlador de flujos 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 (de unidad) a los paquetes de entrada. Como se mencionó anteriormente, esta retraso del nodo 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, ...

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