适用于 iOS 的手势识别指南

借助 MediaPipe 手势识别程序任务,您可以实时识别手势,并提供识别出的手势结果和检测到的手势的手部特征点。以下说明介绍了如何在 iOS 应用中使用手势识别程序。

您可以观看 Web 演示,了解此任务的实际效果。如需详细了解此任务的功能、模型和配置选项,请参阅概览

代码示例

MediaPipe Tasks 示例代码是适用于 iOS 的手势识别程序应用的基本实现。该示例使用 iOS 实体设备上的相机持续检测手势,还可以使用设备图库中的图片和视频来静态检测手势。

您可以使用该应用作为基础来开发自己的 iOS 应用,也可以在修改现有应用时参考该应用。手势识别程序示例代码托管在 GitHub 上。

下载代码

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

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

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

    git clone https://github.com/google-ai-edge/mediapipe-samples
    
  2. (可选)将您的 Git 实例配置为使用稀疏结账,这样您就只有手势识别程序示例应用的文件:

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

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

关键组件

以下文件包含手势识别程序示例应用的关键代码:

初始设置

本部分介绍了为使用手势识别程序而设置开发环境和代码项目的关键步骤。如需了解有关为使用 MediaPipe 任务设置开发环境的一般信息(包括平台版本要求),请参阅 iOS 设置指南

依赖项

手势识别程序使用 MediaPipeTasksVision 库,必须使用 CocoaPods 进行安装。该库与 Swift 和 Objective-C 应用兼容,不需要任何其他针对特定语言的设置。

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

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

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

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

模型

MediaPipe 手势识别程序任务需要使用与此任务兼容的经过训练的模型。如需详细了解手势识别程序可用的经过训练的模型,请参阅任务概览“模型”部分

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

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

创建任务

您可以通过调用手势识别程序任务之一来创建该任务。GestureRecognizer(options:) 初始化程序接受配置选项的值。

如果您不需要使用自定义配置选项初始化的手势识别程序,可以使用 GestureRecognizer(modelPath:) 初始化程序使用默认选项创建手势识别程序。如需详细了解配置选项,请参阅配置概览

手势识别程序任务支持 3 种输入数据类型:静态图片、视频文件和直播视频流。默认情况下,GestureRecognizer(modelPath:) 会初始化静态图片任务。如果您希望将任务初始化为处理视频文件或直播视频流,请使用 GestureRecognizer(options:) 指定视频或直播的运行模式。直播模式还需要一个额外的 gestureRecognizerLiveStreamDelegate 配置选项,以便手势识别程序能够将手势识别结果异步传送给代理。

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

Swift

映像

import MediaPipeTasksVision

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

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

let gestureRecognizer = try GestureRecognizer(options: options)
    

视频

import MediaPipeTasksVision

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

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

let gestureRecognizer = try GestureRecognizer(options: options)
    

直播

import MediaPipeTasksVision

// Class that conforms to the `GestureRecognizerLiveStreamDelegate` protocol and
// implements the method that the gesture recognizer calls once it finishes
// performing recognizing hand gestures in each input frame.
class GestureRecognizerResultProcessor: NSObject, GestureRecognizerLiveStreamDelegate {

  func gestureRecognizer(
    _ gestureRecognizer: GestureRecognizer,
    didFinishRecognition result: GestureRecognizerResult?,
    timestampInMilliseconds: Int,
    error: Error?) {

    // Process the gesture recognizer result or errors here.

  }
}

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

let options = GestureRecognizerOptions()
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 `gestureRecognizerLiveStreamDelegate`
// property.
let processor = GestureRecognizerResultProcessor()
options.gestureRecognizerLiveStreamDelegate = processor

let gestureRecognizer = try GestureRecognizer(options: options)
    

Objective-C

映像

@import MediaPipeTasksVision;

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

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

MPPGestureRecognizer *gestureRecognizer =
      [[MPPGestureRecognizer alloc] initWithOptions:options error:nil];
    

视频

@import MediaPipeTasksVision;

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

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

MPPGestureRecognizer *gestureRecognizer =
      [[MPPGestureRecognizer alloc] initWithOptions:options error:nil];
    

直播

@import MediaPipeTasksVision;

// Class that conforms to the `MPPGestureRecognizerLiveStreamDelegate` protocol
// and implements the method that the gesture recognizer calls once it finishes
// performing gesture recognition on each input frame.

@interface APPGestureRecognizerResultProcessor : NSObject 

@end

@implementation APPGestureRecognizerResultProcessor

