适用于 iOS 的手部特征检测指南

借助 MediaPipe Hand Landmarker 任务,您可以检测图片中的手的特征点。 以下说明介绍了如何将手部地标检测器与 iOS 应用搭配使用。这些说明中介绍的代码示例可在 GitHub 上找到。

如需详细了解此任务的功能、模型和配置选项,请参阅概览

代码示例

MediaPipe Tasks 示例代码是对适用于 iOS 的手部地标定位应用的基本实现。该示例使用实体 iOS 设备上的摄像头在连续视频流中检测手部地标。该应用还可以检测设备图库中的图片和视频中的手部地标。

您可以将该应用用作自己的 iOS 应用的起点,也可以在修改现有应用时参考该应用。手部地标示例代码托管在 GitHub 上。

下载代码

以下说明介绍了如何使用 git 命令行工具创建示例代码的本地副本。

如需下载示例代码,请执行以下操作:

  1. 使用以下命令克隆 Git 代码库:

    git clone https://github.com/google-ai-edge/mediapipe-samples
    
  2. (可选)将您的 Git 实例配置为使用稀疏检出,以便您只保留 Hand Landmarker 示例应用的文件:

    cd mediapipe-samples
    git sparse-checkout init --cone
    git sparse-checkout set examples/hand_landmarker/ios/
    

创建示例代码的本地版本后,您可以安装 MediaPipe 任务库,使用 Xcode 打开项目并运行应用。如需了解相关说明,请参阅 适用于 iOS 的设置指南

关键组件

以下文件包含手部地标示例应用的重要代码:

设置

本部分介绍了设置开发环境和编写代码项目以使用手部地标检测器的关键步骤。如需了解如何设置开发环境以使用 MediaPipe 任务(包括平台版本要求)的一般信息,请参阅 适用于 iOS 的设置指南

依赖项

手部地标检测器使用 MediaPipeTasksVision 库,该库必须使用 CocoaPods 安装。该库与 Swift 和 Objective-C 应用兼容,并且无需任何额外的语言专用设置。

如需了解如何在 macOS 上安装 CocoaPods,请参阅 CocoaPods 安装指南。如需了解如何创建包含应用所需 pod 的 Podfile,请参阅使用 CocoaPods

使用以下代码在 Podfile 中添加 MediaPipeTasksVision pod:

target 'MyHandLandmarkerApp' do
  use_frameworks!
  pod 'MediaPipeTasksVision'
end

如果您的应用包含单元测试目标,请参阅 iOS 设置指南,详细了解如何设置 Podfile

型号

MediaPipe 手部地标任务需要与此任务兼容的训练有素的模型。如需详细了解适用于手部地标检测器的已训练模型,请参阅任务概览的“模型”部分

选择并下载模型,然后使用 Xcode 将其添加到项目目录中。 如需了解如何向 Xcode 项目添加文件,请参阅管理 Xcode 项目中的文件和文件夹

使用 BaseOptions.modelAssetPath 属性指定 app bundle 中的模型路径。如需查看代码示例,请参阅下一部分。

创建任务

您可以通过调用其某个初始化程序来创建手部地标任务。HandLandmarker(options:) 初始化程序接受配置选项的值。

如果您不需要使用自定义配置选项初始化手部地标检测器,可以使用 HandLandmarker(modelPath:) 初始化程序使用默认选项创建手部地标检测器。如需详细了解配置选项,请参阅配置概览

手部地标任务支持 3 种输入数据类型:静态图片、视频文件和实时视频直播。默认情况下,HandLandmarker(modelPath:) 会为静态图片初始化任务。如果您希望任务初始化以处理视频文件或实时视频串流,请使用 HandLandmarker(options:) 指定视频或直播运行模式。直播模式还需要额外的 handLandmarkerLiveStreamDelegate 配置选项,该选项可让手部地标检测器异步将手部地标检测结果传递给代理。

选择与您的运行模式对应的标签页,了解如何创建任务并运行推理。

Swift

import MediaPipeTasksVision

let modelPath = Bundle.main.path(forResource: "hand_landmarker",
                                      ofType: "task")

let options = HandLandmarkerOptions()
options.baseOptions.modelAssetPath = modelPath
options.runningMode = .image
options.minHandDetectionConfidence = minHandDetectionConfidence
options.minHandPresenceConfidence = minHandPresenceConfidence
options.minTrackingConfidence = minHandTrackingConfidence
options.numHands = numHands

let handLandmarker = try HandLandmarker(options: options)
    
import MediaPipeTasksVision

let modelPath = Bundle.main.path(forResource: "hand_landmarker",
                                      ofType: "task")

let options = HandLandmarkerOptions()
options.baseOptions.modelAssetPath = modelPath
options.runningMode = .video
options.minHandDetectionConfidence = minHandDetectionConfidence
options.minHandPresenceConfidence = minHandPresenceConfidence
options.minTrackingConfidence = minHandTrackingConfidence
options.numHands = numHands

