Обзор
MediaPipe поддерживает узлы калькулятора для вычислений и рендеринга на графическом процессоре и позволяет комбинировать несколько узлов графического процессора, а также смешивать их с узлами калькулятора на базе ЦП. На мобильных платформах существует несколько API-интерфейсов графических процессоров (например, OpenGL ES, Metal и Vulkan). MediaPipe не пытается предложить единую абстракцию графического процессора между API. Отдельные узлы могут быть написаны с использованием разных API, что позволяет им при необходимости использовать преимущества конкретных функций платформы.
Поддержка графического процессора необходима для хорошей производительности на мобильных платформах, особенно для видео в реальном времени. MediaPipe позволяет разработчикам писать калькуляторы, совместимые с графическим процессором, которые поддерживают использование графического процессора для:
- Обработка в реальном времени на устройстве, а не только пакетная обработка
- Рендеринг видео и эффекты, а не только анализ
Ниже приведены принципы проектирования поддержки графического процессора в MediaPipe.
- Калькуляторы на базе графического процессора должны иметь возможность появляться в любом месте графика и не обязательно использоваться для рендеринга на экране.
- Передача данных кадра из одного калькулятора на базе графического процессора в другой должна быть быстрой и не требовать дорогостоящих операций копирования.
- Передача данных кадра между ЦП и ГП должна быть настолько эффективной, насколько это позволяет платформа.
- Поскольку для достижения наилучшей производительности на разных платформах могут потребоваться разные методы, API должен обеспечивать гибкость в способах скрытой реализации.
- Калькулятору должна быть предоставлена максимальная гибкость в использовании графического процессора для всех или части его операций, при необходимости объединяя его с центральным процессором.
Поддержка OpenGL ES
MediaPipe поддерживает OpenGL ES до версии 3.2 на Android/Linux и до ES 3.0 на iOS. Кроме того, MediaPipe также поддерживает Metal на iOS.
Для запуска калькуляторов и графиков машинного обучения требуется OpenGL ES 3.1 или более поздняя версия (в системах Android/Linux).
MediaPipe позволяет графам запускать OpenGL в нескольких контекстах GL. Например, это может быть очень полезно в графах, которые сочетают более медленный путь вывода графического процессора (например, со скоростью 10 кадров в секунду) с более быстрым путем рендеринга графического процессора (например, со скоростью 30 кадров в секунду): поскольку один контекст GL соответствует одной последовательной очереди команд, используя один и тот же контекст для обеих задач приведет к снижению частоты кадров рендеринга.
Одной из проблем, которую решает использование MediaPipe нескольких контекстов, является возможность взаимодействия между ними. Примером сценария является сценарий, в котором входное видео отправляется как на пути рендеринга, так и на пути вывода, а для рендеринга необходим доступ к последним выходным данным вывода.
Контекст OpenGL не может быть доступен нескольким потокам одновременно. Более того, переключение активного контекста GL в том же потоке может быть медленным на некоторых устройствах Android. Поэтому наш подход заключается в том, чтобы иметь один выделенный поток для каждого контекста. Каждый поток выдает команды GL, создавая последовательную очередь команд в своем контексте, которая затем асинхронно выполняется графическим процессором.
Жизнь калькулятора графического процессора
В этом разделе представлена базовая структура метода Process калькулятора графического процессора, производного от базового класса GlSimpleCalculator. В качестве примера показан калькулятор графического процессора LuminanceCalculator
. Метод LuminanceCalculator::GlRender
вызывается из GlSimpleCalculator::Process
.
// Converts RGB images into luminance images, still stored in RGB format.
// See GlSimpleCalculator for inputs, outputs and input side packets.
class LuminanceCalculator : public GlSimpleCalculator {
public:
absl::Status GlSetup() override;
absl::Status GlRender(const GlTexture& src,
const GlTexture& dst) override;
absl::Status GlTeardown() override;
private:
GLuint program_ = 0;
GLint frame_;
};
REGISTER_CALCULATOR(LuminanceCalculator);
absl::Status LuminanceCalculator::GlRender(const GlTexture& src,
const GlTexture& dst) {
static const GLfloat square_vertices[] = {
-1.0f, -1.0f, // bottom left
1.0f, -1.0f, // bottom right
-1.0f, 1.0f, // top left
1.0f, 1.0f, // top right
};
static const GLfloat texture_vertices[] = {
0.0f, 0.0f, // bottom left
1.0f, 0.0f, // bottom right
0.0f, 1.0f, // top left
1.0f, 1.0f, // top right
};
// program
glUseProgram(program_);
glUniform1i(frame_, 1);
// vertex storage
GLuint vbo[2];
glGenBuffers(2, vbo);
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// vbo 0
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), square_vertices,
GL_STATIC_DRAW);
glEnableVertexAttribArray(ATTRIB_VERTEX);
glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, nullptr);
// vbo 1
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), texture_vertices,
GL_STATIC_DRAW);
glEnableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
glVertexAttribPointer(ATTRIB_TEXTURE_POSITION, 2, GL_FLOAT, 0, 0, nullptr);
// draw
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// cleanup
glDisableVertexAttribArray(ATTRIB_VERTEX);
glDisableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(2, vbo);
return absl::OkStatus();
}
Упомянутые выше принципы проектирования привели к следующим вариантам проектирования поддержки графического процессора MediaPipe:
- У нас есть тип данных графического процессора, называемый
GpuBuffer
, для представления данных изображения, оптимизированный для использования графического процессора. Точное содержимое этого типа данных непрозрачно и зависит от платформы. - Низкоуровневый API, основанный на композиции, где любой калькулятор, желающий использовать графический процессор, создает экземпляр класса
GlCalculatorHelper
и владеет им. Этот класс предлагает независимый от платформы API для управления контекстом OpenGL, настройки текстур для входных и выходных данных и т. д. - Высокоуровневый API, основанный на подклассах, где простые калькуляторы реализуют подкласс фильтров изображений из
GlSimpleCalculator
, и им нужно всего лишь переопределить пару виртуальных методов с помощью их конкретного кода OpenGL, в то время как суперкласс позаботится обо всей сантехнике. - Данные, которые необходимо совместно использовать всем калькуляторам на базе графического процессора, предоставляются в виде внешних входных данных, которые реализованы как графический сервис и управляются классом
GlCalculatorHelper
. - Комбинация вспомогательных функций, специфичных для калькулятора, и службы общих графов обеспечивает большую гибкость в управлении ресурсами графического процессора: мы можем иметь отдельный контекст для каждого калькулятора, совместно использовать один контекст, совместно использовать блокировку или другие примитивы синхронизации и т. д. — и все этим управляет помощник и скрыто от отдельных калькуляторов.
Конвертеры GpuBuffer в ImageFrame
Мы предоставляем два калькулятора: GpuBufferToImageFrameCalculator
и ImageFrameToGpuBufferCalculator
. Эти калькуляторы конвертируют между ImageFrame
и GpuBuffer
, позволяя строить графики, объединяющие калькуляторы графического процессора и процессора. Они поддерживаются как на iOS, так и на Android.
Когда это возможно, эти калькуляторы используют специфичные для платформы функции для обмена данными между ЦП и ГП без копирования.
На диаграмме ниже показан поток данных в мобильном приложении, которое захватывает видео с камеры, пропускает его через график MediaPipe и отображает выходные данные на экране в режиме реального времени. Пунктирная линия показывает, какие части находятся внутри самого графа MediaPipe. Это приложение запускает фильтр обнаружения границ Canny на ЦП с помощью OpenCV и накладывает его поверх исходного видео с помощью графического процессора.
Видеокадры с камеры передаются в граф в виде пакетов GpuBuffer
. Доступ к входному потоку осуществляется двумя калькуляторами параллельно. GpuBufferToImageFrameCalculator
преобразует буфер в ImageFrame
, который затем отправляется через преобразователь оттенков серого и фильтр Канни (оба основаны на OpenCV и работают на ЦП), выходные данные которых затем снова преобразуются в GpuBuffer
. Калькулятор графического процессора с несколькими входами, GlOverlayCalculator, принимает в качестве входных данных как исходный GpuBuffer
, так и тот, который выходит из детектора краев, и накладывает их с помощью шейдера. Затем выходные данные отправляются обратно в приложение с помощью калькулятора обратного вызова, и приложение отображает изображение на экране с помощью OpenGL.