Hello World! na urządzeniach z iOS

Wstęp

Ten samouczek Hello World! korzysta z platformy MediaPipe Framework, aby utworzyć aplikację na iOS, która uruchamia wykres MediaPipe na urządzeniu z iOS.

Co utworzysz

Prosta aplikacja kamery do wykrywania krawędzi Sobel w czasie rzeczywistym w transmisji wideo na żywo na urządzeniu z iOS.

edge_detection_ios_gpu_gif

Konfiguracja

  1. Zainstaluj MediaPipe Framework w systemie. Szczegółowe informacje znajdziesz w przewodniku instalacji ramek.
  2. Skonfiguruj urządzenie z iOS do programowania.
  3. Skonfiguruj Bazel w swoim systemie, aby skompilować i wdrożyć aplikację na iOS.

Wykres wykrywania krawędzi

Będziemy używać tego wykresu: edge_detection_mobile_gpu.pbtxt:

# MediaPipe graph that performs GPU Sobel edge detection on a live video stream.
# Used in the examples
# mediapipe/examples/android/src/java/com/google/mediapipe/apps/basic:helloworld
# and mediapipe/examples/ios/helloworld.

# Images coming into and out of the graph.
input_stream: "input_video"
output_stream: "output_video"

# Converts RGB images into luminance images, still stored in RGB format.
node: {
  calculator: "LuminanceCalculator"
  input_stream: "input_video"
  output_stream: "luma_video"
}

# Applies the Sobel filter to luminance images stored in RGB format.
node: {
  calculator: "SobelEdgesCalculator"
  input_stream: "luma_video"
  output_stream: "output_video"
}

Wizualizacja wykresu znajduje się poniżej:

edge_detection_mobile_gpu

Ten wykres zawiera pojedynczy strumień wejściowy o nazwie input_video dla wszystkich przychodzących klatek, które dostarczy aparat Twojego urządzenia.

Pierwszy węzeł na wykresie – LuminanceCalculator – pobiera 1 pakiet (ramkę obrazu) i stosuje zmianę luminancji za pomocą cieniowania OpenGL. Otrzymana ramka obrazu jest wysyłana do strumienia wyjściowego luma_video.

Drugi węzeł, SobelEdgesCalculator, stosuje wykrywanie brzegów do pakietów przychodzących w strumieniu luma_video i zwraca wynik strumienia wyjściowego output_video.

Nasza aplikacja na iOS wyświetli ramki obrazu wyjściowego strumienia output_video.

Wstępna minimalna konfiguracja aplikacji

Zaczniemy od prostej aplikacji na iOS i pokażemy, jak ją utworzyć w bazel.

Najpierw utwórz projekt XCode, klikając Plik > Nowy > Aplikacja z jednym widokiem.

Ustaw nazwę usługi na „HelloWorld” i użyj odpowiedniego identyfikatora organizacji, np. com.google.mediapipe. Identyfikatorem organizacji wraz z nazwą usługi będzie element bundle_id aplikacji, np. com.google.mediapipe.HelloWorld.

Ustaw język na Objective-C.

Zapisz projekt w odpowiedniej lokalizacji. Nazwijmy je $PROJECT_TEMPLATE_LOC. Oznacza to, że Twój projekt znajdzie się w katalogu $PROJECT_TEMPLATE_LOC/HelloWorld. Ten katalog będzie zawierał inny katalog o nazwie HelloWorld i plik HelloWorld.xcodeproj.

Pole HelloWorld.xcodeproj nie będzie przydatne w tym samouczku, ponieważ do skompilowania aplikacji na iOS użyjemy Baidu. Zawartość katalogu $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld jest wymieniona poniżej:

  1. AppDelegate.hAppDelegate.m
  2. ViewController.hViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboardLaunch.storyboard
  6. Katalog Assets.xcassets.

