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
.
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
Enregistrez le sous-graphique à l'aide de la règle BUILD
mediapipe_simple_subgraph
. Le paramètreregister_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", ], )
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.
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'
}