Графики

График

Прототип CalculatorGraphConfig определяет топологию и функциональность графа MediaPipe. Каждый node в графе представляет конкретный калькулятор или подграф и определяет необходимые конфигурации, такие как зарегистрированный тип калькулятора/подграфа, входные и выходные данные и дополнительные поля, такие как параметры, специфичные для узла, политика ввода и исполнитель, обсуждаемые в разделе Синхронизация .

CalculatorGraphConfig имеет несколько других полей для настройки глобальных параметров уровня графа, например, конфигурации исполнителя графа, количество потоков и максимальный размер очереди входных потоков. Несколько настроек на уровне графа полезны для настройки производительности графика на разных платформах (например, настольных и мобильных устройствах). Например, на мобильных устройствах подключение тяжелого калькулятора вывода модели к отдельному исполнителю может повысить производительность приложения реального времени, поскольку это обеспечивает локальность потока.

Ниже приведен тривиальный пример CalculatorGraphConfig , в котором у нас есть серия сквозных калькуляторов:

# 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 предлагает альтернативное представление C++ для сложных графов (например, конвейеров машинного обучения, обработки метаданных модели, дополнительных узлов и т. д.). Приведенный выше график может выглядеть так:

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

Более подробную информацию см. в разделе «Построение графиков на C++» .

Подграф

Чтобы разбить CalculatorGraphConfig на подмодули и облегчить повторное использование решений по восприятию, граф MediaPipe можно определить как Subgraph . Открытый интерфейс подграфа состоит из набора входных и выходных потоков, аналогичных общедоступному интерфейсу калькулятора. Затем подграф можно включить в CalculatorGraphConfig , как если бы это был калькулятор. Когда граф MediaPipe загружается из CalculatorGraphConfig , каждый узел подграфа заменяется соответствующим графом калькуляторов. В результате семантика и производительность подграфа идентична соответствующему графу калькуляторов.

Ниже приведен пример создания подграфа с именем TwoPassThroughSubgraph .

  1. Определение подграфа.

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

    Публичный интерфейс подграфа состоит из:

    • Входные потоки графов
    • Потоки вывода графа
    • Входные пакеты графа
    • Выходные пакеты графа
  2. Зарегистрируйте подграф, используя правило BUILD mediapipe_simple_subgraph . Параметр register_as определяет имя компонента для нового подграфа.

    # 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. Используйте подграф в основном графике.

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

Параметры графика

Для графа MediaPipe можно указать протобуф «параметры графика», аналогичный протобуфу Calculator Options , указанному для калькулятора MediaPipe. Эти «параметры графика» можно указать при вызове графика и использовать для заполнения параметров калькулятора и параметров подграфа внутри графика.

В CalculatorGraphConfig параметры графика могут быть указаны для подграфа точно так же, как параметры калькулятора, как показано ниже:

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

В CalculatorGraphConfig можно принять параметры графика и использовать их для заполнения параметров калькулятора, как показано ниже:

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

В этом примере FaceDetectionSubgraph принимает опцию графа protobuf FaceDetectionOptions . FaceDetectionOptions используется для определения некоторых значений полей в параметрах калькулятора ImageToTensorCalculatorOptions и некоторых значений полей в параметрах подграфа InferenceCalculatorOptions . Значения полей определяются с использованием синтаксиса option_value:

В CalculatorGraphConfig::Node protobuf поля node_options: и option_value: вместе определяют значения параметров для калькулятора, такого как ImageToTensorCalculator . Поле node_options: определяет набор литеральных константных значений с использованием текстового синтаксиса protobuf. Каждое поле option_value: определяет значение для одного поля protobuf, используя информацию из включающего графа, в частности, из значений полей опций включающего графа. В приведенном выше примере option_value: "output_tensor_width:options/tensor_width" определяет поле ImageToTensorCalculatorOptions.output_tensor_width с использованием значения FaceDetectionOptions.tensor_width .