Skopiuj te pliki do katalogu o nazwie HelloWorld do lokalizacji, w której ma dostęp do kodu źródłowego platformy MediaPipe Framework. Na przykład kod źródłowy aplikacji, którą skompilujemy w tym samouczku, znajduje się tutaj: mediapipe/examples/ios/HelloWorld. W trakcie ćwiczeń z programowania tę ścieżkę będziemy nazywać $APPLICATION_PATH.

Utwórz plik BUILD w $APPLICATION_PATH i dodaj te reguły kompilacji:

MIN_IOS_VERSION = "11.0"

load(
    "@build_bazel_rules_apple//apple:ios.bzl",
    "ios_application",
)

ios_application(
    name = "HelloWorldApp",
    bundle_id = "com.google.mediapipe.HelloWorld",
    families = [
        "iphone",
        "ipad",
    ],
    infoplists = ["Info.plist"],
    minimum_os_version = MIN_IOS_VERSION,
    provisioning_profile = "//mediapipe/examples/ios:developer_provisioning_profile",
    deps = [":HelloWorldAppLibrary"],
)

objc_library(
    name = "HelloWorldAppLibrary",
    srcs = [
        "AppDelegate.m",
        "ViewController.m",
        "main.m",
    ],
    hdrs = [
        "AppDelegate.h",
        "ViewController.h",
    ],
    data = [
        "Base.lproj/LaunchScreen.storyboard",
        "Base.lproj/Main.storyboard",
    ],
    sdk_frameworks = [
        "UIKit",
    ],
    deps = [],
)

Reguła objc_library dodaje zależności klas AppDelegate i ViewController, main.m oraz scenorysów aplikacji. Aplikacja oparta na szablonie zależy tylko od pakietu SDK UIKit.

Reguła ios_application używa wygenerowanej biblioteki HelloWorldAppLibrary Objective-C do tworzenia aplikacji na iOS do zainstalowania na urządzeniu z iOS.

Aby utworzyć aplikację, użyj w terminalu tego polecenia:

bazel build -c opt --config=ios_arm64 <$APPLICATION_PATH>:HelloWorldApp'

Aby na przykład utworzyć aplikację HelloWorldApp w mediapipe/examples/ios/helloworld, użyj tego polecenia:

bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/helloworld:HelloWorldApp

Następnie wróć do XCode, otwórz Okno > Urządzenia i symulatory, wybierz swoje urządzenie i dodaj do niego plik .ipa wygenerowany poleceniem powyżej. Oto dokument na temat konfigurowania i kompilowania aplikacji iOS Framework.

Otwórz aplikację na urządzeniu. Pole jest puste, więc powinien wyświetlać się biały ekran.

Używaj aparatu do podglądu na żywo

W tym samouczku użyjemy klasy MPPCameraInputSource do uzyskiwania dostępu do klatek z aparatu i pobierania ich z niego. Ta klasa używa interfejsu AVCaptureSession API do pobierania klatek z kamery.

Jednak przed użyciem tej klasy zmień plik Info.plist, aby umożliwić korzystanie z kamery w aplikacji.

W pliku ViewController.m dodaj ten wiersz importu:

#import "mediapipe/objc/MPPCameraInputSource.h"

Aby utworzyć obiekt _cameraSource, dodaj do jego bloku implementacji ten kod:

@implementation ViewController {
  // Handles camera access via AVCaptureSession library.
  MPPCameraInputSource* _cameraSource;
}

Dodaj do pliku viewDidLoad() ten kod:

-(void)viewDidLoad {
  [super viewDidLoad];

  _cameraSource = [[MPPCameraInputSource alloc] init];
  _cameraSource.sessionPreset = AVCaptureSessionPresetHigh;
  _cameraSource.cameraPosition = AVCaptureDevicePositionBack;
  // The frame's native format is rotated with respect to the portrait orientation.
  _cameraSource.orientation = AVCaptureVideoOrientationPortrait;
}

Kod inicjuje _cameraSource, ustawia gotowe ustawienia sesji nagrywania i używa kamery.

