人体肢体动作捕捉在动漫影视制作、游戏CG 动画、实时模型驱动中有着广泛的应用,利用 ARKit,无须额外的硬件设备即可实现 2D和3D人体一系列关节和骨骼的动态捕捉,由于移动AR 的便携性及低成本,必将促进相关产业的发展。
ARBody TrackingConfiguration
ARKit 配置类 ARBodyTrackingConfiguration 专用于 2D、3D人体肢体检测捕捉,同时,该配置类也可以设置实现 2D 图像检测和平面检测,构建对现实环境的跟踪。为更真实地渲染虚拟元素,ARBodyTrackingConfiguration 还支持 HDR(High Dynamic Range Imaging,高动态范围成像)环境反射功能。其主要属性如下表所示。
|-----------------------------------------|--------------------------------------------------------------------------------|
| 属性名 | 描述 |
| automaticSkeletonScaleEstimationEnabled | 布尔值,指定 ARKit是否进行人体骨骼尺寸评估,设置为true 时,ARKit会根据人体距离摄像头的远近调整所驱动的模型大小,使其更匹配 |
| isAutoFocusEnabled | 设置是否自动对焦 |
| planeDetection | 在进行人体肢体检测跟踪时是否进行平面检测,可以设置为水平(horizontal)或者垂直(vertical),或者两者都设置。设置该值后就会启动平面检测功能 |
| automaticImageScaleEstimationEnabled | 自动评估检测到的2D图像的尺寸,这在设置2D 图像跟踪时有效 |
| detectionImages | 参考图像库 |
| maximum NumberOfTrackedImages | 最大可同时跟踪的2D 图像数量 |
| wantsHDREnvironment Textures | 是否使用HDR 环境纹理反射,使用后渲染的虚拟元素更真实 |
| environment Texturing | 环境纹理来源,可设置为自动(automatic)、手动(manual)、无(none)三者之一,当设置为手动时,需要提供环境纹理图 |
通常,在实现人体肢体检测和人形遮挡功能时,还需要设置 frameSemantics 语义属性,使用ARBoodyTrackingConfiguration 配置类进行人体肢体检测和动作捕捉时,frameSemantics 语义属性值只能设置 bodyDetection(默认值)。frameSemantics 语义属性中 bodyDetection 用于肢体检测跟踪,后两个用于人形遮挡,personSegmentation现实屏幕空间的人形分离,而 personSegmentation WithDepth则是带有深度信息的人形分离。
2D人体姿态估计
在ARKit 中,2D人体姿态估计是指对摄像头采集的视频图像中人像在屏幕空间中的姿态进行估计,通常使用人体骨骼关节点来描述人体姿态。近年来,随着深度学习技术的发展,人体骨骼关节点检测效率与效果不断提升,已经开始广泛应用于计算机视觉的相关领域。2D人体姿态检测估计在视频安防、动作分类、行为检测、人机交互、体育科学中有着广阔的应用前景。
人体骨骼关节点检测
人体骨骼关节点检测(Pose Estimation)主要检测人体的一些关键节点,如关节、头部、手掌等,通过关节点描述人体骨骼及姿态信息。人体骨骼关节点检测在计算机视觉人体姿态检测相关领域的研究中起到了基础性的作用,是智能视频监控、病人监护系统、人机交互、虚拟现实、智能家居、智能安防、运动员辅助训练等应用的基础性算法。
在实际应用中,由于人体具有相当的柔性,会出现各种姿态和形状,人体任何一个部位的微小变化都会产生一种新的姿态,同时其关节点的可见性受穿着、姿态、视角等影响非常大,而且还受到光照、遮挡等环境影响。除此之外,2D人体关节点和3D人体关节点在视觉上会有明显的差异,身体不同部位都会有视觉上的缩短效应(Fore Shortening),使得人体骨骼关节点检测成为计算机视觉领域中一个极具挑战性的课题。
使用2D 人体姿态估计
在 ARKit 中,我们不必关心底层的人体骨骼关节点检测算法,也不必自己去调用这些算法,在运行使用ARBodyTrackingConfiguration 配置的ARSession 之后,基于摄像头图像的2D人体姿态估计任务就会启动。
2D人体姿态检测基于屏幕空间,获取的人体姿态信息没有深度值。在 ARKit 检测到屏幕空间中的人形后,可以通过 ARFrame. detectedBody 获取一个 ARBody2D 对象,也就是说 ARKit 目前对屏幕空间中的2D人体,只支持单个人形检测。ARBody2D对象描述了检测到的人形结构信息,其结构如下图所示。
通过图可以看到,在使用 session(_ session: ARSession,didUpdate frame: ARFrame)方法获取ARFrame 中表示 2D人体的 ARBody2D 对象后,就可以使用其 skeleton. jointLandmarks 获取所有关节点位置信息,也可以通过其 skeleton. definition.jointNames 获取所有关节点名称。jointLandmarks 是一个包含所有关节点位置信息的数组,我们可以通过索引值检索某个关节点的位置,也可以通过 skeleton.landmark(for: ARSkeleton. JointName(rawValue: jointName))方法取指定关节点名称的位置信息。
2D人体姿态估计是在屏幕空间中对摄像头采集的图像进行逐帧分析,解算出的关节点位置也是在屏幕空间中的归一化坐标,以屏幕左上角(0,0)右下角为(1,1),如下图所示。
为了描述人体骨骼关节点,ARKit 新建了一个 ARSkeleton 类,该类包含一个人体关节点(Joint)集合及关节之间关系的定义,该类预定义了8个关节点,分别是 head、 leftFoot、 left Hand、 leftShoulder.hannnt riahtHand.rightShoulder、root.这是在应用开发中使用最多的关节点,2D和3D人体肢体都包含这些关节点,因此我们可以通过这些预定义的节点名字快速找到骨骼节点位置。ARSkeleton 类是ARSkeleton2D 和 ARSkeleton3D类的父类。
ARSkeleton2D 类继承自 ARSkeleton,其 jointLandmarks 包含了所有2D关节点的位置信息,也可以通过该类的landmark(forJointNamed:)方法获取某个名字关节点的位置,此方法需要传递关节点的原始名称(rawValue)而不是 ARSkeleton 预定义的关节点名(预定义关节点名可以通过其.rawValue 获取原始名称)。jointLandmarks 是 simd_float2 类型数组,因此我们也可以直接通过下标获取特定的关节点位置信息,下标方法取值比使用 landmark(forJointNamed:)快得多,特别是对每帧都要执行的循环操作,可以节省很多时间。获取特定节点名称的索引值可以通过 definition.index(for:)方法实现。除此之外,还可以通过 ARSkeleton2D的isJointTracked(_:)方法查询每一个关节点在当前帧的检测跟踪情况,还可以获取每一个节点的父节点。
|--------------------------|----|------------------------|----|
| 骨骼关节点名称 | 索引 | 父节点名称 | 索引 |
| invalid | -1 | 无 | |
| head_ joint | 0 | neck_1 joint | 1 |
| neck_ 1_joint | 1 | root | 16 |
| right_ shoulder_1 joint | 2 | neck_1_joint | 1 |
| right forearm_joint | 3 | right_shoulder_1_joint | 2 |
| right_hand joint | 4 | right forearm_joint | 3 |
| left_shoulder_1_joint | 5 | neck_1 joint | 1 |
| left_forearm_joint | 6 | | 5 |
| left_hand joint | 7 | left forearm_joint | 6 |
| right_upLeg_joint | 8 | root | 16 |
| right_leg joint | 9 | right_upLeg_joint | 8 |
| right_foot joint | 10 | right_leg joint | 9 |
| left_ upLeg_joint | 11 | root | 16 |
| left_leg joint | 12 | left_upLeg_joint | 11 |
| left_foot joint | 13 | left_leg joint | 12 |
| right_eye joint | 14 | head _joint | 0 |
| left_eye_joint | 15 | head_joint | 0 |
| root | 16 | Invalid | -1 |
| right_ear_joint | 17 | right_eye joint | 14 |
| left-ear- joint | 18 | left_eye_joint | 15 |
ARKit 2D 人体骨骼关节点定义及它们之间的关联关系如上表所示,通过表可以看到,在ARKit 中,检测到的2D人体共包含19个关节点(root 节点代表了整个 ARBody2D 对象,不计算在内时包含18个关节点),这些关节点相互之间有很强的相关性,存在紧密的父子连接关系,通过节点之间的相互关系,就可以画出各骨骼节点之间的连结图。
下面演示利用 ARKit 检测到的2D人体骨骼关节点信息,将每一个关节点用一个圆圈标示出来,具体代码如下
Swift
//
// BodyTrackingView.swift
// ARKitDeamo
//
// Created by zhaoquan du on 2024/2/1.
//
import SwiftUI
import ARKit
import RealityKit
struct BodyTrackingView: View {
var body: some View {
BodyTrackingViewContainer().edgesIgnoringSafeArea(.all).navigationTitle("人体骨架2D检测")
}
}
struct BodyTrackingViewContainer:UIViewRepresentable {
func makeUIView(context: Context) ->ARView {
let arView = ARView(frame: .zero)
return arView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
guard ARBodyTrackingConfiguration.isSupported else {
return
}
context.coordinator.arView = uiView
let config = ARBodyTrackingConfiguration()
config.frameSemantics = .bodyDetection
config.automaticSkeletonScaleEstimationEnabled = true
uiView.session.delegate = context.coordinator
uiView.session.run(config)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject,ARSessionDelegate {
var arView : ARView? = nil
let circleWidth: CGFloat = 10
let circleHeight: CGFloat = 10
var isPrinted = false
func session(_ session: ARSession, didUpdate frame: ARFrame) {
guard let arView = arView else {
return
}
//清除骨骼圆圈
arView.layer.sublayers?.compactMap({
$0 as? CAShapeLayer
}).forEach({
$0.removeFromSuperlayer()
})
guard let detectedBody = frame.detectedBody else {
return
}
guard let orientation = arView.window?.windowScene?.interfaceOrientation else {
return
}
let transform = frame.displayTransform(for: orientation, viewportSize: arView.frame.size)
detectedBody.skeleton.jointLandmarks.forEach { landmark in
let normalizeCenter = CGPoint(x: CGFloat(landmark.x), y: CGFloat(landmark.y)).applying(transform)
let center = normalizeCenter.applying(.identity.scaledBy(x: arView.frame.width, y: arView.frame.height))
let rect = CGRect(x: center.x - circleWidth/2, y: center.y - circleWidth/2, width: circleWidth, height: circleHeight)
let circleLayer = CAShapeLayer()
circleLayer.path = UIBezierPath(ovalIn: rect).cgPath
arView.layer.addSublayer(circleLayer)
}
if !isPrinted {
let jointNames = detectedBody.skeleton.definition.jointNames
for name in jointNames {
let landmark = detectedBody.skeleton.landmark(for: ARSkeleton.JointName(rawValue: name))
let index = detectedBody.skeleton.definition.index(for: ARSkeleton.JointName(rawValue: name))
print("\(name),\(String(describing: landmark)),the index is \(index) parent index is \(detectedBody.skeleton.definition.parentIndices[index])")
}
print("last: \(ARSkeleton2D.JointName.rightShoulder.rawValue)")
isPrinted = true
}
}
}
}
代码中实现的主要功能是在每一个检测到的2D人体关节点位置画一个圆圈,效果如图所示。
代码很多语句都是执行画图操作,但也演示了 ARKit 2D 人体检测使用的几个重要功能:
(1)演示了如何获取屏幕空间中的 ARBody2D对象,为确保代码在没有检测到2D人体时也能正确执行,我们使用了 guard 语句。
(2)演示了如何获取2D 人体所有骨骼关节点名字集合,以及各关节点索引及其父节点索引。
(3)演示了如何利用关节点名字获取该关节点在屏幕空间中的位置信息。
如前所述,使用索引值获取特定的关节点位置信息比使用关节点名字快得多,代码清演示了利用关节点名字获取对应索引值,在实际开发中,可以直接使用前表中各关节点的索引值以提高性能。