let handLandmarker = try HandLandmarker(options: options)
    
import MediaPipeTasksVision

// Class that conforms to the `HandLandmarkerLiveStreamDelegate` protocol and
// implements the method that the hand landmarker calls once it finishes
// performing landmarks detection in each input frame.
class HandLandmarkerResultProcessor: NSObject, HandLandmarkerLiveStreamDelegate {

  func handLandmarker(
    _ handLandmarker: HandLandmarker,
    didFinishDetection result: HandLandmarkerResult?,
    timestampInMilliseconds: Int,
    error: Error?) {

    // Process the hand landmarker result or errors here.

  }
}

let modelPath = Bundle.main.path(
  forResource: "hand_landmarker",
  ofType: "task")

let options = HandLandmarkerOptions()
options.baseOptions.modelAssetPath = modelPath
options.runningMode = .liveStream
options.minHandDetectionConfidence = minHandDetectionConfidence
options.minHandPresenceConfidence = minHandPresenceConfidence
options.minTrackingConfidence = minHandTrackingConfidence
options.numHands = numHands

// Assign an object of the class to the `handLandmarkerLiveStreamDelegate`
// property.
let processor = HandLandmarkerResultProcessor()
options.handLandmarkerLiveStreamDelegate = processor

let handLandmarker = try HandLandmarker(options: options)
    

Objective-C

@import MediaPipeTasksVision;

NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"hand_landmarker"
                                                      ofType:@"task"];

MPPHandLandmarkerOptions *options = [[MPPHandLandmarkerOptions alloc] init];
options.baseOptions.modelAssetPath = modelPath;
options.runningMode = MPPRunningModeImage;
options.minHandDetectionConfidence = minHandDetectionConfidence;
options.minHandPresenceConfidence = minHandPresenceConfidence;
options.minTrackingConfidence = minHandTrackingConfidence;
options.numHands = numHands;

MPPHandLandmarker *handLandmarker =
  [[MPPHandLandmarker alloc] initWithOptions:options error:nil];
    
@import MediaPipeTasksVision;

NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"hand_landmarker"
                                                      ofType:@"task"];

MPPHandLandmarkerOptions *options = [[MPPHandLandmarkerOptions alloc] init];
options.baseOptions.modelAssetPath = modelPath;
options.runningMode = MPPRunningModeVideo;
options.minHandDetectionConfidence = minHandDetectionConfidence;
options.minHandPresenceConfidence = minHandPresenceConfidence;
options.minTrackingConfidence = minHandTrackingConfidence;
options.numHands = numHands;

MPPHandLandmarker *handLandmarker =
  [[MPPHandLandmarker alloc] initWithOptions:options error:nil];
    
@import MediaPipeTasksVision;

// Class that conforms to the `MPPHandLandmarkerLiveStreamDelegate` protocol
// and implements the method that the hand landmarker calls once it finishes
// performing landmarks detection in each input frame.

@interface APPHandLandmarkerResultProcessor : NSObject 

@end

@implementation APPHandLandmarkerResultProcessor

-   (void)handLandmarker:(MPPHandLandmarker *)handLandmarker
    didFinishDetectionWithResult:(MPPHandLandmarkerResult *)handLandmarkerResult
         timestampInMilliseconds:(NSInteger)timestampInMilliseconds
                           error:(NSError *)error {

    // Process the hand landmarker result or errors here.

}

@end

NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"hand_landmarker"
                                                      ofType:@"task"];

MPPHandLandmarkerOptions *options = [[MPPHandLandmarkerOptions alloc] init];
options.baseOptions.modelAssetPath = modelPath;
options.runningMode = MPPRunningModeLiveStream;
options.minHandDetectionConfidence = minHandDetectionConfidence;
options.minHandPresenceConfidence = minHandPresenceConfidence;
options.minTrackingConfidence = minHandTrackingConfidence;
options.numHands = numHands;

// Assign an object of the class to the `handLandmarkerLiveStreamDelegate`
// property.
APPHandLandmarkerResultProcessor *processor =
  [APPHandLandmarkerResultProcessor new];
options.handLandmarkerLiveStreamDelegate = processor;

MPPHandLandmarker *handLandmarker =
  [[MPPHandLandmarker alloc] initWithOptions:options error:nil];
    

配置选项

此任务针对 iOS 应用提供了以下配置选项:

选项名称 说明 值范围 默认值
running_mode 设置任务的运行模式。共有三种模式:

IMAGE:适用于单张图片输入的模式。

视频:视频的解码帧的模式。