Aby je wyświetlić, musimy pobrać klatki z _cameraSource do naszej aplikacji ViewController. MPPCameraInputSource to podklasa klasy MPPInputSource, która stanowi protokół dla jego przedstawicieli – MPPInputSourceDelegate. Nasza aplikacja ViewController może być przedstawicielem konta _cameraSource.

Zaktualizuj odpowiednio definicję interfejsu elementu ViewController:

@interface ViewController () <MPPInputSourceDelegate>

Do obsługi konfigurowania kamery i przetwarzania przychodzących klatek musimy użyć innej kolejki niż główna kolejka. Do bloku wdrożenia ViewController dodaj ten kod:

// Process camera frames on this queue.
dispatch_queue_t _videoQueue;

W viewDidLoad() po zainicjowaniu obiektu _cameraSource dodaj ten wiersz:

[_cameraSource setDelegate:self queue:_videoQueue];

Dodaj też ten kod, aby zainicjować kolejkę przed skonfigurowaniem obiektu _cameraSource:

dispatch_queue_attr_t qosAttribute = dispatch_queue_attr_make_with_qos_class(
      DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, /*relative_priority=*/0);
_videoQueue = dispatch_queue_create(kVideoQueueLabel, qosAttribute);

Do przetwarzania ramek aparatu użyjemy kolejki szeregowej o priorytecie QOS_CLASS_USER_INTERACTIVE.

Dodaj ten wiersz po zaimportowaniu nagłówka na początku pliku, przed interfejsem/implementacją ViewController:

static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue";

Przed zaimplementowaniem dowolnej metody z protokołu MPPInputSourceDelegate musimy skonfigurować sposób wyświetlania klatek kamery. Mediapipe Framework udostępnia inne narzędzie o nazwie MPPLayerRenderer do wyświetlania obrazów na ekranie. Za pomocą tego narzędzia można wyświetlać obiekty CVPixelBufferRef, czyli typ obrazów udostępnianych przez MPPCameraInputSource jego przedstawicielom.

W pliku ViewController.m dodaj ten wiersz importu:

#import "mediapipe/objc/MPPLayerRenderer.h"

Aby wyświetlać obrazy ekranu, musimy dodać do interfejsu ViewController nowy obiekt UIView o nazwie _liveView.

Dodaj te wiersze do bloku implementacji ViewController:

// Display the camera preview frames.
IBOutlet UIView* _liveView;
// Render frames in a layer.
MPPLayerRenderer* _renderer;

Otwórz Main.storyboard i dodaj obiekt UIView z biblioteki obiektów do View klasy ViewController. Dodaj punkt odniesienia z tego widoku do obiektu _liveView dodanego przed chwilą do klasy ViewController. Zmień rozmiar widoku, tak aby był wyśrodkowany i zajmował cały ekran aplikacji.

Wróć do ViewController.m i dodaj do viewDidLoad() ten kod, aby zainicjować obiekt _renderer:

_renderer = [[MPPLayerRenderer alloc] init];
_renderer.layer.frame = _liveView.layer.bounds;
[_liveView.layer addSublayer:_renderer.layer];
_renderer.frameScaleMode = MPPFrameScaleModeFillAndCrop;

Aby pobierać klatki z aparatu, wdrożymy tę metodę:

// Must be invoked on _videoQueue.
-   (void)processVideoFrame:(CVPixelBufferRef)imageBuffer
                timestamp:(CMTime)timestamp
               fromSource:(MPPInputSource*)source {
  if (source != _cameraSource) {
    NSLog(@"Unknown source: %@", source);
    return;
  }
  // Display the captured image on the screen.
  CFRetain(imageBuffer);
  dispatch_async(dispatch_get_main_queue(), ^{
    [_renderer renderPixelBuffer:imageBuffer];
    CFRelease(imageBuffer);
  });
}

To jest metoda przekazywania dostępu do MPPInputSource. Najpierw sprawdzamy, czy pochodzimy z odpowiedniego źródła, tj. z _cameraSource. Następnie w głównej kolejce wyświetlamy klatkę odebraną z kamery przez _renderer.

