Graphiques

Graphique

Un proto CalculatorGraphConfig spécifie la topologie et les fonctionnalités d'un graphique MediaPipe. Chaque node du graphique représente un calculateur ou un sous-graphique particulier et spécifie les configurations nécessaires, telles que le type de calcul ou de sous-graphe enregistré, les entrées, les sorties et les champs facultatifs, tels que les options spécifiques au nœud, la règle de saisie et l'exécuteur, décrits dans la section Synchronisation.

CalculatorGraphConfig comporte plusieurs autres champs permettant de configurer des paramètres globaux au niveau du graphique, tels que les configurations de l'exécuteur de graphique, le nombre de threads et la taille maximale de la file d'attente des flux d'entrée. Plusieurs paramètres au niveau du graphique sont utiles pour ajuster les performances du graphique sur différentes plates-formes (par exemple, ordinateur et mobile). Par exemple, sur mobile, l'association d'un simulateur d'inférence de modèle lourd à un exécuteur distinct peut améliorer les performances d'une application en temps réel, car cela permet la localité des threads.

Vous trouverez ci-dessous un exemple CalculatorGraphConfig simple dans lequel nous disposons d'une série de calculateurs de contournement :

# 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 propose une autre représentation C++ pour les graphes complexes (par exemple, pipelines de ML, gestion des métadonnées de modèle, nœuds facultatifs, etc.). Le graphique ci-dessus peut se présenter comme suit:

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

Pour en savoir plus, consultez Créer des graphiques en C++.

Sous-graphique

Pour modulariser un CalculatorGraphConfig en sous-modules et faciliter la réutilisation des solutions de perception, un graphique MediaPipe peut être défini en tant que Subgraph. L'interface publique d'un sous-graphe est constituée d'un ensemble de flux d'entrée et de sortie semblables à l'interface publique d'une calculatrice. Le sous-graphique peut ensuite être inclus dans un CalculatorGraphConfig comme s'il s'agissait d'une calculatrice. Lorsqu'un graphe MediaPipe est chargé à partir d'un CalculatorGraphConfig, chaque nœud de sous-graphique est remplacé par le graphe de calculateurs correspondant. Par conséquent, la sémantique et les performances du sous-graphique sont identiques à celles du graphe de calculateurs correspondant.

Vous trouverez ci-dessous un exemple de création d'un sous-graphique nommé TwoPassThroughSubgraph.

  1. Définir le sous-graphique

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

    L'interface publique du sous-graphique comprend les éléments suivants:

    • Représenter graphiquement des flux d'entrée
    • Flux de sortie graphique
    • Représenter graphiquement les paquets côté entrée
    • Paquets côté sortie graphique
  2. Enregistrez le sous-graphique à l'aide de la règle BUILD mediapipe_simple_subgraph. Le paramètre register_as définit le nom du composant du nouveau sous-graphique.

    # 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. Utilisez le sous-graphique dans le graphique 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"
    }
    

Options du graphique

Il est possible de spécifier un tampon de protocole "options de graphique" pour un graphique MediaPipe semblable au tampon de protocole Calculator Options spécifié pour un calculateur MediaPipe. Ces "options de graphique" peuvent être spécifiées là où un graphique est appelé, et utilisées pour renseigner les options du calculateur et les options de sous-graphe dans le graphique.

Dans un CalculatorGraphConfig, les options de graphique peuvent être spécifiées pour un sous-graphique exactement comme les options de la calculatrice, comme indiqué ci-dessous:

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

Dans un CalculatorGraphConfig, les options de graphique peuvent être acceptées et utilisées pour renseigner les options de calculatrice, comme indiqué ci-dessous:

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

Dans cet exemple, FaceDetectionSubgraph accepte l'option de graphique protobuf FaceDetectionOptions. FaceDetectionOptions permet de définir certaines valeurs de champ dans les options du simulateur (ImageToTensorCalculatorOptions) et d'autres valeurs de champ dans les options de sous-graphe InferenceCalculatorOptions. Les valeurs des champs sont définies à l'aide de la syntaxe option_value:.

Dans le tampon de protocole CalculatorGraphConfig::Node, les champs node_options: et option_value: définissent ensemble les valeurs d'option pour un simulateur tel que ImageToTensorCalculator. Le champ node_options: définit un ensemble de valeurs littérales constantes à l'aide de la syntaxe protobuf de texte. Chaque champ option_value: définit la valeur d'un champ protobuf à l'aide des informations du graphique englobant, en particulier des valeurs de champ des options de graphe du graphique englobant. Dans l'exemple ci-dessus, l'élément option_value: "output_tensor_width:options/tensor_width" définit le champ ImageToTensorCalculatorOptions.output_tensor_width à l'aide de la valeur de FaceDetectionOptions.tensor_width.

