Omówienie
MediaPipe obsługuje węzły kalkulatora na potrzeby obliczeń i renderowania za pomocą GPU. Umożliwia łączenie wielu węzłów GPU, a także mieszanie ich z węzłami kalkulatorów opartych na procesorze CPU. Na platformach mobilnych jest kilka interfejsów API GPU (np. OpenGL ES, Metal i Vulkan). MediaPipe nie próbuje oferować abstrakcji GPU pomiędzy interfejsami API. Poszczególne węzły można zapisywać przy użyciu różnych interfejsów API, co pozwala w razie potrzeby korzystać z funkcji platformy.
Obsługa GPU jest niezbędna do zapewnienia dobrej wydajności na platformach mobilnych, zwłaszcza w przypadku filmów w czasie rzeczywistym. MediaPipe umożliwia programistom pisanie kalkulatorów zgodnych z GPU, które obsługują GPU, do:
- Przetwarzanie w czasie rzeczywistym na urządzeniu, a nie tylko przetwarzanie wsadowe
- Renderowanie i efekty filmów, a nie tylko analiza
Poniżej znajdziesz zasady projektowania obsługi GPU w MediaPipe
- Kalkulatory oparte na GPU powinny być możliwe w dowolnym miejscu na wykresie. Nie muszą być używane do renderowania na ekranie.
- Przenoszenie danych dotyczących klatek z jednego kalkulatora opartego na GPU powinno być szybkie i nie wiąże się z kosztownymi operacjami kopiowania.
- Przenoszenie danych dotyczących klatek między CPU i GPU powinno być tak efektywne, na ile pozwala platforma.
- Różne platformy mogą wymagać różnych technik w celu uzyskania optymalnej wydajności, dlatego interfejs API powinien dawać elastyczność w sposobie implementacji za kulisami.
- Kalkulator powinien zapewniać maksymalną elastyczność korzystania z GPU do całości lub części swojego działania, w razie potrzeby łącząc go z CPU.
Obsługa OpenGL ES
MediaPipe obsługuje standard OpenGL ES do wersji 3.2 w systemach Android/Linux i do ES 3.0 na iOS. Dodatkowo MediaPipe obsługuje również Metal na iOS.
Do uruchamiania wymagany jest interfejs OpenGL ES 3.1 lub nowszy (w systemach Android/Linux) kalkulatorów i wykresów wnioskowania z systemów uczących się.
MediaPipe umożliwia korzystanie z wykresów OpenGL w wielu kontekstach GL. Na przykład to może być bardzo przydatne na wykresach łączących wolniejsze ścieżki wnioskowania GPU (np. przy 10 kl./s) z szybszą ścieżką renderowania GPU (np. 30 kl./s): ponieważ jeden kontekst GL odpowiada jednej sekwencyjnej kolejce poleceń, używając tego samego kontekstu w przypadku obu ograniczy liczbę klatek podczas renderowania.
Jednym z wyzwań, z jakimi radzi sobie MediaPipe, jest możliwość komunikacji między nimi. Przykładem może być film, w którym wysyłane zarówno do ścieżek renderowania, jak i ścieżek zależności, przy czym renderowanie musi mieć do najnowszych danych wyjściowych z wnioskowania.
Dostęp do kontekstu OpenGL przez wiele wątków nie jest możliwy jednocześnie. Poza tym przełączanie aktywnego kontekstu GL w tym samym wątku może działać wolno niektórych urządzeniach z Androidem. Dlatego stosujemy jeden wątek w zależności od kontekstu. Każdy wątek generuje polecenia GL, tworząc szereg poleceń w kontekście, który jest następnie wykonywany asynchronicznie przez GPU.
Jak działa kalkulator GPU
W tej sekcji przedstawiamy podstawową strukturę metody przetwarzania GPU
kalkulator uzyskany na podstawie klasy bazowej GlSimpleCalculator. Kalkulator GPU
Jako przykład podano LuminanceCalculator
. Metoda
LuminanceCalculator::GlRender
dzwoni z numeru 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();
}
Zgodnie z powyższymi zasadami projektowania możliwości obsługi GPU MediaPipe:
- Korzystamy z typu danych GPU o nazwie
GpuBuffer
, który służy do przedstawiania danych obrazu i jest zoptymalizowany pod kątem wykorzystania GPU. Dokładna zawartość tego typu danych jest nieprzejrzysta i różnie się od konkretnej platformy. - Niskopoziomowy interfejs API oparty na strukturze, w którym każdy kalkulator, który chce skorzystać z GPU, tworzy instancję klasy
GlCalculatorHelper
i jest jej właścicielem. Ta klasa udostępnia niezależny od platformy interfejs API do zarządzania kontekstem OpenGL, konfigurowania tekstur dla danych wejściowych i wyjściowych itp. - Wysoki poziom interfejsu API oparty na podklasie, w którym proste kalkulatory implementujące podklasę filtrów obrazów z
GlSimpleCalculator
i muszą zastąpić kilka metod wirtualnych odpowiednim kodem OpenGL, a klasa superklasa zajmuje się całą hydrauliką. - Dane, które muszą być współdzielone przez wszystkie kalkulatory oparte na GPU, są dostarczane jako zewnętrzne dane wejściowe implementowane jako usługa grafowa i zarządzane przez klasę
GlCalculatorHelper
. - Połączenie funkcji pomocniczych kalkulatora i wspólnej usługi wykresów zapewnia nam dużą elastyczność zarządzania zasobem GPU: możemy mieć osobny kontekst dla każdego kalkulatora, współużytkować pojedynczy kontekst, blokadę lub inne podstawowe elementy synchronizacji itd. Wszystko to odbywa się za pomocą funkcji pomocniczej, ale jest niewidoczne dla poszczególnych kalkulatorów.
Konwertery GpuBuffer na ImageFrame
Udostępniamy dwa kalkulatory o nazwach GpuBufferToImageFrameCalculator
i ImageFrameToGpuBufferCalculator
. Te kalkulatory umożliwiają przeliczanie wartości od ImageFrame
do GpuBuffer
, co umożliwia tworzenie wykresów łączących kalkulatory liczby GPU i CPU. Są obsługiwane zarówno w systemie iOS, jak i Androidzie
W miarę możliwości kalkulatory te korzystają z funkcji platformy, aby udostępniać dane między CPU i GPU bez kopiowania.
Poniższy diagram przedstawia przepływ danych w aplikacji mobilnej, która rejestruje obraz z kamery, przesyła go na wykres MediaPipe i renderuje na ekranie w czasie rzeczywistym. Linia przerywana wskazuje, które części znajdują się na wykresie MediaPipe. Aplikacja uruchamia na CPU filtr wykrywania krawędzi Canny'ego przy użyciu OpenCV i nakłada go na oryginalny film za pomocą GPU.
Klatki wideo z kamery są przesyłane do wykresu w postaci pakietów GpuBuffer
.
do strumienia wejściowego mają dostęp równolegle dwa kalkulatory.
GpuBufferToImageFrameCalculator
konwertuje bufor na ImageFrame
,
który jest przesyłany przez konwerter w skali szarości i filtr Canny’ego (oparte na obu
w OpenCV i na CPU), którego dane wyjściowe są następnie konwertowane
GpuBuffer
. Kalkulator GPU z wieloma danymi wejściowymi, GlOverlayCalculator, przyjmuje
wpisz zarówno oryginalny GpuBuffer
, jak i ten wychodzący z czujnika krawędzi,
i nakłada je za pomocą cieni. Dane wyjściowe są następnie wysyłane z powrotem do
za pomocą kalkulatora wywołań zwrotnych, a aplikacja renderuje obraz
do ekranu przy użyciu trybu OpenGL.