借助 MediaPipe Hand Landmarker 任务,您可以检测图片中的手的特征点。 以下说明介绍了如何在 iOS 应用中使用手部特征器。GitHub 上提供了这些说明中所述的代码示例。
如需详细了解此任务的功能、模型和配置选项,请参阅概览。
代码示例
MediaPipe Tasks 示例代码是适用于 iOS 的 Hand Markerer 应用的基本实现。该示例使用 iOS 实体设备上的相机来检测连续视频串流中的手部地标。该应用还可以检测设备图库中的图片和视频中的手部地标。
您可以将该应用用作自己的 iOS 应用的起点,也可以在修改现有应用时参考该应用。手部地标示例代码托管在 GitHub 上。
下载代码
以下说明介绍了如何使用 git 命令行工具创建示例代码的本地副本。
如需下载示例代码,请执行以下操作:
使用以下命令克隆 Git 代码库:
git clone https://github.com/google-ai-edge/mediapipe-samples
(可选)将您的 Git 实例配置为使用稀疏签出,这样您便只拥有 Hand Markerer 示例应用的文件:
cd mediapipe git sparse-checkout init --cone git sparse-checkout set examples/hand_landmarker/ios/
创建示例代码的本地版本后,您可以安装 MediaPipe 任务库,使用 Xcode 打开项目并运行应用。如需了解相关说明,请参阅 适用于 iOS 的设置指南。
关键组件
以下文件包含手部地标示例应用的重要代码:
- HandLandmarkerService.swift:初始化手部地标,处理模型选择,并对输入数据运行推理。
- CameraViewController.swift:为实时摄像头画面输入模式实现界面,并直观呈现结果。
- MediaLibraryViewController.swift:实现静态图片和视频文件输入模式的界面,并直观呈现结果。
设置
本部分介绍了设置开发环境和代码项目以使用 Hand Markerer 的关键步骤。如需了解如何设置开发环境以使用 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
Image
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
Image
@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:)
方法,在处理每一帧的手部特征点检测结果后,手部特征点会调用该方法。
选项名称 | 说明 | 值范围 | 默认值 |
---|---|---|---|
handLandmarkerLiveStreamDelegate |
启用手部特征器,以在直播模式下异步接收手部特征点检测结果。实例设为此属性的类必须实现 handLandmarker(_:didFinishDetection:timestampInMilliseconds:error:) 方法。 |
不适用 | 未设置 |
准备数据
您需要先将输入图片或帧转换为 MPImage
对象,然后才能将其传递给手部地标定位器。MPImage
支持不同类型的 iOS 图片格式,并且可以在任何运行模式下使用这些格式进行推理。如需详细了解 MPImage
,请参阅 MPImage API。
根据您的用例和应用所需的运行模式选择 iOS 映像格式。MPImage
接受 UIImage
、CVPixelBuffer
和 CMSampleBuffer
iOS 映像格式。
UIImage
UIImage
格式非常适合以下运行模式:
图片:应用软件包、用户图库或文件系统中格式为
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 AVCaptureVideoDataOutput 以 CMSampleBuffer
格式异步传送。
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 开发者文档。
运行任务
如需运行手部特征点,请使用特定于分配的跑步模式的 detect()
方法:
- 静态图片:
detect(image:)
- 视频:
detect(videoFrame:timestampInMilliseconds:)
- 直播:
detectAsync(image:timestampInMilliseconds:)
Swift
Image
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
Image
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 Dispatch 或 NSOperation 框架在后台线程中执行处理。
在直播模式下运行时,手部地标任务会立即返回,并且不会阻塞当前线程。处理每个输入帧后,它会对手形地标器结果调用
handLandmarker(_:didFinishDetection:timestampInMilliseconds:error:)
方法。手部地标器在专用串行调度队列中异步调用此方法。如需在界面上显示结果,请在处理结果后将结果分派到主队列。如果在手部地标定位器任务忙于处理另一个帧时调用detectAsync
函数,手部地标定位器会忽略新的输入帧。
处理和显示结果
运行推理后,手部特征点检测器任务会返回一个 HandLandmarkerResult
,其中包含图像坐标中的手部特征点、世界坐标中的手部特征点以及检测到的手的左右手性。
以下是此任务的输出数据示例:
HandLandmarkerResult
输出包含三个组成部分。每个组成部分都是一个数组,其中每个元素都包含所检测到的单个手的以下结果:
惯用手
惯用手表示检测到的手是左手还是右手。
地标
手部地标共有 21 个,每个地标都由
x
、y
和z
坐标组成。x
和y
坐标分别按图片宽度和高度归一化为 [0.0, 1.0]。z
坐标表示地标深度,其中手腕处的深度为原点。值越小,地标离相机越近。z
的大小与x
大致相同。世界地标
21 个手部特征点也以世界坐标表示。每个地标都由
x
、y
和z
组成,分别表示现实世界的 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)
下图显示了任务输出的可视化结果: