สวัสดีทุกคน! บน iOS

เกริ่นนำ

บทแนะนำ Hello World! นี้ใช้ MediaPipe Framework ในการพัฒนาแอปพลิเคชัน iOS ที่เรียกใช้กราฟ MediaPipe บน iOS

สิ่งที่คุณจะสร้าง

แอปกล้องที่ใช้งานง่ายสำหรับการตรวจจับขอบ Sobel แบบเรียลไทม์ซึ่งใช้กับสตรีมวิดีโอสดในอุปกรณ์ iOS

edge_detection_ios_gpu_gif

ตั้งค่า

  1. ติดตั้งเฟรมเวิร์ก MediaPipe ในระบบของคุณ โปรดดูรายละเอียดที่คู่มือการติดตั้งเฟรมเวิร์ก
  2. ตั้งค่าอุปกรณ์ iOS เพื่อการพัฒนา
  3. ตั้งค่า Bazel ในระบบเพื่อสร้างและทำให้แอป iOS ใช้งานได้

กราฟสำหรับการตรวจจับขอบ

เราจะใช้กราฟ 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"
}

การแสดงภาพกราฟจะแสดงที่ด้านล่าง

edge_detection_mobile_gpu

กราฟนี้มีสตรีมอินพุตเดียวที่ชื่อ input_video สำหรับเฟรมขาเข้าทั้งหมดซึ่งกล้องของอุปกรณ์มีให้

โหนดแรกในกราฟ LuminanceCalculator รับแพ็กเก็ตเดี่ยว (เฟรมรูปภาพ) และเปลี่ยนความสว่างโดยใช้ตัวปรับแสงเงา OpenGL ระบบจะส่งเฟรมรูปภาพที่ได้ไปยังสตรีมเอาต์พุต luma_video

โหนดที่ 2 SobelEdgesCalculator จะใช้การตรวจจับ Edge กับแพ็กเก็ตขาเข้าในสตรีม luma_video และเอาต์พุตจะทำให้มีสตรีมเอาต์พุต output_video รายการ

แอปพลิเคชัน iOS จะแสดงเฟรมรูปภาพเอาต์พุตของสตรีม output_video

การตั้งค่าแอปพลิเคชันขั้นต่ำเริ่มต้น

เราเริ่มต้นด้วยแอปพลิเคชัน iOS ง่ายๆ ก่อนและสาธิตวิธีใช้ bazel เพื่อสร้าง

ก่อนอื่นให้สร้างโปรเจ็กต์ XCode ผ่าน File > New > Single View App

ตั้งชื่อผลิตภัณฑ์เป็น " HelloWorld" และใช้ตัวระบุองค์กรที่เหมาะสม เช่น com.google.mediapipe ตัวระบุองค์กรพร้อมด้วยชื่อผลิตภัณฑ์จะเป็น bundle_id สำหรับแอปพลิเคชัน เช่น com.google.mediapipe.HelloWorld

ตั้งค่าภาษาเป็น Objective-C

บันทึกโปรเจ็กต์ไปยังตำแหน่งที่เหมาะสม เรียกว่า $PROJECT_TEMPLATE_LOC ดังนั้นโปรเจ็กต์จะอยู่ในไดเรกทอรี $PROJECT_TEMPLATE_LOC/HelloWorld ไดเรกทอรีนี้จะมีอีกไดเรกทอรีหนึ่งชื่อ HelloWorld และไฟล์ HelloWorld.xcodeproj

HelloWorld.xcodeproj จะไม่มีประโยชน์สำหรับบทแนะนำนี้ เนื่องจากเราจะใช้ Bazel ในการสร้างแอปพลิเคชัน iOS เนื้อหาของไดเรกทอรี $PROJECT_TEMPLATE_LOC/HelloWorld/HelloWorld แสดงอยู่ด้านล่าง

  1. AppDelegate.hและAppDelegate.m
  2. ViewController.hและViewController.m
  3. main.m
  4. Info.plist
  5. Main.storyboardและLaunch.storyboard
  6. ไดเรกทอรี Assets.xcassets

