Hello World! na urządzeniach z iOS

Wprowadzenie

Witaj świecie! samouczek korzysta z MediaPipe Framework do programowania na iOS aplikacja uruchamiająca graf MediaPipe w systemie iOS.

Co utworzysz

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

edge_detection_ios_gpu_gif

Konfiguracja

  1. Zainstaluj w systemie MediaPipe Framework – patrz Instalacja Framework. .
  2. Skonfiguruj urządzenie z iOS na potrzeby programowania.
  3. Skonfiguruj w systemie Bazel, aby skompilować i wdrożyć aplikację na iOS.

Wykres wykrywania krawędzi

Użyjemy 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 ma jeden strumień wejściowy o nazwie input_video dla wszystkich przychodzących klatek udostępniane przez aparat urządzenia.

Pierwszy węzeł na wykresie, LuminanceCalculator, przyjmuje pojedynczy pakiet (obraz ) i stosuje zmianę luminancji przy użyciu cieniowania OpenGL. W wyniku ramka obrazu jest wysyłana do strumienia wyjściowego luma_video.

Drugi węzeł (SobelEdgesCalculator) stosuje wykrywanie brzegowych do przychodzących pakiety w strumieniu luma_video i wyprowadzają w wyniku output_video .

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

Wstępna minimalna konfiguracja aplikacji

Zaczynamy od prostej aplikacji na iOS i pokazujemy, jak używać bazel aby go stworzyć.

Najpierw utwórz projekt XCode. W tym celu wybierz Plik > Nowe > Aplikacja widoku pojedynczego.

Ustaw nazwę usługi na „HelloWorld” i użyj odpowiedniej organizacji identyfikatora, np. com.google.mediapipe. Identyfikator organizacji wraz z nazwą produktu będą właściwościami bundle_id dla aplikacji, np. com.google.mediapipe.HelloWorld

Ustaw język na Objective-C.

Zapisz projekt w odpowiedniej lokalizacji. Nazwijmy to $PROJECT_TEMPLATE_LOC Twój projekt będzie więc Katalog $PROJECT_TEMPLATE_LOC/HelloWorld. Ten katalog będzie zawierać inny katalog o nazwie HelloWorld i plik HelloWorld.xcodeproj.

Obiekt HelloWorld.xcodeproj nie będzie przydatny w tym samouczku, ponieważ użyjemy aby stworzyć aplikację na iOS. Zawartość Katalog $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld jest wymieniony 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 i do lokalizacji, do której masz dostęp za pomocą kodu źródłowego platformy MediaPipe Framework. Na przykład kod źródłowy funkcji aplikacja, w której skompilujemy ten samouczek, znajduje się mediapipe/examples/ios/HelloWorld Będziemy ją nazywać $APPLICATION_PATH w ramach ćwiczeń z programowania.

Utwórz plik BUILD w $APPLICATION_PATH i dodaj tę kompilację reguły:

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: AppDelegate oraz Zajęcia: ViewController, main.m i scenorysy aplikacji. aplikacja oparta na szablonie korzysta tylko z pakietu SDK UIKit.

Reguła ios_application używa biblioteki Objective-C HelloWorldAppLibrary wygenerowane w celu utworzenia aplikacji na iOS, którą można zainstalować 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 skompilować 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 Window > Urządzenia i symulatory, wybierz i dodaj na nim plik .ipa wygenerowany za pomocą powyższego polecenia. Poniżej znajduje się dokument o konfigurowaniu i kompilowaniu aplikacji iOS Framework.

Otwórz aplikację na urządzeniu. Jest puste, dlatego powinien wyświetlić się pusty, biały ekran.

Użyj aparatu do wyświetlania podglądu na żywo

W tym samouczku użyjemy klasy MPPCameraInputSource, aby uzyskać dostęp do rejestruje klatki z aparatu. Ta klasa używa interfejsu API AVCaptureSession, aby pobrać dzięki klatkom z kamery.

Zanim jednak użyjesz tej klasy, zmień plik Info.plist, aby obsługiwał aparat z użytkowania w aplikacji.

W ViewController.m dodaj ten wiersz importu:

#import "mediapipe/objc/MPPCameraInputSource.h"

Dodaj ten fragment do bloku implementacji, aby utworzyć obiekt _cameraSource:

@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 zdarzenie _cameraSource, ustawia gotowe ustawienie sesji przechwytywania i określa, z aparatu.

Musimy pobrać ramki z pliku _cameraSource do aplikacji ViewController, aby je wyświetlić. MPPCameraInputSource jest podklasą MPPInputSource, która udostępnia swoim przedstawicielom protokół, MPPInputSourceDelegate Nasza aplikacja ViewController może być delegatem z _cameraSource.

Zaktualizuj odpowiednio definicję interfejsu elementu ViewController:

@interface ViewController () <MPPInputSourceDelegate>

Aby skonfigurować kamerę i przetwarzać przychodzące klatki, powinniśmy użyć kolejki niż główna kolejka. Dodaj poniższy kod do bloku implementacji ViewController:

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

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

[_cameraSource setDelegate:self queue:_videoQueue];

