ARKit下肢体动作捕捉与人形遮挡

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
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容