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.
Konfiguracja
- Zainstaluj MediaPipe Framework w systemie. Szczegółowe informacje znajdziesz w przewodniku instalacji ramek.
- Skonfiguruj urządzenie z iOS do programowania.
- 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:
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:
AppDelegate.h
iAppDelegate.m
ViewController.h
iViewController.m
main.m
Info.plist
Main.storyboard
iLaunch.storyboard
- 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!
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.