Dodaj ten kod, aby zainicjować kolejkę przed skonfigurowaniem _cameraSource obiekt:

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);

Użyjemy kolejki szeregowej o priorytecie QOS_CLASS_USER_INTERACTIVE dla przetwarzania klatek kamery.

Dodaj poniższy wiersz po zaimportowaniu nagłówka na górze pliku, przed interfejs/implementacja ViewController:

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

Przed wdrożeniem metody z protokołu MPPInputSourceDelegate musimy skonfiguruj sposób wyświetlania ramek kamery. Platforma Mediapipe Framework inne narzędzie o nazwie MPPLayerRenderer. Ten można użyć do wyświetlania obiektów CVPixelBufferRef, które są typem obrazy udostępnione przedstawicielom przez firmę MPPCameraInputSource.

W ViewController.m dodaj ten wiersz importu:

#import "mediapipe/objc/MPPLayerRenderer.h"

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

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 z ViewController klasy. Dodaj punkt odniesienia z tego widoku do obiekt _liveView dodany właśnie do klasy ViewController. Zmień rozmiar w taki sposób, aby był wyśrodkowany i zakrywał cały ekran aplikacji.

Wróć do strony ViewController.m i dodaj ten kod do witryny viewDidLoad() w zainicjuj 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);
  });
}

Jest to metoda przekazywania dostępu do funkcji MPPInputSource. Najpierw sprawdzamy, czy pobieranie klatek z odpowiedniego źródła, np. z urządzenia _cameraSource. Następnie wyświetlamy klatkę odebraną z kamery za pomocą _renderer w głównej kolejce.

Teraz musimy uruchomić aparat, gdy tylko na ekranie pojawią się klatki, pojawią się wkrótce. W tym celu wdrożymy Funkcja viewWillAppear:(BOOL)animated:

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

Zanim uruchomimy kamerę, potrzebujemy zgody użytkownika. MPPCameraInputSource udostępnia funkcję requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler, aby poprosić o dostęp do kamery i wykonać czynności, jeśli użytkownik: napisał(a). Dodaj do viewWillAppear:animated ten kod:

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

Zanim utworzysz aplikację, dodaj do BUILD te zależności plik:

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. Powinna pojawić się transmisja na żywo obraz obrazu z kamery po zaakceptowaniu uprawnień do korzystania z aparatu.

Teraz na wykresie MediaPipe możemy używać ramek aparatu.

Korzystanie z grafu MediaPipe w systemie iOS

Dodaj odpowiednie zależności

Dodaliśmy już zależności kodu platformy MediaPipe, który zawiera interfejsu API iOS, aby używać grafu MediaPipe. Aby użyć wykresu MediaPipe, musimy dodać zależności od wykresu, którego chcemy użyć w aplikacji. Dodaj następujące elementy do listy data w pliku BUILD:

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

Dodaj zależność do kalkulatorów użytych 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 Objective-C++.

Użyj wykresu w: ViewController

W ViewController.m dodaj ten wiersz importu:

#import "mediapipe/objc/MPPGraph.h"

Zadeklaruj stałą statyczną w nazwie grafu, strumienia wejściowego oraz strumień wyjściowy:

static NSString* const kGraphName = @"mobile_gpu";

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

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

// 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 w komentarzu powyżej, zainicjujemy ten wykres w Najpierw viewDidLoad. W tym celu musimy wczytać wykres z pliku .pbtxt przy użyciu 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;
}

Użyj tej funkcji, aby zainicjować wykres w viewDidLoad w następujący sposób:

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

Wykres powinien przesłać wyniki przetwarzania klatek aparatu z powrotem do funkcji ViewController Po zainicjowaniu wykresu dodaj następujący wiersz, aby ustawić ViewController jako delegat obiektu mediapipeGraph:

self.mediapipeGraph.delegate = self;

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

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

Teraz zacznij tworzyć wykres, gdy użytkownik zezwoli na używanie 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 otrzymaliśmy klatki z kamery w processVideoFrame , wyświetliliśmy je w _liveView za pomocą _renderer. Teraz przesyłać te ramki do wykresu, a potem renderować wyniki. Zmień implementacji tej funkcji w taki sposób:

-   (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 pakiet imageBuffer do self.mediapipeGraph jako pakiet typu MPPPacketTypePixelBuffer do strumienia wejściowego kInputStream, tj. „input_video”.

Wykres będzie działał z tym pakietem wejściowym i wyświetli wynik w kOutputStream, czyli „output_video”. Możemy wyznaczyć przedstawiciela: do odbierania pakietów w tym strumieniu wyjściowym i wyświetlania ich na ekranie:

-   (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. Utwórz i uruchom aplikację na urządzeniu z iOS. Powinna pojawić się ikona wyniki zastosowania wykresu wykrywania krawędzi w kanale wideo na żywo. Gratulacje!

edge_detection_ios_gpu_gif

Pamiętaj, że w przykładach na iOS jest teraz używana popularna aplikacja do tworzenia szablonów. Kod w argumencie Ten samouczek jest używany w aplikacji do tworzenia standardowych szablonów. Aplikacja helloworld ma: odpowiednie zależności pliku BUILD na potrzeby wykresu wykrywania krawędzi.