Visão geral
O MediaPipe oferece suporte a nós de calculadora para computação e renderização de GPU e permite combinar vários nós da GPU, além de combiná-los com nós de calculadora baseados na CPU. Existem várias APIs de GPU em plataformas móveis (por exemplo, OpenGL ES, Metal e Vulkan). O MediaPipe não tenta oferecer uma única abstração de GPU entre APIs. Nós individuais podem ser escritos usando APIs diferentes, o que permite que eles aproveitem recursos específicos da plataforma quando necessário.
O suporte à GPU é essencial para um bom desempenho em plataformas móveis, especialmente para vídeos em tempo real. O MediaPipe permite que os desenvolvedores criem calculadoras compatíveis com GPU compatíveis com o uso da GPU para:
- Processamento em tempo real no dispositivo, não apenas em lote
- Renderização e efeitos de vídeo, não apenas análise
Confira abaixo os princípios de design para suporte a GPUs no MediaPipe
- Calculadoras baseadas em GPU devem poder ocorrer em qualquer lugar do gráfico e não necessariamente ser usadas para renderização na tela.
- A transferência de dados de frame de uma calculadora baseada em GPU para outra precisa ser rápida e não gerar operações de cópia dispendiosas.
- A transferência de dados de frame entre a CPU e a GPU precisa ser tão eficiente quanto a plataforma permite.
- Como cada plataforma pode exigir técnicas diferentes para um melhor desempenho, a API deve permitir flexibilidade na forma como as coisas são implementadas nos bastidores.
- Uma calculadora precisa ter flexibilidade máxima no uso da GPU em toda a operação ou parte dela, combinando-a com a CPU, se necessário.
Compatibilidade com OpenGL ES
O MediaPipe oferece suporte ao OpenGL ES até a versão 3.2 no Android/Linux e até o ES 3.0 no iOS. Além disso, o MediaPipe também é compatível com Metal no iOS.
O OpenGL ES 3.1 ou versão mais recente é necessário (em sistemas Android/Linux) para executar calculadoras e gráficos de inferência de aprendizado de máquina.
O MediaPipe permite que os gráficos executem o OpenGL em vários contextos GL. Por exemplo, isso pode ser muito útil em gráficos que combinam um caminho de inferência de GPU mais lento (a 10 QPS) com um caminho de renderização de GPU mais rápido (por exemplo, a 30 QPS): já que um contexto GL corresponde a uma fila de comando sequencial, usar o mesmo contexto para as duas tarefas reduziria o frame rate de renderização.
Um desafio do MediaPipe que o uso de vários contextos resolve é a capacidade de se comunicar entre eles. Um cenário de exemplo é um com um vídeo de entrada que é enviado aos caminhos de renderização e inferência, e a renderização precisa ter acesso à saída mais recente da inferência.
Um contexto OpenGL não pode ser acessado por várias linhas de execução ao mesmo tempo. Além disso, mudar o contexto de GL ativo na mesma linha de execução pode ser lento em alguns dispositivos Android. Portanto, nossa abordagem é ter uma linha de execução dedicada por contexto. Cada linha de execução emite comandos GL, criando uma fila de comandos serial no contexto, que é então executada pela GPU de forma assíncrona.
A vida de uma calculadora de GPU
Esta seção apresenta a estrutura básica do método Process de uma calculadora
de GPU derivada da classe base GlSimpleCalculator. A calculadora da GPU
LuminanceCalculator
é mostrada como exemplo. O método
LuminanceCalculator::GlRender
é chamado em 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();
}
Os princípios de design mencionados acima resultaram nas seguintes opções de design para suporte a GPUs do MediaPipe:
- Temos um tipo de dados de GPU,
GpuBuffer
, para representar dados de imagem, otimizado para uso da GPU. O conteúdo exato desse tipo de dados é opaco e específico da plataforma. - Uma API de baixo nível baseada em composição, em que qualquer calculadora que queira usar a GPU cria e é proprietária de uma instância da classe
GlCalculatorHelper
. Essa classe oferece uma API independente de plataforma para gerenciar o contexto do OpenGL, configurar texturas para entradas e saídas etc. - Uma API de alto nível baseada em subclasses, em que calculadoras simples que implementam a subclasse de filtros de imagem da
GlSimpleCalculator
e só precisam substituir alguns métodos virtuais pelo código OpenGL específico, enquanto a superclasse cuida de todo o encanamento. - Os dados que precisam ser compartilhados entre todas as calculadoras baseadas em GPU são fornecidos como uma entrada externa, implementada como um serviço de gráfico e gerenciada pela classe
GlCalculatorHelper
. - A combinação de auxiliares específicos para calculadoras e um serviço de gráfico compartilhado proporciona grande flexibilidade no gerenciamento do recurso da GPU: podemos ter um contexto separado por calculadora, compartilhar um único contexto, compartilhar um bloqueio ou outros primitivos de sincronização etc. Tudo isso é gerenciado pelo assistente e ocultado nas calculadoras individuais.
Conversores GpuBuffer para ImageFrame
Oferecemos duas calculadoras chamadas GpuBufferToImageFrameCalculator
e ImageFrameToGpuBufferCalculator
. Essas calculadoras são convertidas entre ImageFrame
e GpuBuffer
, permitindo a criação de gráficos que combinam calculadoras de GPU e CPU. Eles são compatíveis com iOS e Android
Quando possível, essas calculadoras usam funcionalidades específicas da plataforma para compartilhar dados entre a CPU e a GPU sem copiar.
O diagrama abaixo mostra o fluxo de dados em um aplicativo para dispositivos móveis que captura vídeos da câmera, os executa em um gráfico do MediaPipe e renderiza a saída na tela em tempo real. A linha tracejada indica quais partes estão dentro do gráfico do MediaPipe correto. Este aplicativo executa um filtro de detecção de borda do Canny na CPU usando o OpenCV e o sobrepõe ao vídeo original usando a GPU.
Os frames de vídeo da câmera são inseridos no gráfico como pacotes GpuBuffer
. O
fluxo de entrada é acessado por duas calculadoras em paralelo.
O GpuBufferToImageFrameCalculator
converte o buffer em um ImageFrame
,
que é enviado por um conversor de escala de cinza e um filtro canny (ambos baseados
no OpenCV e em execução na CPU), cuja saída é convertida novamente em
GpuBuffer
. A calculadora GPU de várias entradas, a GlOverlayCalculator, usa como
entrada o GpuBuffer
original e o que sai do detector de borda
e os sobrepõe usando um sombreador. Em seguida, a saída é enviada de volta ao
aplicativo usando uma calculadora de callback, e o aplicativo renderiza a imagem
na tela usando o OpenGL.