-   (void)gestureRecognizer:(MPPGestureRecognizer *)gestureRecognizer
    didFinishRecognitionWithResult:(MPPGestureRecognizerResult *)gestureRecognizerResult
           timestampInMilliseconds:(NSInteger)timestampInMilliseconds
                             error:(NSError *)error {

    // Process the gesture recognizer result or errors here.

}

@end

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

MPPGestureRecognizerOptions *options =
  [[MPPGestureRecognizerOptions 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 `gestureRecognizerLiveStreamDelegate`
// property.
APPGestureRecognizerResultProcessor *processor =
  [APPGestureRecognizerResultProcessor new];
options.gestureRecognizerLiveStreamDelegate = processor;

MPPGestureRecognizer *gestureRecognizer =
      [[MPPGestureRecognizer alloc] initWithOptions:options error:nil];
    

配置选项

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

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

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

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

LIVE_STREAM:输入数据(例如来自摄像头)的直播的模式。在此模式下,必须调用 resultListener 来设置监听器,以异步接收结果。 在此模式下,必须将 gestureRecognizerLiveStreamDelegate 设置为实现 GestureRecognizerLiveStreamDelegate 的类的实例,才能接收异步执行手势识别的结果。
{RunningMode.image, RunningMode.video, RunningMode.liveStream} RunningMode.image
num_hands GestureRecognizer 可检测出手的数量上限。 Any integer > 0 1
min_hand_detection_confidence 要在手掌检测模型中被视为成功的手部检测的最低置信度分数。 0.0 - 1.0 0.5
min_hand_presence_confidence 手部特征点检测模型中手部存在分数的最低置信度分数。在手势识别程序的视频模式和直播模式下,如果手部特征点模型的手部存在置信度分数低于此阈值,则会触发手掌检测模型。否则,将使用轻量级手部跟踪算法确定手部位置,以便进行后续地标检测。 0.0 - 1.0 0.5
min_tracking_confidence 手部跟踪被视为成功所需的最低置信度分数。这是当前帧和最后一帧中手之间的边界框 IoU 阈值。在手势识别程序的视频模式和流模式下,如果跟踪失败,手势识别程序会触发手部检测。否则,系统会跳过手部检测。 0.0 - 1.0 0.5
canned_gestures_classifier_options 用于配置预设手势分类器行为的选项。预设手势如下:["None", "Closed_Fist", "Open_Palm", "Pointing_Up", "Thumb_Down", "Thumb_Up", "Victory", "ILoveYou"]
  • 显示名称语言区域:要用于通过 TFLite 模型元数据(如果有)指定的显示名称的语言区域。
  • 最大结果数:要返回的得分最高的分类结果的数量上限。如果小于 0,将返回所有可用的结果。
  • 得分阈值:如果得分低于该得分,则结果将被拒绝。如果设置为 0,将返回所有可用的结果。
  • 类别许可名单:类别名称的许可名单。如果为非空,则会过滤掉类别不在此集合中的分类结果。它与拒绝名单互斥。
  • 类别拒绝名单:类别名称的拒绝名单。如果为非空,则会过滤掉类别在此集合中的分类结果。此政策与许可名单相互排斥。
    • 显示名称语言区域:any string
    • 结果数上限:any integer
    • 分数阈值:0.0-1.0
    • 类别许可名单:vector of strings
    • 类别拒绝名单:vector of strings
    • 显示名称语言区域:"en"
    • 结果数上限:-1
    • 分数阈值:0
    • 类别许可名单:空
    • 类别拒绝名单:空
    custom_gestures_classifier_options 用于配置自定义手势分类器行为的选项。
  • 显示名称语言区域:要用于通过 TFLite 模型元数据(如果有)指定的显示名称的语言区域。
  • 最大结果数:要返回的得分最高的分类结果的数量上限。如果小于 0,将返回所有可用的结果。
  • 得分阈值:如果得分低于该得分,则结果将被拒绝。如果设置为 0,将返回所有可用的结果。
  • 类别许可名单:类别名称的许可名单。如果为非空,则会过滤掉类别不在此集合中的分类结果。它与拒绝名单互斥。
  • 类别拒绝名单:类别名称的拒绝名单。如果为非空,则会过滤掉类别在此集合中的分类结果。此政策与许可名单相互排斥。
    • 显示名称语言区域:any string
    • 结果数上限:any integer
    • 分数阈值:0.0-1.0
    • 类别许可名单:vector of strings
    • 类别拒绝名单:vector of strings
    • 显示名称语言区域:"en"
    • 结果数上限:-1
    • 分数阈值:0
    • 类别许可名单:空
    • 类别拒绝名单:空
    result_listener 设置结果监听器,以在手势识别器处于直播模式时异步接收分类结果。 只能在跑步模式设为“LIVE_STREAM”时使用 ResultListener N/A N/A

    当运行模式设置为直播时,手势识别程序需要额外的 gestureRecognizerLiveStreamDelegate 配置选项,该选项可让手势识别程序以异步方式提供手势识别结果。代理必须实现 gestureRecognizer(_:didFinishRecognition:timestampInMilliseconds:error:) 方法,手势识别程序会在处理每一帧执行手势识别的结果后调用该方法。

    选项名称 说明 值范围 默认值
    gestureRecognizerLiveStreamDelegate 允许手势识别程序在直播模式下异步接收手势识别结果。将此属性设置为该属性的类必须实现 gestureRecognizer(_:didFinishRecognition:timestampInMilliseconds:error:) 方法。 不适用 未设置

    准备数据

    您需要先将输入图片或帧转换为 MPImage 对象,然后再将其传递给手势识别程序。MPImage 支持不同类型的 iOS 图片格式,可以在任何运行模式下使用它们进行推理。如需详细了解 MPImage,请参阅 MPImage API

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

    UIImage

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

    • 图片:可将 app bundle、用户图库或文件系统中的图片转换为 UIImage 图片格式,并将其转换为 MPImage 对象。

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

    Swift

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

    Objective-C

    // 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 格式以进行处理,然后再发送到直播模式下的手势识别程序。

    Swift

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

    Objective-C

    // 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 格式异步传送。

    Swift

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

    Objective-C

    // 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 开发者文档

    运行任务

    如需运行手势识别程序,请使用特定于指定运行模式的 recognize() 方法:

    • 静态图片:recognize(image:)
    • 视频:recognize(videoFrame:timestampInMilliseconds:)
    • 直播:recognizeAsync(image:timestampInMilliseconds:)

    以下代码示例展示了如何在这些不同的运行模式下运行手势识别程序的基本示例:

    Swift

    映像

    let result = try gestureRecognizer.recognize(image: image)
        

    视频

    let result = try gestureRecognizer.recognize(
      videoFrame: image,
      timestampInMilliseconds: timestamp)
        

    直播

    try gestureRecognizer.recognizeAsync(
      image: image,
      timestampInMilliseconds: timestamp)
        

    Objective-C

    映像

      MPPGestureRecognizerResult *result =
        [gestureRecognizer recognizeImage:mppImage
                                    error:nil];
        

    视频

    MPPGestureRecognizerResult *result =
      [gestureRecognizer recognizeVideoFrame:image
                     timestampInMilliseconds:timestamp
                                       error:nil];
        

    直播

    BOOL success =
      [gestureRecognizer recognizeAsyncImage:image
                     timestampInMilliseconds:timestamp
                                       error:nil];
        

    示例代码允许用户在处理模式之间切换,而您的用例可能不需要这些模式。

    请注意以下几点:

    • 在视频模式或直播模式下运行时,您还必须向手势识别程序任务提供输入帧的时间戳。

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

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

    处理和显示结果

    运行推断后,手势识别程序任务会返回一个 GestureRecognizerResult,其中包含图片坐标中的手部特征点、世界坐标中的手部特征点、惯用手(左/右手)以及检测到的手部的手势类别。

    下面显示了此任务的输出数据示例:

    生成的 GestureRecognizerResult 包含四个组成部分,每个组成部分都是一个数组,其中的每个元素都包含检测到一只手的检测结果。

    • 用手习惯

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

    • 手势

      检测到的手部的识别出的手势类别。

    • 地标

      共有 21 个手部位置标记,每个位置标记由 xyz 坐标组成。xy 坐标分别根据图片宽度和高度标准化为 [0.0, 1.0]。z 坐标表示地标深度,以手腕处的深度为原点。值越小,地标越靠近相机。z 的大小大小与 x 的大小大致相同。

    • 世界地标

      21 只手部位标也以世界坐标表示。每个地标由 xyz 组成,表示现实世界的 3D 坐标(以米为单位),原点位于手的几何中心。

    GestureRecognizerResult:
      Handedness:
        Categories #0:
          index        : 0
          score        : 0.98396
          categoryName : Left
      Gestures:
        Categories #0:
          score        : 0.76893
          categoryName : Thumb_Up
      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)
    

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