Musimy teraz uruchomić aparat, gdy tylko pojawi się widok z klatkami. W tym celu wdrożymy funkcję viewWillAppear:(BOOL)animated:

-(void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
}

Zanim zaczniemy uruchamiać kamerę, potrzebujemy zgody użytkownika na dostęp do niej. MPPCameraInputSource udostępnia funkcję requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler, która prosi o dostęp do aparatu i wykona pewne działania, gdy użytkownik odpowie. Dodaj ten kod do domeny viewWillAppear:animated:

[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
  if (granted) {
    dispatch_async(_videoQueue, ^{
      [_cameraSource start];
    });
  }
}];

Przed skompilowaniem aplikacji dodaj te zależności do pliku BUILD:

sdk_frameworks = [
    "AVFoundation",
    "CoreGraphics",
    "CoreMedia",
],
deps = [
    "//mediapipe/objc:mediapipe_framework_ios",
    "//mediapipe/objc:mediapipe_input_sources_ios",
    "//mediapipe/objc:mediapipe_layer_renderer",
],

Teraz skompiluj i uruchom aplikację na urządzeniu z iOS. Po zaakceptowaniu uprawnień powinno wyświetlić się obraz z kamery.

Teraz możesz użyć ramek aparatu na wykresie MediaPipe.

Korzystanie z wykresu MediaPipe w iOS

Dodaj odpowiednie zależności

Dodaliśmy już zależności z kodu platformy MediaPipe, który zawiera interfejs API iOS, umożliwiając użycie wykresu MediaPipe. Aby użyć wykresu MediaPipe, musimy dodać zależność do wykresu, którego zamierzamy użyć w naszej aplikacji. Dodaj ten wiersz do listy data w pliku BUILD:

"//mediapipe/graphs/edge_detection:mobile_gpu_binary_graph",

Teraz dodaj zależność do kalkulatorów używanych na tym wykresie w polu deps w pliku BUILD:

"//mediapipe/graphs/edge_detection:mobile_calculators",

Na koniec zmień nazwę pliku ViewController.m na ViewController.mm, aby obsługiwać Objective-C++.

Użyj wykresu w aplikacji ViewController

W pliku ViewController.m dodaj ten wiersz importu:

#import "mediapipe/objc/MPPGraph.h"

Zadeklaruj stałą statyczną – podaj nazwę wykresu oraz strumień wejściowy i wyjściowy:

static NSString* const kGraphName = @"mobile_gpu";

static const char* kInputStream = "input_video";
static const char* kOutputStream = "output_video";

Dodaj tę właściwość do interfejsu interfejsu ViewController:

// The MediaPipe graph currently in use. Initialized in viewDidLoad, started in viewWillAppear: and
// sent video frames on _videoQueue.
@property(nonatomic) MPPGraph* mediapipeGraph;

Jak wyjaśniliśmy powyżej w komentarzu powyżej, najpierw zainicjujemy ten wykres w narzędziu viewDidLoad. W tym celu musimy wczytać wykres z pliku .pbtxt za pomocą tej funkcji:

+   (MPPGraph*)loadGraphFromResource:(NSString*)resource {
  // Load the graph config resource.
  NSError* configLoadError = nil;
  NSBundle* bundle = [NSBundle bundleForClass:[self class]];
  if (!resource || resource.length == 0) {
    return nil;
  }
  NSURL* graphURL = [bundle URLForResource:resource withExtension:@"binarypb"];
  NSData* data = [NSData dataWithContentsOfURL:graphURL options:0 error:&configLoadError];
  if (!data) {
    NSLog(@"Failed to load MediaPipe graph config: %@", configLoadError);
    return nil;
  }

  // Parse the graph config resource into mediapipe::CalculatorGraphConfig proto object.
  mediapipe::CalculatorGraphConfig config;
  config.ParseFromArray(data.bytes, data.length);

  // Create MediaPipe graph with mediapipe::CalculatorGraphConfig proto object.
  MPPGraph* newGraph = [[MPPGraph alloc] initWithGraphConfig:config];
  [newGraph addFrameOutputStream:kOutputStream outputPacketType:MPPPacketTypePixelBuffer];
  return newGraph;
}

