Przegląd
MediaPipe obsługuje węzły kalkulatora do obliczeń i renderowania za pomocą GPU. Umożliwia też łączenie wielu węzłów GPU, a także ich łączenie z węzłami kalkulatora opartego na procesorach. Na platformach mobilnych istnieje kilka interfejsów API GPU (np. OpenGL ES, Metal i Vulkan). MediaPipe nie próbuje oferować jednej abstrakcji GPU w wielu interfejsach API. Poszczególne węzły można pisać za pomocą różnych interfejsów API, co pozwala w razie potrzeby korzystać z funkcji dostępnych na platformie.
Obsługa GPU jest niezbędna do zapewnienia wysokiej 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 w przypadku:
- Przetwarzanie w czasie rzeczywistym na urządzeniu, nie tylko przetwarzanie wsadowe
- Renderowanie i efekty filmów, nie tylko analiza
Poniżej znajdziesz zasady projektowania obsługi GPU w MediaPipe
- Kalkulatory oparte na GPU powinny znajdować się w dowolnym miejscu na wykresie i nie muszą być używane do renderowania na ekranie.
- Przenoszenie danych dotyczących klatek z jednego kalkulatora opartego na GPU do innego powinno przebiegać szybko i nie wiązać się z kosztownymi kopiowaniem.
- Przesyłanie danych klatek między procesorem a GPU powinno odbywać się z maksymalną wydajnością platformy.
- Różne platformy mogą wymagać różnych technik do uzyskania najlepszej wydajności, dlatego interfejs API powinien umożliwiać elastyczność w sposobie wdrażania rozwiązań w tle.
- Kalkulator powinien mieć maksymalną elastyczność w korzystaniu z GPU w całym działaniu lub jego części, w razie potrzeby łącząc go z procesorem.
Obsługa OpenGL ES
MediaPipe obsługuje platformę OpenGL ES do wersji 3.2 w systemach Android/Linux i do ES 3.0 w systemie iOS. MediaPipe obsługuje też Metal na urządzeniach z iOS.
Do uruchamiania kalkulatorów i wykresów wnioskowania na podstawie systemów uczących się wymagany jest standard OpenGL ES 3.1 lub nowszy (w systemach Android i Linux).
MediaPipe umożliwia uruchamianie wykresów OpenGL w wielu kontekstach GL. Może to być na przykład bardzo przydatne na wykresach, które łączą wolniejszą ścieżkę wnioskowania GPU (np. przy 10 kl./s) z szybszą ścieżką renderowania GPU (np. 30 FPS): ponieważ 1 kontekst GL odpowiada jednej kolejce poleceń sekwencyjnych, a użycie tego samego kontekstu w obu zadaniach zmniejszyłoby liczbę klatek w renderowaniu.
Jednym z problemów, jakie MediaPipe rozwiązuje w zakresie różnych kontekstów, jest możliwość komunikowania się między nimi. Przykładowy scenariusz to film wejściowy, który jest wysyłany zarówno na ścieżkę renderowania, jak i do wnioskowania, a renderowanie musi mieć dostęp do najnowszych danych wyjściowych wnioskowania.
Do kontekstu OpenGL nie ma dostępu jednocześnie kilka wątków. Poza tym przełączanie kontekstu aktywnego GL w tym samym wątku może być powolne na niektórych urządzeniach z Androidem. W związku z tym przyjmujemy, że każdy kontekst ma jeden wątek. Każdy wątek generuje polecenia GL, tworząc w kontekście kolejkę poleceń szeregowych, która jest następnie wykonywana asynchronicznie przez GPU.
Czas pracy kalkulatora GPU
W tej sekcji przedstawiamy podstawową strukturę metody Process kalkulatora GPU utworzonego na podstawie klasy podstawowej GlSimpleCalculator. Przykład: kalkulator GPU LuminanceCalculator
. Metoda LuminanceCalculator::GlRender
jest wywoływana z metody 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 w przypadku obsługi GPU MediaPipe wybrano jeden z tych sposobów:
- Używamy typu danych GPU o nazwie
GpuBuffer
, który służy do prezentowania danych graficznych i jest zoptymalizowany pod kątem użycia GPU. Dokładna zawartość tego typu danych jest nieprzejrzysta i zależy od platformy. - Niski poziom interfejsu API oparty na kompozycji, w którym każdy kalkulator, który chce skorzystać z GPU, tworzy i jest właścicielem instancji klasy
GlCalculatorHelper
. 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. - Wysokiej klasy interfejs API oparty na podklasie, w której proste kalkulatory implementujące filtry obrazu odfiltrowują podklasę z
GlSimpleCalculator
i wystarczą tylko zastąpić kilka metod wirtualnych określonym kodem OpenGL, podczas gdy klasa nadrzędna zajmuje się całą pracą hydrauliczną. - Dane, które muszą być udostępniane przez wszystkie kalkulatory oparte na GPU, są dostarczane jako zewnętrzne dane wejściowe, które są zaimplementowane jako usługa wykresowa i są zarządzane przez klasę
GlCalculatorHelper
. - Połączenie pomocników specyficznych dla kalkulatora i wspólnej usługi wykresów zapewnia nam dużą elastyczność w zarządzaniu zasobami GPU: możemy mieć osobny kontekst dla kalkulatora, jeden kontekst, udostępniać blokady lub inne podstawowe elementy synchronizacji itd. – a wszystkim tym zarządza osoba pomocnicza, a wszystkie te funkcje są ukryte przed poszczególnymi kalkulatorami.
Konwertery GpuBuffer na ImageFrame
Udostępniamy dwa kalkulatory o nazwie GpuBufferToImageFrameCalculator
i ImageFrameToGpuBufferCalculator
. Te kalkulatory przeliczają wartości od ImageFrame
do GpuBuffer
, co umożliwia tworzenie wykresów łączących kalkulatory GPU i CPU. Są obsługiwane zarówno na urządzeniach z iOS, jak i z Androidem.
W miarę możliwości kalkulatory korzystają z funkcji związanych z platformą, aby udostępniać dane między procesorem a GPU bez kopiowania.
Poniższy schemat przedstawia przepływ danych w aplikacji mobilnej, która rejestruje obraz z kamery, wyświetla go na wykresie MediaPipe i wyświetla na ekranie w czasie rzeczywistym dane wyjściowe. Linia przerywana wskazuje, które części znajdują się we właściwym wykresie MediaPipe. Ta aplikacja wykorzystuje na procesorze CPU filtr wykrywania krawędzi, Canny'ego brzegu, i nakłada go na oryginalny film, korzystając z GPU.
Klatki wideo z kamery są przesyłane na wykres w postaci pakietów GpuBuffer
. Do strumienia danych wejściowych mają dostęp 2 równoległe kalkulatory.
GpuBufferToImageFrameCalculator
przekształca bufor w parametr ImageFrame
, który jest następnie wysyłany przez konwerter skali szarości i filtr Canny (za pomocą algorytmu OpenCV i uruchamianego na procesorze), którego dane wyjściowe są następnie ponownie konwertowane na GpuBuffer
. Kalkulator GPU z wieloma danymi wejściowymi, GlOverlayCalculator, wykorzystuje dane wejściowe zarówno pierwotnego elementu GpuBuffer
, jak i tego, który wychodzi z detektora brzegowego, i nakłada je za pomocą cieniowania. Wynik jest następnie wysyłany do aplikacji za pomocą kalkulatora wywołań zwrotnych, a aplikacja renderuje obraz na ekranie za pomocą OpenGL.