คัดลอกไฟล์เหล่านี้ไปยังไดเรกทอรีชื่อ HelloWorld ไปยังตำแหน่งที่เข้าถึงซอร์สโค้ดของ MediaPipe Framework ได้ ตัวอย่างเช่น ซอร์สโค้ดของแอปพลิเคชันที่เราจะสร้างในบทแนะนำนี้อยู่ใน mediapipe/examples/ios/HelloWorld เราจะเรียกเส้นทางนี้ว่า $APPLICATION_PATH ใน Codelab

สร้างไฟล์ BUILD ใน $APPLICATION_PATH และเพิ่มกฎบิลด์ต่อไปนี้

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

กฎ objc_library เพิ่มทรัพยากร Dependency สำหรับคลาส AppDelegate และ ViewController, main.m และสตอรีบอร์ดของแอปพลิเคชัน แอปที่ใช้เทมเพลตต้องใช้ SDK ของ UIKit เท่านั้น

กฎ ios_application ใช้ไลบรารี Objective-C ของ HelloWorldAppLibrary ที่สร้างขึ้นเพื่อสร้างแอปพลิเคชัน iOS สำหรับการติดตั้งในอุปกรณ์ iOS

หากต้องการสร้างแอป ให้ใช้คำสั่งต่อไปนี้ในเทอร์มินัล:

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

เช่น หากต้องการสร้างแอปพลิเคชัน HelloWorldApp ใน mediapipe/examples/ios/helloworld ให้ใช้คำสั่งต่อไปนี้

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

จากนั้นกลับไปที่ XCode เปิดหน้าต่าง > อุปกรณ์และเครื่องมือจำลอง เลือกอุปกรณ์ของคุณ แล้วเพิ่มไฟล์ .ipa ที่สร้างขึ้นจากคำสั่งด้านบนลงในอุปกรณ์ นี่คือเอกสารเกี่ยวกับการตั้งค่าและคอมไพล์แอป iOS Framework

เปิดแอปพลิเคชันบนอุปกรณ์ เนื่องจากเป็นพื้นที่ว่าง จึงควรแสดงหน้าจอ ว่างเปล่าสีขาว

ใช้กล้องสําหรับฟีด Live View

ในบทแนะนำนี้ เราจะใช้คลาส MPPCameraInputSource เพื่อเข้าถึงและจับเฟรมจากกล้อง คลาสนี้ใช้ AVCaptureSession API เพื่อรับเฟรมจากกล้อง

แต่ก่อนที่จะใช้คลาสนี้ ให้เปลี่ยนไฟล์ Info.plist เพื่อรองรับการใช้งานกล้องในแอป

ใน ViewController.m ให้เพิ่มบรรทัดนำเข้าต่อไปนี้

#import "mediapipe/objc/MPPCameraInputSource.h"

เพิ่มรายการต่อไปนี้ลงในบล็อกการใช้งานเพื่อสร้างออบเจ็กต์ _cameraSource

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

เพิ่มโค้ดต่อไปนี้ใน 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;
}

โค้ดจะเริ่มต้น _cameraSource ตั้งค่าที่กำหนดล่วงหน้าของเซสชันการจับภาพ และกล้องที่จะใช้

เราต้องรับเฟรมจาก _cameraSource ลงในแอปพลิเคชัน ViewController เพื่อแสดง MPPCameraInputSource เป็นกลุ่มย่อยของ MPPInputSource ซึ่งมอบโปรโตคอลสำหรับผู้รับมอบสิทธิ์ ซึ่งก็คือ MPPInputSourceDelegate ดังนั้นใบสมัคร ViewController ของเราจึงได้รับมอบสิทธิ์ _cameraSource

อัปเดตคําจํากัดความของอินเทอร์เฟซ ViewController ให้สอดคล้องกันดังนี้

@interface ViewController () <MPPInputSourceDelegate>

ในการจัดการการตั้งค่ากล้องและประมวลผลเฟรมขาเข้า เราควรใช้คิวที่แตกต่างจากคิวหลัก เพิ่มโค้ดต่อไปนี้ลงในบล็อกการติดตั้งใช้งานของ ViewController

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

ใน viewDidLoad() ให้เพิ่มบรรทัดต่อไปนี้หลังจากเริ่มต้นออบเจ็กต์ _cameraSource

[_cameraSource setDelegate:self queue:_videoQueue];

และเพิ่มโค้ดต่อไปนี้เพื่อเริ่มต้นคิวก่อนตั้งค่าออบเจ็กต์ _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);

