Përshëndetje Botë! në iOS

Hyrje

Kjo Hello World! tutoriali përdor MediaPipe Framework për të zhvilluar një aplikacion iOS që ekzekuton një grafik MediaPipe në iOS.

Çfarë do të ndërtoni

Një aplikacion i thjeshtë kamere për zbulimin e skajeve të Sobel në kohë reale aplikohet në një transmetim video të drejtpërdrejtë në një pajisje iOS.

edge_detection_ios_gpu_gif

Konfigurimi

  1. Instaloni MediaPipe Framework në sistemin tuaj, shikoni udhëzuesin e instalimit të Framework për detaje.
  2. Konfiguro pajisjen tuaj iOS për zhvillim.
  3. Konfiguro Bazel në sistemin tënd për të ndërtuar dhe vendosur aplikacionin iOS.

Grafiku për zbulimin e skajeve

Ne do të përdorim grafikun e mëposhtëm, 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"
}

Një vizualizimi i grafikut është paraqitur më poshtë:

edge_detection_mobile_gpu

Ky grafik ka një transmetim të vetëm hyrje të quajtur input_video për të gjitha kornizat hyrëse që do të ofrohen nga kamera e pajisjes suaj.

Nyja e parë në grafik, LuminanceCalculator , merr një paketë të vetme (kornizë imazhi) dhe zbaton një ndryshim në ndriçimin duke përdorur një shader OpenGL. Korniza e imazhit që rezulton dërgohet në rrjedhën e daljes luma_video .

Nyja e dytë, SobelEdgesCalculator aplikon zbulimin e skajeve për paketat hyrëse në rrjedhën luma_video dhe nxjerr rezultatet në rrjedhën e daljes output_video .

Aplikacioni ynë iOS do të shfaqë kornizat e imazhit në dalje të transmetimit output_video .

Konfigurimi fillestar minimal i aplikacionit

Fillimisht fillojmë me një aplikacion të thjeshtë iOS dhe demonstrojmë se si të përdorim bazel për ta ndërtuar atë.

Së pari, krijoni një projekt XCode përmes File > New > Single View App.

Cakto emrin e produktit në "HelloWorld" dhe përdor një identifikues të përshtatshëm organizimi, si p.sh. com.google.mediapipe . Identifikuesi i organizatës së bashku me emrin e produktit do të jetë bundle_id për aplikacionin, si p.sh. com.google.mediapipe.HelloWorld .

Vendoseni gjuhën në Objektivi-C.

Ruani projektin në një vend të përshtatshëm. Le ta quajmë këtë $PROJECT_TEMPLATE_LOC . Kështu që projekti juaj do të jetë në drejtorinë $PROJECT_TEMPLATE_LOC/HelloWorld . Kjo direktori do të përmbajë një drejtori tjetër të quajtur HelloWorld dhe një skedar HelloWorld.xcodeproj .

HelloWorld.xcodeproj nuk do të jetë i dobishëm për këtë tutorial, pasi ne do të përdorim bazel për të ndërtuar aplikacionin iOS. Përmbajtja e drejtorisë $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld është renditur më poshtë:

  1. AppDelegate.h dhe AppDelegate.m
  2. ViewController.h dhe ViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboard dhe Launch.storyboard
  6. Drejtoria Assets.xcassets .

Kopjojini këta skedarë në një drejtori të quajtur HelloWorld në një vendndodhje që mund të hyjë në kodin burimor të MediaPipe Framework. Për shembull, kodi burim i aplikacionit që do të ndërtojmë në këtë tutorial ndodhet në mediapipe/examples/ios/HelloWorld . Ne do t'i referohemi kësaj rruge si $APPLICATION_PATH në të gjithë laboratorin e kodeve.

Krijoni një skedar BUILD$APPLICATION_PATH dhe shtoni rregullat e mëposhtme të ndërtimit:

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 = [],
)

Rregulli objc_library shton varësi për klasat AppDelegate dhe ViewController , main.m dhe skedat e aplikacioneve. Aplikacioni i modeluar varet vetëm nga UIKit SDK.

Rregulli ios_application përdor bibliotekën HelloWorldAppLibrary Objective-C të krijuar për të ndërtuar një aplikacion iOS për instalim në pajisjen tuaj iOS.

Për të ndërtuar aplikacionin, përdorni komandën e mëposhtme në një terminal:

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

Për shembull, për të ndërtuar aplikacionin HelloWorldAppmediapipe/examples/ios/helloworld , përdorni komandën e mëposhtme:

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