Zainicjuj wykres w funkcji viewDidLoad w następujący sposób:

self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName];

Wykres powinien przesłać wyniki przetwarzania klatek aparatu z powrotem do ViewController. Po zainicjowaniu wykresu dodaj ten wiersz, aby ustawić ViewController jako przedstawiciela obiektu mediapipeGraph:

self.mediapipeGraph.delegate = self;

Aby uniknąć rywalizacji o pamięć podczas przetwarzania klatek z transmisji wideo, dodaj ten wiersz:

// Set maxFramesInFlight to a small value to avoid memory contention for real-time processing.
self.mediapipeGraph.maxFramesInFlight = 2;

Zacznij wykres, gdy użytkownik zezwoli na korzystanie z aparatu w naszej aplikacji:

[_cameraSource requestCameraAccessWithCompletionHandler:^void(BOOL granted) {
  if (granted) {
    // Start running self.mediapipeGraph.
    NSError* error;
    if (![self.mediapipeGraph startWithError:&error]) {
      NSLog(@"Failed to start graph: %@", error);
    }
    else if (![self.mediapipeGraph waitUntilIdleWithError:&error]) {
      NSLog(@"Failed to complete graph initial run: %@", error);
    }

    dispatch_async(_videoQueue, ^{
      [_cameraSource start];
    });
  }
}];

Wcześniej, gdy otrzymywaliśmy klatki z aparatu w ramach funkcji processVideoFrame, wyświetlaliśmy je w _liveView za pomocą funkcji _renderer. Teraz musimy wysłać te klatki do wykresu i wyrenderować wyniki. Zmodyfikuj implementację tej funkcji, aby:

-   (void)processVideoFrame:(CVPixelBufferRef)imageBuffer
                timestamp:(CMTime)timestamp
               fromSource:(MPPInputSource*)source {
  if (source != _cameraSource) {
    NSLog(@"Unknown source: %@", source);
    return;
  }
  [self.mediapipeGraph sendPixelBuffer:imageBuffer
                            intoStream:kInputStream
                            packetType:MPPPacketTypePixelBuffer];
}

Wysyłamy imageBuffer do self.mediapipeGraph w postaci pakietu typu MPPPacketTypePixelBuffer do strumienia wejściowego kInputStream, tj. „input_video”.

Wykres zostanie uruchomiony z tym pakietem wejściowym i zwróci wynik w postaci kOutputStream, czyli „output_video”. Aby odbierać pakiety w tym strumieniu wyjściowym i wyświetlać je na ekranie, możemy wdrożyć następującą metodę przekazywania:

-   (void)mediapipeGraph:(MPPGraph*)graph
   didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer
             fromStream:(const std::string&)streamName {
  if (streamName == kOutputStream) {
    // Display the captured image on the screen.
    CVPixelBufferRetain(pixelBuffer);
    dispatch_async(dispatch_get_main_queue(), ^{
      [_renderer renderPixelBuffer:pixelBuffer];
      CVPixelBufferRelease(pixelBuffer);
    });
  }
}

Zaktualizuj definicję interfejsu ViewController za pomocą MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

To wszystko. Stwórz i uruchom aplikację na urządzeniu z iOS. Powinny być widoczne wyniki użycia wykresu wykrywania krawędzi w przypadku bieżącego kanału wideo. Gratulacje!

edge_detection_ios_gpu_gif

Pamiętaj, że w przykładach na iOS jest teraz używana common aplikacja z szablonami. Kod w tym samouczku jest używany w common aplikacji z szablonami. Aplikacja helloworld ma odpowiednie zależności w plikach BUILD dla wykresu wykrywania krawędzi.