เราจะใช้คิวอนุกรมที่มีลำดับความสำคัญ QOS_CLASS_USER_INTERACTIVE สำหรับการประมวลผลเฟรมกล้อง

เพิ่มบรรทัดต่อไปนี้หลังจากนำเข้าส่วนหัวที่ด้านบนของไฟล์ ก่อนอินเทอร์เฟซ/การใช้งานของ ViewController

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

ก่อนใช้เมธอดจากโปรโตคอล MPPInputSourceDelegate ก่อนอื่น เราต้องตั้งค่าวิธีแสดงเฟรมกล้อง Mediapipe Framework มียูทิลิตีอีกชื่อหนึ่งชื่อว่า MPPLayerRenderer เพื่อแสดงรูปภาพบนหน้าจอ ยูทิลิตีนี้ใช้เพื่อแสดงออบเจ็กต์ CVPixelBufferRef รายการได้ ซึ่งเป็นประเภทของรูปภาพที่ MPPCameraInputSource ให้ไว้แก่ผู้รับมอบสิทธิ์

ใน ViewController.m ให้เพิ่มบรรทัดนำเข้าต่อไปนี้

#import "mediapipe/objc/MPPLayerRenderer.h"

ในการแสดงรูปภาพของหน้าจอ เราต้องเพิ่มออบเจ็กต์ UIView ใหม่ที่ชื่อว่า _liveView ไปยัง ViewController

เพิ่มบรรทัดต่อไปนี้ลงในบล็อกการติดตั้งใช้งานของ ViewController

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

ไปที่ Main.storyboard แล้วเพิ่มออบเจ็กต์ UIView จากไลบรารีออบเจ็กต์ลงใน View ของคลาส ViewController เพิ่มเอาต์พุตการอ้างอิงจากมุมมองนี้ไปยังออบเจ็กต์ _liveView ที่คุณเพิ่งเพิ่มในคลาส ViewController ปรับขนาดมุมมองให้อยู่กึ่งกลางและครอบคลุมหน้าจอแอปพลิเคชันทั้งหมด

กลับไปที่ ViewController.m และเพิ่มโค้ดต่อไปนี้ลงใน viewDidLoad() เพื่อเริ่มต้นออบเจ็กต์ _renderer

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

หากต้องการรับเฟรมจากกล้อง เราจะใช้วิธีการต่อไปนี้

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

นี่คือเมธอดมอบสิทธิ์ของ MPPInputSource ก่อนอื่นเราตรวจสอบว่าเรากำลังรับเฟรม จากแหล่งที่มาที่ถูกต้อง ซึ่งก็คือ _cameraSource จากนั้น เราจะแสดงเฟรมที่ได้รับจากกล้องผ่าน _renderer บนคิวหลัก

คราวนี้เราต้องเริ่มเปิดกล้องทันทีที่มุมมองที่จะแสดงเฟรม ปรากฏขึ้น ในการดำเนินการนี้ เราจะใช้ฟังก์ชัน viewWillAppear:(BOOL)animated:

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

เราต้องได้รับอนุญาตจากผู้ใช้เพื่อเข้าถึงกล้องก่อนเริ่มเรียกใช้กล้อง MPPCameraInputSource มีฟังก์ชัน requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler เพื่อขอสิทธิ์เข้าถึงกล้องและทำงานบางอย่างเมื่อผู้ใช้ตอบกลับ เพิ่มโค้ดต่อไปนี้ใน viewWillAppear:animated:

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

ก่อนสร้างแอปพลิเคชัน ให้เพิ่มทรัพยากร Dependency ต่อไปนี้ลงในไฟล์ BUILD ของคุณ

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

สร้างและเรียกใช้แอปพลิเคชันบนอุปกรณ์ iOS ของคุณได้เลย คุณควรจะเห็นฟีดมุมมองกล้องแบบสด หลังจากยอมรับสิทธิ์เข้าถึงกล้อง

ตอนนี้เราพร้อมใช้เฟรมกล้องในกราฟ MediaPipe แล้ว

การใช้กราฟ MediaPipe ใน iOS

เพิ่มทรัพยากร Dependency ที่เกี่ยวข้อง