Më pas, kthehuni te XCode, hapni dritaren > Pajisjet dhe simuluesit, zgjidhni pajisjen tuaj dhe shtoni skedarin .ipa të krijuar nga komanda e mësipërme në pajisjen tuaj. Këtu është dokumenti për konfigurimin dhe përpilimin e aplikacioneve iOS Framework.

Hapni aplikacionin në pajisjen tuaj. Meqenëse është bosh, duhet të shfaqë një ekran të bardhë bosh.

Përdorni kamerën për furnizimin e pamjes së drejtpërdrejtë

Në këtë tutorial, ne do të përdorim klasën MPPCameraInputSource për të hyrë dhe për të kapur kornizat nga kamera. Kjo klasë përdor API- AVCaptureSession për të marrë kornizat nga kamera.

Por përpara se të përdorni këtë klasë, ndryshoni skedarin Info.plist për të mbështetur përdorimin e kamerës në aplikacion.

ViewController.m , shtoni linjën e mëposhtme të importit:

#import "mediapipe/objc/MPPCameraInputSource.h"

Shtoni sa vijon në bllokun e tij të zbatimit për të krijuar një objekt _cameraSource :

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

Shto kodin e mëposhtëm për të viewDidLoad() :

-(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;
}

Kodi inicializon _cameraSource , cakton paracaktimin e sesionit të kapjes dhe cilën kamerë të përdoret.

Ne duhet të marrim korniza nga _cameraSource në aplikacionin tonë ViewController për t'i shfaqur ato. MPPCameraInputSource është një nënklasë e MPPInputSource , e cila ofron një protokoll për delegatët e tij, përkatësisht MPPInputSourceDelegate . Pra, aplikacioni ynë ViewController mund të jetë një delegat i _cameraSource .

Përditësoni përkufizimin e ndërfaqes së ViewController në përputhje me rrethanat:

@interface ViewController () <MPPInputSourceDelegate>

Për të trajtuar konfigurimin e kamerës dhe për të përpunuar kornizat hyrëse, duhet të përdorim një radhë të ndryshme nga radha kryesore. Shtoni sa vijon në bllokun e zbatimit të ViewController :

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

viewDidLoad() , shtoni rreshtin e mëposhtëm pas inicializimit të objektit _cameraSource :

[_cameraSource setDelegate:self queue:_videoQueue];

Dhe shtoni kodin e mëposhtëm për të inicializuar radhën përpara se të vendosni objektin _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);

Ne do të përdorim një radhë serike me përparësi QOS_CLASS_USER_INTERACTIVE për përpunimin e kornizave të kamerës.

Shtoni rreshtin e mëposhtëm pas importimit të kokës në krye të skedarit, përpara ndërfaqes/zbatimit të ViewController :

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

Përpara se të zbatojmë ndonjë metodë nga protokolli MPPInputSourceDelegate , së pari duhet të vendosim një mënyrë për të shfaqur kornizat e kamerës. Mediapipe Framework ofron një mjet tjetër të quajtur MPPLayerRenderer për të shfaqur imazhet në ekran. Ky mjet mund të përdoret për të shfaqur objektet CVPixelBufferRef , që është lloji i imazheve të ofruara nga MPPCameraInputSource për delegatët e tij.

ViewController.m , shtoni linjën e mëposhtme të importit:

#import "mediapipe/objc/MPPLayerRenderer.h"

Për të shfaqur imazhet e ekranit, duhet të shtojmë një objekt të ri UIView të quajtur _liveViewViewController .

Shtoni linjat e mëposhtme në bllokun e zbatimit të ViewController :

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

Shkoni te Main.storyboard , shtoni një objekt UIView nga biblioteka e objekteve në View e klasës ViewController . Shtoni një prizë referimi nga kjo pamje tek objekti _liveView që sapo keni shtuar në klasën ViewController . Ndryshoni madhësinë e pamjes në mënyrë që të jetë në qendër dhe të mbulojë të gjithë ekranin e aplikacionit.

Kthehuni te ViewController.m dhe shtoni kodin e mëposhtëm në viewDidLoad() për të inicializuar objektin _renderer :

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

Për të marrë korniza nga kamera, ne do të zbatojmë metodën e mëposhtme:

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

Kjo është një metodë delegimi e MPPInputSource . Fillimisht kontrollojmë që po marrim korniza nga burimi i duhur, pra _cameraSource . Më pas ne shfaqim kornizën e marrë nga kamera nëpërmjet _renderer në radhën kryesore.

Tani, duhet ta nisim kamerën sapo të shfaqet pamja për të shfaqur kornizat. Për ta bërë këtë, ne do të zbatojmë funksionin viewWillAppear:(BOOL)animated :

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