Синтаксис option_value: аналогичен синтаксису input_stream: . Синтаксис: option_value: "LHS:RHS" . Левая шкала идентифицирует поле опции калькулятора, а правая шкала идентифицирует поле опции графика. Более конкретно, LHS и RHS состоят из серии имен полей protobuf, идентифицирующих вложенные сообщения и поля protobuf, разделенные символом «/». Это известно как синтаксис «ProtoPath». Вложенные сообщения, на которые есть ссылки в LHS или RHS, должны быть уже определены во включающем protobuf, чтобы их можно было пройти с помощью option_value: .

Циклы

По умолчанию MediaPipe требует, чтобы графики калькулятора были ациклическими, и рассматривает циклы в графике как ошибки. Если граф предназначен для циклов, циклы должны быть аннотированы в конфигурации графа. На этой странице описано, как это сделать.

ПРИМЕЧАНИЕ. Текущий подход является экспериментальным и может быть изменен. Мы приветствуем ваши отзывы.

Используйте модульный тест CalculatorGraphTest.Cycle в mediapipe/framework/calculator_graph_test.cc в качестве примера кода. Ниже показан циклический график теста. Выходная sum сумматора представляет собой сумму целых чисел, сгенерированных калькулятором целочисленного источника.

a cyclic graph that adds a stream of integers

Этот простой график иллюстрирует все проблемы поддержки циклических графов.

Аннотация на заднем крае

Мы требуем, чтобы ребро в каждом цикле было аннотировано как заднее ребро. Это позволяет топологической сортировке MediaPipe работать после удаления всех задних ребер.

Обычно существует несколько способов выбора задних краев. Какие ребра помечены как задние ребра, влияют на то, какие узлы считаются восходящими, а какие — нисходящими, что, в свою очередь, влияет на приоритеты, которые MediaPipe назначает узлам.

Например, тест CalculatorGraphTest.Cycle помечает ребро old_sum как заднее ребро, поэтому узел Delay считается нижестоящим узлом узла сумматора и получает более высокий приоритет. В качестве альтернативы мы могли бы пометить ввод sum в узел задержки как задний фронт, и в этом случае узел задержки будет рассматриваться как восходящий узел узла сумматора и ему будет присвоен более низкий приоритет.

Начальный пакет

Чтобы калькулятор сумматора мог работать при поступлении первого целого числа из источника целых чисел, нам нужен начальный пакет со значением 0 и с той же меткой времени во входном потоке old_sum в сумматор. Этот первоначальный пакет должен быть выведен калькулятором задержки в методе Open() .

Задержка в цикле

Каждый цикл должен вызывать задержку для выравнивания предыдущего вывода sum со следующим целочисленным входом. Это также выполняется узлом задержки. Таким образом, узлу задержки необходимо знать следующее о временных метках калькулятора целочисленного источника:

  • Временная метка первого вывода.

  • Разница во времени между последовательными выходными данными.

Мы планируем добавить альтернативную политику планирования, которая заботится только о порядке пакетов и игнорирует временные метки пакетов, что устранит это неудобство.

Раннее завершение работы калькулятора после завершения одного входного потока

По умолчанию MediaPipe вызывает метод Close() калькулятора, не являющегося исходным кодом, когда все его входные потоки завершены. В примере графика мы хотим остановить узел сумматора, как только будет завершено получение целочисленного источника. Это достигается путем настройки узла сумматора с альтернативным обработчиком входного потока EarlyCloseInputStreamHandler .

Соответствующий исходный код

Калькулятор задержки

Обратите внимание на код Open() , который выводит исходный пакет, и код в Process() , который добавляет (единичную) задержку к входным пакетам. Как отмечалось выше, этот узел задержки предполагает, что его выходной поток используется вместе с входным потоком с временными метками пакетов 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();
  }
};

Конфигурация графика

Обратите внимание на аннотацию back_edge и альтернативный 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'
}