La syntaxe de option_value: est semblable à la syntaxe de input_stream:. La syntaxe est option_value: "LHS:RHS". Le LHS identifie un champ d'option de calculateur et le RHS identifie un champ d'option de graphique. Plus précisément, les parties LHS et RHS sont chacune constituées d'une série de noms de champs protobuf identifiant les messages protobufs imbriqués et les champs séparés par "/". C'est ce que l'on appelle la syntaxe "ProtoPath". Les messages imbriqués référencés dans le LHS ou le côté droit doivent déjà être définis dans le tampon de protocole englobant pour pouvoir être traversés à l'aide de option_value:.

Cycles

Par défaut, MediaPipe exige que les graphiques de la calculatrice soient acycliques et traite les cycles d'un graphe comme des erreurs. Si un graphe est destiné à comporter des cycles, ceux-ci doivent être annotés dans la configuration du graphique. Cette page vous explique comment procéder.

REMARQUE: L'approche actuelle est expérimentale et susceptible d'être modifiée. N'hésitez pas à nous faire part de vos commentaires.

Veuillez utiliser le test unitaire CalculatorGraphTest.Cycle dans mediapipe/framework/calculator_graph_test.cc comme exemple de code. Le graphique cyclique du test est illustré ci-dessous. La sortie sum de l'additionneur correspond à la somme des entiers générés par le calculateur de source d'entiers.

un graphe cyclique qui ajoute un flux d&#39;entiers

Ce graphe simple illustre tous les problèmes liés aux graphes cycliques pris en charge.

Annotation de la bordure arrière

Une arête doit être annotée en tant que bord arrière dans chaque cycle. Cela permet au tri topologique de MediaPipe de fonctionner, une fois toutes les bordures arrière supprimées.

Il existe généralement plusieurs façons de sélectionner les bords arrière. Les arêtes marquées comme arêtes arrière ont une incidence sur les nœuds considérés comme en amont et sur les nœuds considérés comme en aval, ce qui affecte les priorités que MediaPipe attribue aux nœuds.

Par exemple, le test CalculatorGraphTest.Cycle marque le bord old_sum comme étant un bord arrière. Le nœud Delay est donc considéré comme un nœud en aval du nœud d'addition et reçoit une priorité plus élevée. Nous pouvons également marquer l'entrée sum sur le nœud de délai comme étant le bord arrière, auquel cas le nœud de délai sera considéré comme un nœud en amont du nœud d'addition et se voit attribuer une priorité inférieure.

Paquet initial

Pour que le calculateur d'additionneur puisse être exécuté lorsque le premier entier de la source d'entiers arrive, nous avons besoin d'un paquet initial, de valeur 0 et avec le même horodatage, sur le flux d'entrée old_sum vers l'additionnaire. Ce paquet initial doit être généré par le calculateur de délai dans la méthode Open().

Retard dans une boucle

Chaque boucle doit entraîner un délai pour aligner la sortie sum précédente sur la prochaine entrée entière. Cette opération est également effectuée par le nœud de délai. Le nœud de délai doit donc connaître les informations suivantes sur les horodatages du calculateur de source d'entiers:

  • Horodatage de la première sortie.

  • Le delta de code temporel entre les sorties successives.

Nous prévoyons d'ajouter une autre règle de planification qui se concentre uniquement sur l'ordre des paquets et ignore les horodatages de paquets, ce qui éliminera ce désagrément.

Arrêt anticipé d'une calculatrice lorsqu'un flux d'entrée est terminé

Par défaut, MediaPipe appelle la méthode Close() d'un calculateur non source lorsque tous ses flux d'entrée sont terminés. Dans l'exemple de graphique, nous voulons arrêter le nœud d'ajout dès que la source de l'entier est terminée. Pour ce faire, vous devez configurer le nœud d'addition avec un autre gestionnaire de flux d'entrée, EarlyCloseInputStreamHandler.

Code source pertinent

Calculateur de délai

Notez le code dans Open() qui génère le paquet initial et le code dans Process() qui ajoute un délai (unité) aux paquets d'entrée. Comme indiqué ci-dessus, ce nœud de délai suppose que son flux de sortie est utilisé avec un flux d'entrée avec les horodatages de paquet 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();
  }
};

Configuration du graphique

Notez l'annotation back_edge et l'autre input_stream_handler.

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