เราได้เพิ่มทรัพยากร Dependency ของโค้ดเฟรมเวิร์ก MediaPipe ซึ่งมี iOS API แล้วเพื่อใช้กราฟ MediaPipe ในการใช้กราฟ MediaPipe เราต้องเพิ่ม การขึ้นต่อกันบนกราฟที่เราตั้งใจจะใช้ในแอปพลิเคชันของเรา เพิ่มบรรทัดต่อไปนี้ลงในรายการ data ในไฟล์ BUILD

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

ตอนนี้ให้เพิ่มการอ้างอิงไปยังเครื่องคำนวณที่ใช้ในกราฟนี้ในช่อง deps ในไฟล์ BUILD ดังนี้

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

สุดท้าย ให้เปลี่ยนชื่อไฟล์ ViewController.m เป็น ViewController.mm เพื่อให้รองรับ Objective-C++

ใช้กราฟใน ViewController

ใน ViewController.m ให้เพิ่มบรรทัดนำเข้าต่อไปนี้

#import "mediapipe/objc/MPPGraph.h"

ประกาศค่าคงที่คงที่ด้วยชื่อของกราฟ สตรีมอินพุต และสตรีมเอาต์พุต ดังนี้

static NSString* const kGraphName = @"mobile_gpu";

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

เพิ่มพร็อพเพอร์ตี้ต่อไปนี้ในอินเทอร์เฟซของ ViewController

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

เราจะเริ่มต้นกราฟนี้ใน viewDidLoad ก่อนตามที่อธิบายไว้ในความคิดเห็นด้านบน ในการดำเนินการดังกล่าว เราต้องโหลดกราฟจากไฟล์ .pbtxt โดยใช้ฟังก์ชันต่อไปนี้

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

ใช้ฟังก์ชันนี้เพื่อเริ่มต้นกราฟใน viewDidLoad ดังนี้

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

กราฟควรส่งผลลัพธ์ของการประมวลผลเฟรมกล้องกลับไปที่ ViewController เพิ่มบรรทัดต่อไปนี้หลังจากเริ่มต้นกราฟเพื่อตั้ง ViewController เป็นผู้รับมอบสิทธิ์ของออบเจ็กต์ mediapipeGraph

self.mediapipeGraph.delegate = self;

หากต้องการหลีกเลี่ยงการแย่งชิงหน่วยความจําขณะประมวลผลเฟรมจากฟีดวิดีโอสด ให้เพิ่มบรรทัดต่อไปนี้

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

ต่อไปให้เริ่มกราฟเมื่อผู้ใช้ให้สิทธิ์ใช้กล้องในแอป ดังนี้

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

ก่อนหน้านี้ เมื่อได้รับเฟรมจากกล้องในฟังก์ชัน processVideoFrame เราแสดงใน _liveView โดยใช้ _renderer ทีนี้เราต้องส่งเฟรมเหล่านั้นไปยังกราฟและแสดงผลลัพธ์แทน แก้ไขการติดตั้งใช้งานของฟังก์ชันนี้เพื่อทำสิ่งต่อไปนี้

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

เราจะส่ง imageBuffer ไปยัง self.mediapipeGraph เป็นแพ็กเก็ตประเภท MPPPacketTypePixelBuffer ไปยังสตรีมอินพุต kInputStream นั่นคือ "input_video"

กราฟจะทํางานกับแพ็กเก็ตอินพุตนี้และแสดงผลเป็น kOutputStream เช่น "output_video" เราสามารถใช้วิธีการมอบสิทธิ์ต่อไปนี้เพื่อรับแพ็กเก็ตในสตรีมเอาต์พุตนี้ และแสดงบนหน้าจอได้

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

อัปเดตการกำหนดอินเทอร์เฟซของ ViewController ด้วย MPPGraphDelegate:

@interface ViewController () <MPPGraphDelegate, MPPInputSourceDelegate>

เท่านี้ก็เรียบร้อย สร้างและเรียกใช้แอปบนอุปกรณ์ iOS คุณควรจะเห็นผลลัพธ์ของการเรียกใช้กราฟการตรวจจับขอบในฟีดวิดีโอสด ยินดีด้วย!

edge_detection_ios_gpu_gif

โปรดทราบว่าตอนนี้ตัวอย่าง iOS ใช้แอปเทมเพลตcommon โค้ดในบทแนะนำนี้จะใช้ในแอปเทมเพลตcommon แอป helloworld มีการพึ่งพาไฟล์ BUILD ที่เหมาะสมสำหรับกราฟการตรวจจับขอบ