LIVE_STREAM:输入数据(例如来自摄像头)的直播模式。在此模式下,必须调用 resultListener 以设置监听器以异步接收结果。 在此模式下,handLandmarkerLiveStreamDelegate 必须设置为实现 HandLandmarkerLiveStreamDelegate 的类的实例,以异步接收手部地标检测结果。
{RunningMode.image, RunningMode.video, RunningMode.liveStream} RunningMode.image
numHands 手部特征点检测器检测到的手的数量上限。 Any integer > 0 1
minHandDetectionConfidence 在手掌检测模型中,手检测被视为成功所需的最低置信度得分。 0.0 - 1.0 0.5
minHandPresenceConfidence 手掌地标检测模型中手掌存在得分的最小置信度得分。在视频模式和直播模式下,如果手部地标模型的手部存在置信度得分低于此阈值,手部地标定位器会触发手掌检测模型。否则,轻量级手部跟踪算法会确定手的具体位置,以便进行后续的特征点检测。 0.0 - 1.0 0.5
minTrackingConfidence 手部跟踪被视为成功所需的最低置信度得分。这是当前帧和上一帧中手部之间的边界框 IoU 阈值。在手部特征点检测器的“视频”模式和“流式传输”模式下,如果跟踪失败,手部特征点检测器会触发手部检测。否则,它会跳过手部检测。 0.0 - 1.0 0.5
result_listener 设置结果监听器,以便在手部地标检测器处于实时流式传输模式时异步接收检测结果。 仅在运行模式设置为 LIVE_STREAM 时适用 不适用 不适用

将运行模式设置为直播时,手部地标检测器需要额外的 handLandmarkerLiveStreamDelegate 配置选项,以便手部地标检测器异步提供手部地标检测结果。代理必须实现 handLandmarker(_:didFinishDetection:timestampInMilliseconds:error:) 方法,Hand Landmarker 会在处理每个帧的手部地标检测结果后调用该方法。

选项名称 说明 值范围 默认值
handLandmarkerLiveStreamDelegate 让手部地标检测器能够在直播模式下异步接收手部地标检测结果。实例设为此属性的类必须实现 handLandmarker(_:didFinishDetection:timestampInMilliseconds:error:) 方法。 不适用 未设置

准备数据

您需要先将输入图片或帧转换为 MPImage 对象,然后才能将其传递给手部地标定位器。MPImage 支持不同类型的 iOS 图片格式,并且可以在任何运行模式下使用这些格式进行推理。如需详细了解 MPImage,请参阅 MPImage API

根据您的用例和应用所需的运行模式选择 iOS 图片格式。MPImage 接受 UIImageCVPixelBufferCMSampleBuffer iOS 图片格式。

UIImage

UIImage 格式非常适合以下运行模式:

  • 图片:应用 bundle、用户图库或文件系统中格式为 UIImage 图片的图片可以转换为 MPImage 对象。

  • 视频:使用 AVAssetImageGenerator 将视频帧提取为 CGImage 格式,然后将其转换为 UIImage 图片。

// Load an image on the user's device as an iOS `UIImage` object.

// Convert the `UIImage` object to a MediaPipe's Image object having the default
// orientation `UIImage.Orientation.up`.
let image = try MPImage(uiImage: image)
    
// Load an image on the user's device as an iOS `UIImage` object.

// Convert the `UIImage` object to a MediaPipe's Image object having the default
// orientation `UIImageOrientationUp`.
MPImage *image = [[MPPImage alloc] initWithUIImage:image error:nil];
    

该示例使用默认的 UIImage.Orientation.Up 方向初始化 MPImage。您可以使用任何受支持的 UIImage.Orientation 值初始化 MPImage。手部地标检测器不支持镜像屏幕方向,例如 .upMirrored.downMirrored.leftMirrored.rightMirrored

如需详细了解 UIImage,请参阅 UIImage Apple 开发者文档

CVPixelBuffer

CVPixelBuffer 格式非常适合生成帧并使用 iOS CoreImage 框架进行处理的应用。

CVPixelBuffer 格式非常适合以下运行模式:

  • 图片:在图片运行模式下,使用 iOS 的 CoreImage 框架进行一些处理后生成 CVPixelBuffer 图片的应用可以发送到手部地标检测器。

  • 视频:视频帧可以转换为 CVPixelBuffer 格式以进行处理,然后以视频模式发送到手部地标定位器。

  • 直播:使用 iOS 相机生成帧的应用可能会先转换为 CVPixelBuffer 格式进行处理,然后再以直播模式发送到手部地标检测器。

// Obtain a CVPixelBuffer.

// Convert the `CVPixelBuffer` object to a MediaPipe's Image object having the default
// orientation `UIImage.Orientation.up`.
let image = try MPImage(pixelBuffer: pixelBuffer)
    
// Obtain a CVPixelBuffer.

// Convert the `CVPixelBuffer` object to a MediaPipe's Image object having the
// default orientation `UIImageOrientationUp`.
MPImage *image = [[MPPImage alloc] initWithUIImage:image error:nil];
    

