ARKit中肢体检测捕捉的配置
ARBodyTrackingConfiguration专门用于2D、3D的人体肢体检测捕捉,同时该配置类也可以设置实现2D图像检测和平面检测,构建对现实环境的跟踪。同时ARBodyTrackingConfiguration还支持HDR(High Dynamic Range Imaging,高动态范围成像)环境反射功能。其主要属性如下:
属性名 | 描述 |
---|---|
automaticSkeletonScaleEstimationEnabled | 布尔值,指定ARKit是否进行人体骨骼尺寸评估,设置为true时,ARKit 会根据人体距离摄像机远近调整所驱动的模型大小,使其更匹配 |
isAutoFocusEnabled | 设置是否自动对焦 |
planeDetection | 在进行肢体检测跟踪时是否进行平面检测,设置后就会自动启动平面检 测功能 |
automaticImageScaleEstimationEnabled | 自动评估检测到的2D图像尺寸,这在设置2D图像跟踪时有效 |
detectionImages | 参考图像库 |
maximumNumberOfTrackedImages | 最大可同时跟踪的2D图像数量 |
wantsHDREnvironmentTextures | 是否使用HDR环境纹理反射,使用后渲染的虚拟元素更真实 |
environmentTexturing | 环境纹理来源,可设置为自动(automatic)、手动(maunal)、无(none)三 者之一,当设置为手动时需提供环境纹理图 |
通常在实现人体肢体检测和人形遮挡功能时,还需要设置frameSemantics语义属性,再进行人体肢体检测和动作捕捉时,frameSemantics语义属性值只能设置为bodyDetection(默认值),sceneDepth、smoothedSceneDepth用于人形遮挡,personSegmentation实现屏幕空间的人形分离,而personSegmentationWithDepth则是带有深度信息的人形分离。肢体捕捉和人形分离对计算资源要求很高,只有A13及以上的处理设备才能使用该功能。启动代码如下
guard ARBodyTrackingConfiguration.isSupported else {
fatalError("当前设备不支持人体肢体捕捉")
}
let config = ARBodyTrackingConfiguration()
config.automaticSkeletonScaleEstimationEnabled = true
config.frameSemantics = .bodyDetection
arView.session.delegate = arView
arView.session.run(config)
self.view.addSubview(arView)
2D人体姿态估计
在ARKit中,2D人体姿态评估是指对摄像头采集的视频图像中人像在屏幕中的姿态进行估计,通常使用人体骨骼关节点来描述人体姿态。人体骨骼关键点检测(Pose Estimation)主要检测人体的关键点,如关节、头部、手掌等,通过关键点信息描述人体骨骼及姿态信息,人体骨骼关节点检测在计算机视觉人体姿态检测相关领域的研究中起到了基础性作用。
2D人体姿态检测基于屏幕空间,获取的人体姿态信息没有深度值。在ARKit检测到屏幕空间中的人形后,可以通过ARFrame。detectedBody获取一个ARBody2D对象,也就是说ARKit目前对屏幕空间中的2D人体只支持单个人形检测,ARBody2D对象描述了检测到的人形结构信息,其中包含Skeleton属性以获取关节点坐标。2D人体姿态估计是在平面空间中对摄像头采集的图像进行助阵分析,解算出来的关节点位置也是在屏幕空间中的归一化坐标,以屏幕的左上角为(0, 0),右下角为(1, 1)。
ARSkeleton类包含一个人体关节点(Joint)集合以及个关节点之间关系的定义,该类预定义了8个关节点,依次分别是root、head、leftHand、rightHand、leftFoot、rightFoot、leftShoulder、rightShoulder,我们可以通过这些节点来找出对应骨骼的位置。下面是在关节点处绘制圆点的代码:
let circleWidth: CGFloat = 10
let circleHeight: CGFloat = 10
var isPrinted = false
extension ARView: ARSessionDelegate {
public func session(_ session: ARSession, didUpdate frame: ARFrame) {
// 清除layer上添加的点
ClearCircleLayers()
if let detectedBody = frame.detectedBody {
// 获取设备朝向
guard let interfaceOrientation = self.window?.windowScene?.interfaceOrientation else { return }
//显示变换可用于将捕获图像的图像空间坐标系中的归一化点转换为视图坐标空间中的归一化点。该变换提供了正确的旋转和纵横比填充,用于以给定的方向和大小呈现捕获的图像。
let transform = frame.displayTransform(for: interfaceOrientation, viewportSize: self.frame.size)
// 遍历骨骼关节点位置
detectedBody.skeleton.jointLandmarks.forEach { landmark in
// 获取节点在空间中的位置,在这里将 rect 从视图坐标控件转换为图片空间,仿射变换
let normalizedCenter = CGPoint(x: CGFloat(landmark[0]), y: CGFloat(landmark[1])).applying(transform)
// 返回由现有点的仿射变换产生的点。这里获取关节点对应在view上的位置
let center = normalizedCenter.applying(CGAffineTransform.identity.scaledBy(x: self.frame.width, y: self.frame.height))
print("x:\(normalizedCenter.x)--\(center.x)")
print("y:\(normalizedCenter.y)--\(center.y)")
// 获取即将绘制圆点的中心位置
let rect = CGRect(origin: CGPoint(x: center.x - circleWidth/2, y: center.y - circleHeight/2), size: CGSize(width: circleWidth, height: circleHeight))
// 绘制圆点
let circleLayer = CAShapeLayer()
circleLayer.path = UIBezierPath(ovalIn: rect).cgPath
self.layer.addSublayer(circleLayer)
}
}
}
private func ClearCircleLayers() {
self.layer.sublayers?.compactMap { $0 as? CAShapeLayer }.forEach { $0.removeFromSuperlayer() }
}
}
3D人体姿态估计
在进行3D姿态评估时,ARKit会在检测到人体时直接提供一个AEBodyAnchor类型对象,该对象包含一个ARSkeleton3D类型的人体骨骼类型,通过这个类型可以获取所有检测到的人体骨骼关节点信息。相对于2D检测,在3D检测中多出了一个表示3D人体空间位置信息的Transform(ARBodyAnchor下的Transform)。在使用上两者使用方法完全相同,只是代表3D人体骨骼Skeleton结构比2D更复杂。由于描述人体结构是在三维空间中的层次结构,ARSkeleton3D包含的数组jointModelTransforms和jointLocalTransforms,其中jointLocalTransforms描述的位置信息是某个节点相对其父节点的位置,而jointModelTransforms描述的位置信息是相对检测到的ARBodyAnchor位置。jointModelTransforms和jointLocalTransforms包含的是3D空间中各个关节点的位置信息矩阵。在使用的过程中可以通过localTransform(for jointName: ARSkeleton.JointName)方法获取某个关节点相对其父节点的位置,同样也可以通过modelTransform(for jointName: ARSkeleton.JointName)方法获得某个关节点相对于ARBodyAnchor的位置。
在2D姿态评估中,ARKit使用了17个人体骨骼关节点对姿态信息进行描述,在3D人体姿态估计中共使用了91个骨骼关节点进行描述,并且这91个关节点是以三维的形式分布在3D空间中。定义人体根骨骼的Root节点在尾椎骨处,所有其它骨骼都是以Root为根节点。
在ARKit中,与检测2D图像或者3D图像物体一样,在检测到3D人体后会生成一个ARBodyAnchor用于在现实世界和虚拟空间之间建立关联关系,绑定虚拟元素到检测的人体上。模型制作与骨骼绑定由美术使用3dx Max完成,在绑定骨骼时要按照骨骼关联关系进行绑定,使用bodyAnchor.skeleton.definition.jointNames中的名称进行骨骼命名可以降低绑定的压力。由于人体骨骼需要绑定的关节点比较多,RealityKit会自动检测场景中加载的BodyTrackedEntity实体对象,并尝试自动执行将检测到的人体骨骼关节点与模型骨骼关节点匹配,如果按照bodyAnchor.skeleton.definition.jointNames中的名称进行绑定则无需人工手动绑定,RealityKit会自动进行关节绑定。人体绑定代码如下:
var robotCharacter: BodyTrackedEntity?
// 设置模型和真人之间的位置偏移量
let robotOffset: SIMD3<Float> = [-1.0, 0, 0]
let robotAnchor = AnchorEntity()
extension ARView: ARSessionDelegate {
// 加载机器模型
func loadRobot() {
var cancellable: AnyCancellable? = nil
cancellable = Entity.loadBodyTrackedAsync(named: "robot.usdz").sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
print("无法加载模型,错误:\(error.localizedDescription)")
}
cancellable?.cancel()
}, receiveValue: { (character: Entity) in
if let character = character as? BodyTrackedEntity {
character.scale = [1.0, 1.0, 1.0]
robotCharacter = character
self.scene.addAnchor(robotAnchor)
cancellable?.cancel()
} else {
print("模型格式不正确,不能解析成人体骨骼")
}
})
}
public func session(_ session: ARSession, didUpdate anchors: [ARAnchor]){
for anchor in anchors {
guard let bodyAnchor = anchor as? ARBodyAnchor else { continue }
// 获取所有3D骨骼关节点名称
let skeletonNames = bodyAnchor.skeleton.definition.jointNames
// 获取root节点在世界空间中的姿态
let hipWorldPosition = bodyAnchor.transform
// 获取骨骼Skeleton对象
let skeleton = bodyAnchor.skeleton
// 获取相对于Root节点的所有关节点姿态信息数组
let jointTransforms = skeleton.jointModelTransforms
for (i, jointTransform) in jointTransforms.enumerated() {
// 获取父节点索引
let parentIndex = skeleton.definition.parentIndices[i]
// 检测是否是root节点
guard parentIndex != -1 else {
continue
}
// 获取父节点位置
let parentJointTransform = jointTransforms[parentIndex]
print("父节点位置:\(parentJointTransform)")
}
// 获取真人在空间中的位置
let bodyPosition = simd_make_float3(bodyAnchor.transform.columns.3)
// 设置机器人的位置
robotAnchor.position = bodyPosition + robotOffset
robotAnchor.orientation = Transform(matrix: bodyAnchor.transform).rotation
if let character = robotCharacter, character.parent == nil{
robotAnchor.addChild(character)
}
}
}
}
给检测出的人形眼前添加两个圆球
var leftEye: ModelEntity!
var rightEye: ModelEntity!
var eyeAnchor = AnchorEntity()
extension ARView: ARSessionDelegate {
func CreateSphere(){
let eyeMat = SimpleMaterial(color: .green, isMetallic: true)
leftEye = ModelEntity(mesh: .generateSphere(radius: 0.02), materials: [eyeMat])
rightEye = ModelEntity(mesh: .generateSphere(radius: 0.02), materials: [eyeMat])
eyeAnchor.addChild(leftEye)
eyeAnchor.addChild(rightEye)
self.scene.addAnchor(eyeAnchor)
}
public func session(_ session: ARSession, didUpdate anchors: [ARAnchor]){
for anchor in anchors {
guard let bodyAnchor = anchor as? ARBodyAnchor else { continue }
let bodyPosition = simd_make_float3(bodyAnchor.transform.columns.3)
// 获取左右眼的位置矩阵
guard let leftEyeMatrix = bodyAnchor.skeleton.modelTransform(for: ARSkeleton.JointName(rawValue: "left_eye_joint")),let rightEyeMatrix = bodyAnchor.skeleton.modelTransform(for: ARSkeleton.JointName(rawValue: "right_eye_joint")) else{ return}
let posLeftEye = simd_make_float3(leftEyeMatrix.columns.3)
leftEye.position = posLeftEye
let posRightEye = simd_make_float3(rightEyeMatrix.columns.3)
rightEye.position = posRightEye
eyeAnchor.position = bodyPosition
eyeAnchor.orientation = Transform(matrix: bodyAnchor.transform).rotation
}
}
}
人形遮挡
在AR系统中,计算机通过对设备摄像头采集的图像进行视觉处理和组织,建立起实景空间,然后将生成的虚拟对象依据集合一致性原理嵌入到实景空间中,形成虚拟融合的增强现实环境,再输出到显示系统中呈现给使用者。当虚拟物体叠加到真实场景中时,虚拟物体与真实场景间存在一定的位置关系,即遮挡与被遮挡的关系,但目前在移动AR领域,通过VIO和IMU实现的AR无法获取真实环境的深度信息,所以虚拟物体无法与真实环境进行深度比较,即无法实现遮挡与被遮挡。当在AR中使用了LiDAR传感器后,可以重建场景表面几何,因此就能够正确地实现虚实物体之间的遮挡与被遮挡。
遮挡问题在计算机图形学中就是深度排序问题。为了解决深度排序问题,ARKit借助神经网络技术将人体从背景中分离出来,并将分离出来的人体图像保存到新增加的人体分离缓冲区中,人体分隔缓冲区是一个像素级缓冲区,可以精确地将人体与环境区分开来,从而通过人体分隔缓冲区可以得到精确的人形图像数据。除此之外还需要人体的深度信息,为此ARKit又新增了一个深度估计缓冲区,这个缓冲区用于存储人体的深度信息。至此,ARKit既可以得到人体区域信息,也可以得到人体的深度信息,图形渲染管线就可以正确的视线虚拟物体与人体遮挡。由于神经网络巨大的计算量、实时地深度估算相当困难,为了降低计算的复杂度,只能降低深度分辨率,即在神经网络进行计算计时,人形数据采样分辨率并不是与人体分隔缓冲区分辨率一致,而是降低到实时计算可以处理的程度。在神经网络处理完深度估计之后,还需要将分辨率调整到与人体分隔缓冲区一样的大小,因为这是像素级操作,如果分辨率不一致,就会导致在深度排序时出问题,即深度估计缓冲区与人体分隔缓冲区中人形大小不一致,,在将神经网络处理结果放大到与人体分隔缓冲区分辨率一致时,由于细节的缺失,导致边缘不匹配,表现出来就是边缘闪烁和穿透。为了解决这个问题,需要进行额外的操作,称为磨砂或者适配(Matting),原理就是利用人体分隔缓冲区匹配分辨率小的深度估算结果,使最终的深度估计缓冲区和人体分隔缓冲区达到像素级一致,从而避免边缘穿透问题。
在AR应用中使用人形遮挡需要使用ARWorldTrackingConfiguration配置类,并设置frameSemantics的值为personSegmentation或者personSegmentationWithDepth之一。当使用personSegmentation时,ARKit不会估算检测到人形的深度信息,人形会无条件遮挡虚拟元素而不管元素远近。当使用personSegmentationWithDepth时,ARKit在检测到人体时,不仅会分理处人形,还会计算出人体到摄像机的距离,从而实现人形遮挡。只有在A13以上的处理器才能拿支持人形遮挡功能,因此在使用前要件检查设备是否支持人形遮挡功能。代码如下:
// 配置ARKit代码
guard ARWorldTrackingConfiguration.supportsFrameSemantics(ARConfiguration.FrameSemantics.personSegmentationWithDepth) else {
fatalError("当前设备不支持人形遮挡")
}
arView = ARView()
let config = ARWorldTrackingConfiguration()
// 设置带有深度信息的人形分离
config.frameSemantics = .personSegmentationWithDepth
config.planeDetection = .horizontal
arView.session.delegate = arView
arView.session.run(config)
arView.loadModel()
self.view.addSubview(arView)
// 加载模型代码
extension ARView: ARSessionDelegate {
func loadModel(){
var cancellable: AnyCancellable? = nil
cancellable = Entity.loadModelAsync(named: "fender_stratocaster.usdz").sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
print("无法加载模型,错误:\(error.localizedDescription)")
}
cancellable?.cancel()
}, receiveValue: { entity in
// 将吉他模型加载到检测到的平面中
let planeAnchor = AnchorEntity(plane:.horizontal)
planeAnchor.addChild(entity)
self.scene.addAnchor(planeAnchor)
cancellable?.cancel()
})
}
}
人形提取
为了解决人形分离和深度估计问题,ARKit新增加了Segmentation Buffer(人体分隔缓冲区)和Estimated Depth Data Buffer(深度估计缓冲区)两个缓冲区用于精确地描述人形区域。既然人体分割缓冲区标识出了人形区域,我们就可以利用该缓冲区提取出场景中的人形以便后续应用。代码实现如下:
var arFrame : ARFrame!
extension ARView: ARSessionDelegate {
public func session(_ session: ARSession, didUpdate frame: ARFrame) {
arFrame = frame
}
func catchHuman(){
if let segmentationBuffer = arFrame.segmentationBuffer {
if let uiImage = UIImage(pixelBuffer: segmentationBuffer)?.rotate(radians: .pi / 2) {
UIImageWriteToSavedPhotosAlbum(uiImage, self, #selector(imageSaveHandler(image:didFinishSavingWithError:contextInfo:)), nil)
}
}
}
@objc func imageSaveHandler(image:UIImage,didFinishSavingWithError error:NSError?,contextInfo:AnyObject) {
if error != nil {
print("保存图片出错")
} else {
print("保存图片成功")
}
}
}
extension UIImage {
public convenience init?(pixelBuffer: CVPixelBuffer) {
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
if let cgImage = cgImage {
self.init(cgImage: cgImage)
} else {
return nil
}
}
func rotate(radians: CGFloat) -> UIImage {
let rotatedSize = CGRect(origin: .zero, size: size)
.applying(CGAffineTransform(rotationAngle: CGFloat(radians)))
.integral.size
UIGraphicsBeginImageContext(rotatedSize)
if let context = UIGraphicsGetCurrentContext() {
let origin = CGPoint(x: rotatedSize.width / 2.0,
y: rotatedSize.height / 2.0)
context.translateBy(x: origin.x, y: origin.y)
context.rotate(by: radians)
draw(in: CGRect(x: -origin.y, y: -origin.x,
width: size.width, height: size.height))
let rotatedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return rotatedImage ?? self
}
return self
}
}