GPU

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.

Jak działają kalkulatory 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.