如需详细了解 CVPixelBuffer,请参阅 CVPixelBuffer Apple 开发者文档

CMSampleBuffer

CMSampleBuffer 格式用于存储统一媒体类型的媒体样本,非常适合直播运行模式。iOS 摄像头的实时帧由 iOS AVCaptureVideoDataOutputCMSampleBuffer 格式异步传送。

// Obtain a CMSampleBuffer.

// Convert the `CMSampleBuffer` object to a MediaPipe's Image object having the default
// orientation `UIImage.Orientation.up`.
let image = try MPImage(sampleBuffer: sampleBuffer)
    
// Obtain a `CMSampleBuffer`.

// Convert the `CMSampleBuffer` object to a MediaPipe's Image object having the
// default orientation `UIImageOrientationUp`.
MPImage *image = [[MPPImage alloc] initWithSampleBuffer:sampleBuffer error:nil];
    

如需详细了解 CMSampleBuffer,请参阅 CMSampleBuffer Apple 开发者文档

运行任务

如需运行手部地标定位器,请使用特定于分配的运行模式的 detect() 方法:

  • 静态图片:detect(image:)
  • 视频:detect(videoFrame:timestampInMilliseconds:)
  • 直播:detectAsync(image:timestampInMilliseconds:)

Swift

let result = try handLandmarker.detect(image: image)
    
let result = try handLandmarker.detect(
    videoFrame: image,
    timestampInMilliseconds: timestamp)
    
try handLandmarker.detectAsync(
  image: image,
  timestampInMilliseconds: timestamp)
    

Objective-C

MPPHandLandmarkerResult *result =
  [handLandmarker detectInImage:image error:nil];
    
MPPHandLandmarkerResult *result =
  [handLandmarker detectInVideoFrame:image
             timestampInMilliseconds:timestamp
                               error:nil];
    
BOOL success =
  [handLandmarker detectAsyncInImage:image
             timestampInMilliseconds:timestamp
                               error:nil];
    

手部地标代码示例更详细地展示了每种模式的实现。示例代码允许用户在处理模式之间切换,但您的用例可能不需要这样做。

请注意以下几点:

  • 在视频模式或直播模式下运行时,您还必须向手部地标任务提供输入帧的时间戳。

  • 在图片或视频模式下运行时,手部地标定位器任务会阻塞当前线程,直到其处理完输入图片或帧。为避免阻塞当前线程,请使用 iOS DispatchNSOperation 框架在后台线程中执行处理。

  • 在直播模式下运行时,手部地标任务会立即返回,并且不会阻塞当前线程。它会在处理每个输入帧后,使用手部地标检测器结果调用 handLandmarker(_:didFinishDetection:timestampInMilliseconds:error:) 方法。手部地标定位器会在专用串行调度队列上异步调用此方法。如需在界面上显示结果,请在处理结果后将结果分派到主队列。如果在手部地标定位器任务忙于处理另一帧时调用 detectAsync 函数,手部地标定位器会忽略新的输入帧。

处理和显示结果

运行推理后,手部特征点检测器任务会返回一个 HandLandmarkerResult,其中包含图像坐标中的手部特征点、世界坐标中的手部特征点以及检测到的手的左右手性。

以下是此任务的输出数据示例:

HandLandmarkerResult 输出包含三个组成部分。每个组件都是一个数组,其中每个元素都包含单个检测到的手的以下结果:

  • 惯用手

    惯用手表示检测到的手是左手还是右手。

  • 地标

    手部地标共有 21 个,每个地标由 xyz 坐标组成。xy 坐标分别按图片宽度和高度归一化为 [0.0, 1.0]。z 坐标表示地标深度,其中手腕处的深度为原点。值越小,地标离相机越近。z 的大小与 x 大致相同。

  • 世界地标

    21 个手部特征点也以世界坐标表示。每个地标均由 xyz 组成,表示以米为单位的真实 3D 坐标,其原点位于手的几何中心。

HandLandmarkerResult:
  Handedness:
    Categories #0:
      index        : 0
      score        : 0.98396
      categoryName : Left
  Landmarks:
    Landmark #0:
      x            : 0.638852
      y            : 0.671197
      z            : -3.41E-7
    Landmark #1:
      x            : 0.634599
      y            : 0.536441
      z            : -0.06984
    ... (21 landmarks for a hand)
  WorldLandmarks:
    Landmark #0:
      x            : 0.067485
      y            : 0.031084
      z            : 0.055223
    Landmark #1:
      x            : 0.063209
      y            : -0.00382
      z            : 0.020920
    ... (21 world landmarks for a hand)

下图显示了任务输出的可视化结果:

一只手做出竖起大拇指的动作,手部的骨骼结构已绘制出来