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

บทนำ

สวัสดีโลกนี้ บทแนะนำ ใช้เฟรมเวิร์ก MediaPipe เพื่อพัฒนา 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 > ใหม่ > แอป Single View

ตั้งชื่อผลิตภัณฑ์เป็น " 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 ตัวอย่างเช่น ซอร์สโค้ดของ ที่เราจะสร้างขึ้นในบทแนะนำนี้ 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 ทำให้ ยูทิลิตีอื่นที่ชื่อว่า 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 ใช้แอปเทมเพลตทั่วไป โค้ดใน บทแนะนำนี้ใช้ในแอปเทมเพลตทั่วไป แอป helloworld มี ทรัพยากร Dependency ของไฟล์ BUILD ที่เหมาะสมสำหรับกราฟการตรวจจับขอบ