Përpara se të fillojmë të ekzekutojmë kamerën, na duhet leja e përdoruesit për të hyrë në të. MPPCameraInputSource ofron një funksion requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler për të kërkuar qasje në kamerë dhe për të bërë disa punë kur përdoruesi është përgjigjur. Shtoni kodin e mëposhtëm për të viewWillAppear:animated :

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

Përpara se të ndërtoni aplikacionin, shtoni varësitë e mëposhtme në skedarin tuaj BUILD :

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

Tani ndërtoni dhe ekzekutoni aplikacionin në pajisjen tuaj iOS. Duhet të shihni një furnizim të drejtpërdrejtë të pamjes së kamerës pasi të pranoni lejet e kamerës.

Tani jemi gati të përdorim kornizat e kamerës në një grafik MediaPipe.

Përdorimi i një grafiku MediaPipe në iOS

Shtoni varësitë përkatëse

Ne kemi shtuar tashmë varësitë e kodit të kornizës MediaPipe i cili përmban API-në iOS për të përdorur një grafik MediaPipe. Për të përdorur një grafik MediaPipe, duhet të shtojmë një varësi nga grafiku që synojmë të përdorim në aplikacionin tonë. Shtoni rreshtin e mëposhtëm në listën e data në skedarin tuaj BUILD :

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

Tani shtoni varësinë te kalkulatorët e përdorur në këtë grafik në fushën deps në skedarin BUILD :

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

Më në fund, riemërtoni skedarin ViewController.mViewController.mm për të mbështetur Objective-C++.

Përdorni grafikun në ViewController

ViewController.m , shtoni linjën e mëposhtme të importit:

#import "mediapipe/objc/MPPGraph.h"

Deklaroni një konstante statike me emrin e grafikut, rrymën hyrëse dhe rrjedhën dalëse:

static NSString* const kGraphName = @"mobile_gpu";

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

Shtoni veçorinë e mëposhtme në ndërfaqen e ViewController :

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

Siç shpjegohet në komentin e mësipërm, ne fillimisht do ta inicializojmë këtë grafik në viewDidLoad . Për ta bërë këtë, ne duhet të ngarkojmë grafikun nga skedari .pbtxt duke përdorur funksionin e mëposhtëm:

+   (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;
}

Përdorni këtë funksion për të inicializuar grafikun në viewDidLoad si më poshtë:

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

Grafiku duhet t'i dërgojë rezultatet e përpunimit të kornizave të kamerës përsëri te ViewController . Shtoni rreshtin e mëposhtëm pas inicializimit të grafikut për të vendosur ViewController si delegat të objektit mediapipeGraph :

self.mediapipeGraph.delegate = self;

Për të shmangur grindjet e kujtesës gjatë përpunimit të kornizave nga furnizimi i drejtpërdrejtë i videos, shtoni rreshtin e mëposhtëm:

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

Tani, filloni grafikun kur përdoruesi ka dhënë lejen për të përdorur kamerën në aplikacionin tonë:

[_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];
    });
  }
}];

Më parë, kur merrnim korniza nga kamera në funksionin processVideoFrame , ne i shfaqnim ato në _liveView duke përdorur _renderer . Tani, ne duhet t'i dërgojmë ato korniza në grafik dhe në vend të kësaj të japim rezultatet. Ndryshoni zbatimin e këtij funksioni për të bërë sa më poshtë:

-   (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];
}

Ne dërgojmë imageBufferself.mediapipeGraph si një paketë të tipit MPPPacketTypePixelBuffer në rrjedhën hyrëse kInputStream , dmth. "input_video".

Grafiku do të ekzekutohet me këtë paketë hyrëse dhe do të nxjerrë një rezultat në kOutputStream , p.sh. "output_video". Ne mund të zbatojmë metodën e mëposhtme të delegimit për të marrë paketa në këtë rrjedhë dalëse dhe për t'i shfaqur ato në ekran:

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

Përditësoni përkufizimin e ndërfaqes së ViewController me MPPGraphDelegate :

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

Dhe kjo është e gjitha! Ndërtoni dhe ekzekutoni aplikacionin në pajisjen tuaj iOS. Ju duhet të shihni rezultatet e ekzekutimit të grafikut të zbulimit të skajeve në një furnizim video të drejtpërdrejtë. urime!

edge_detection_ios_gpu_gif

Ju lutemi vini re se shembujt e iOS tani përdorin një aplikacion të zakonshëm shabllonesh. Kodi në këtë tutorial përdoret në aplikacionin e zakonshëm të shabllonit. Aplikacioni helloworld ka varësitë e duhura të skedarit BUILD për grafikun e zbulimit të skajeve.