【iOS ARKit】同时开启前后摄像头BlendShapes

      在上一节中已经了解了 iOS ARkit 进行BlendShapes的基本操作,这一小节继续实践同时开启前后摄像头进行人脸捕捉和世界追踪。

      iOS设备配备了前后两个摄像头,在运行AR 应用时,需要选择使用哪个摄像头作为图像输人。最常见的AR 体验使用设备后置摄像头进行世界跟踪、虚实融合,通常使用 ARWorldTrackingConfiguration 配置跟踪使用者的真实环境。除了进行虚实融合,我们通常还利用后置摄像头采集的图像信息评估真实世界中的光照情况、对真实环境中的2D图像或者3D物体进行检测等。

       对具备前置深度相机(TrueDepth Camera)或者A12及以上处理器的设备,使用 ARFaceTrackingConfiguration配置可以实时进行人脸检测跟踪,实现人脸姿态和表情的捕捉。拥有前置深度相机或 A12及以上处理器硬件的iPhone/iPad,在运行iOS 13及以上系统时,还可以同时开启设备前后摄像头,即同时进行人脸检测和世界跟踪。这是一项非常有意义且实用的功能,意味着使用者可以使用表情控制场景中的虚拟物体,实现除手势与语音之外的另一种交互方式。

      在 RealityKit 中,同时开启前后摄像头需要使用 ARFaceTrackingConfiguration 配置或者ARWorldTrackingConfiguration 配置之一。使用 ARFaceTracking Configuration 配置时将其 supportsWorldTracking属性设置为 true,使用 ARWorldTrackingConfiguration 配置时将其 userFaceTrackingEnabled 属性设置为true 都可以在支持人脸检测的设备上同时开启前后摄像头。

     同时开启前后摄像头后,RealityKit 会使用后置摄像头跟踪现实世界,同时也会通过前置摄像头实时检测人脸信息,包括人脸表情信息。

     需要注意的是,并不是所有设备都支持同时开启前后摄像头,只有符合前文所描述的设备才支持该功能,因此,在使用之前也应当对该功能的支持情况进行检查。在不支持同时开启前后摄像头的设备上应当执行另外的策略,如提示用户进行只使用单个摄像头的操作。

     在下面的演示中,我们会利用后置摄像头的平面检测功能,在检测到的水平平面上放置机器头像模型,然后利用从前置摄像头中捕获的人脸表情信息驱动头像模型。核心代码如代码如下所示。

//
//  BlendShapeRobot.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/1/25.
//

import SwiftUI
import ARKit
import RealityKit

struct BlendShapeRobot: View {
    var body: some View {
        BlendShapeRobotContainer().edgesIgnoringSafeArea(.all)
    }
}

struct BlendShapeRobotContainer :UIViewRepresentable{
    
    
    func makeUIView(context: Context) -> ARView {
        let arView = ARView(frame: .zero)
        return arView
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
        guard ARFaceTrackingConfiguration.isSupported else {
            return
        }
        let config = ARWorldTrackingConfiguration()
        config.userFaceTrackingEnabled = true
        config.isLightEstimationEnabled = true
        config.worldAlignment = .gravity
        
        config.planeDetection = .horizontal
        
        uiView.session.delegate = context.coordinator
        uiView.automaticallyConfigureSession = false
        uiView.session.run(config, options: [])
        let planeAnchor = AnchorEntity(plane:.horizontal)
        planeAnchor.addChild(context.coordinator.robotHead)
        uiView.scene.addAnchor(planeAnchor)
        
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator: NSObject, ARSessionDelegate{
        var robotHead = RobotHead()
        
        func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
            for anchor in anchors {
                guard  let anchor = anchor as? ARFaceAnchor else {
                    continue
                }
                robotHead.update(with: anchor)
            }
        }
        
       
    }
    
}

       在代码中,我们首先对设备支持情况进行检查,在确保设备支持同时开启前后摄像头功能时使用 ARWorldTrackingConfiguration 配置并运行 AR进程,然后在检测到平面时将机器头像模型放置于平面上,最后利用 session(didUpdate frame:) 代理方法使用实时捕获到的人脸表情数据更新机器头像模型,从而达到了使用人脸表情驱动场景中模型的目的。需要注意的是代码中 userFaceTrackingEnabled 必须设置为true,并且开启平面检测功能,另外,为更好地组织代码,我们将与模型及表情驱动相关的代码放到了RobotHead类中。RobotHead类用于管理机器头像模型加载及使用表情数据驱动模型的工作,关键代码如下所示。

//
//  RobotHead.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/1/25.
//

import RealityKit
import ARKit

class RobotHead: Entity, HasModel {
    
    // Default color values
    private let eyeColor: SimpleMaterial.Color = .blue
    private let eyebrowColor: SimpleMaterial.Color = .brown
    private let headColor: SimpleMaterial.Color = .green
    private let lipColor: SimpleMaterial.Color = .lightGray
    private let mouthColor: SimpleMaterial.Color = .gray
    private let tongueColor: SimpleMaterial.Color = .red
    private let clearColor: SimpleMaterial.Color = .clear
    
    private var originalJawY: Float = 0
    private var originalUpperLipY: Float = 0
    private var originalEyebrowY: Float = 0
    
    private lazy var eyeLeftEntity = findEntity(named: "eyeLeft")!
    private lazy var eyeRightEntity = findEntity(named: "eyeRight")!
    private lazy var eyebrowLeftEntity = findEntity(named: "eyebrowLeft")!
    private lazy var eyebrowRightEntity = findEntity(named: "eyebrowRight")!
    private lazy var jawEntity = findEntity(named: "jaw")!
    private lazy var upperLipEntity = findEntity(named: "upperLip")!
    private lazy var headEntity = findEntity(named: "head")!
    private lazy var tongueEntity = findEntity(named: "tongue")!
    private lazy var mouthEntity = findEntity(named: "mouth")!
    
    private lazy var jawHeight: Float = {
        let bounds = jawEntity.visualBounds(relativeTo: jawEntity)
        return (bounds.max.y - bounds.min.y)
    }()
    
    private lazy var height: Float = {
        let bounds = headEntity.visualBounds(relativeTo: nil)
        return (bounds.max.y - bounds.min.y)
    }()
    
    required init() {
        super.init()
        
        if let robotHead = try? Entity.load(named: "robotHead") {
            
            robotHead.position.y += 0.05
            addChild(robotHead)
        } else {
            fatalError("无法加载模型.")
        }
        originalJawY = jawEntity.position.y
        originalUpperLipY = upperLipEntity.position.y
        originalEyebrowY = eyebrowLeftEntity.position.y
        setColor()
    }
    
    
    func setColor(){
        
        headEntity.color = headColor
        eyeLeftEntity.color = eyeColor
        eyeRightEntity.color = eyeColor
        eyebrowLeftEntity.color = eyebrowColor
        eyebrowRightEntity.color = eyebrowColor
        upperLipEntity.color = lipColor
        jawEntity.color = lipColor
        mouthEntity.color = mouthColor
        tongueEntity.color = tongueColor
    }
    
    // MARK: - Animations
    
    /// - Tag: InterpretBlendShapes
    func update(with faceAnchor: ARFaceAnchor) {
        // Update eyes and jaw transforms based on blend shapes.
        let blendShapes = faceAnchor.blendShapes
        guard let eyeBlinkLeft = blendShapes[.eyeBlinkLeft] as? Float,
            let eyeBlinkRight = blendShapes[.eyeBlinkRight] as? Float,
            let eyeBrowLeft = blendShapes[.browOuterUpLeft] as? Float,
            let eyeBrowRight = blendShapes[.browOuterUpRight] as? Float,
            let jawOpen = blendShapes[.jawOpen] as? Float,
            let upperLip = blendShapes[.mouthUpperUpLeft] as? Float,
            let tongueOut = blendShapes[.tongueOut] as? Float
            else { return }
        
        eyebrowLeftEntity.position.y = originalEyebrowY + 0.03 * eyeBrowLeft
        eyebrowRightEntity.position.y = originalEyebrowY + 0.03 * eyeBrowRight
        tongueEntity.position.z = 0.1 * tongueOut
        jawEntity.position.y = originalJawY - jawHeight * jawOpen
        upperLipEntity.position.y = originalUpperLipY + 0.05 * upperLip
        eyeLeftEntity.scale.z = 1 - eyeBlinkLeft
        eyeRightEntity.scale.z = 1 - eyeBlinkRight
        
        let cameraTransform = self.parent?.transformMatrix(relativeTo: nil)
        let faceTransformFromCamera = simd_mul(simd_inverse(cameraTransform!), faceAnchor.transform)
        let rotationEulers = faceTransformFromCamera.eulerAngles
        let mirroredRotation = Transform(pitch: rotationEulers.x, yaw: -rotationEulers.y + .pi, roll: rotationEulers.z)
        self.orientation = mirroredRotation.rotation
    }
}


extension Entity {
    var color: SimpleMaterial.Color? {
        get {
            if let model = components[ModelComponent.self] as? ModelComponent,
               let color = (model.materials.first as? SimpleMaterial)?.color.tint {
                return color
            }
            return nil
        }
        set {
            if var model = components[ModelComponent.self] as? ModelComponent {
                if let color = newValue {
                    model.materials = [SimpleMaterial(color: color, isMetallic: false)]
                } else {
                    model.materials = []
                }
                components[ModelComponent.self] = model
            }
        }
    }
}

extension simd_float4x4 {
    // Note to ourselves: This is the implementation from AREulerAnglesFromMatrix.
    // Ideally, this would be RealityKit API when this sample gets published.
    var eulerAngles: SIMD3<Float> {
        var angles: SIMD3<Float> = .zero
        
        if columns.2.y >= 1.0 - .ulpOfOne * 10 {
            angles.x = -.pi / 2
            angles.y = 0
            angles.z = atan2(-columns.0.z, -columns.1.z)
        } else if columns.2.y <= -1.0 + .ulpOfOne * 10 {
            angles.x = -.pi / 2
            angles.y = 0
            angles.z = atan2(columns.0.z, columns.1.z)
        } else {
            angles.x = asin(-columns.2.y)
            angles.y = atan2(columns.2.x, columns.2.z)
            angles.z = atan2(columns.0.y, columns.1.y)
        }
        
        return angles
    }
}

     在代码中,我们首先从 ARFaceAnchor 中获取 BendShapes 表情运动因子集合,并从中取出感兴趣的运动因子,然后利用这些表情因子对机器头像模型中的子实体对象相关属性进行调整,最后处理了人脸与模型旋转关系的对应问题。

    在支持同时开启前置与后置摄像头的设备上编译运行,当移动设备在检测到的水平平面时放置好机器头像模型,将前置摄像头对准人脸,可以使用人脸表情驱动机器头像模型,当人体头部旋转时,机器头像模理也会相应地进行旋转,实现效果如图 所示。

   以上演示的是一个简单的实例,完整实现了利用前置摄像头采集的人脸表情信息控制后置摄像头模型的功能。在使用前置摄像头时,后置摄像头可以进行世界追踪。 由于Realiy Kit 目前沒有控制网格变形的函数,要实现利用人脸表情控制驱动模型的功能,需要手动进行人脸表情与模型状态变化的绑定,人工计算模型中各因子对应的位置与方问,这是一个比较容易出错的过程。经过测试发现,ARKit 对人脸表情的捕捉还是比较准确的,在使用配备深度相机的设备时,捕捉精度较高,可以应付一般应用需求。

具体代码地址:https://github.com/duzhaoquan/ARkitDemo.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/351653.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java实现数字化社区网格管理系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、开发背景四、系统展示五、核心源码5.1 查询企事业单位5.2 查询流动人口5.3 查询精准扶贫5.4 查询案件5.5 查询人口 六、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的数字化社区网格管理系统&#xf…

Istio-gateway

一. gateway 在 Kubernetes 环境中&#xff0c;Kubernetes Ingress用于配置需要在集群外部公开的服务。但是在 Istio 服务网格中&#xff0c;更好的方法是使用新的配置模型&#xff0c;即 Istio Gateway&#xff0c;Gateway 允许将 Istio 流量管理的功能应用于进入集群的流量&…

MongoDB日期存储与查询、@Query、嵌套字段查询实战总结

缘由 MongoDB数据库如下&#xff1a; 如上截图&#xff0c;使用MongoDB客户端工具DataGrip&#xff0c;在filter过滤框输入{ profiles.alias: 逆天子, profiles.channel: }&#xff0c;即可实现昵称和渠道多个嵌套字段过滤查询。 现有业务需求&#xff1a;用Java代码来查询…

【代码随想录15】110.平衡二叉树 257. 二叉树的所有路径 404.左叶子之和

目录 110. 平衡二叉树题目描述参考代码 257. 二叉树的所有路径题目描述参考代码 404.左叶子之和题目描述参考代码 110. 平衡二叉树 题目描述 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a; 一个二叉树…

微信小程序(十四)分包和分包预加载

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.分包的配置 2.分包预加载的写法 先说说为什么需要分包&#xff1a; 小程序追求小而快&#xff0c;主包的大小控制是小程序上线的硬性要求&#xff0c;分包有利于小程序优化加载速度 分包的注意事项&#xff1a…

JVM篇:垃圾回收

如何判断对象可以被回收 Java中对象能否被回收&#xff0c;是根据兑现是否被引用来决定的。如果对象被引用了&#xff0c;说明该对象还在使用&#xff0c;不允许被回收 main栈帧中demo变量存储着Demo实例对象的地址&#xff0c;与Demo实例对象建立了连接关系此时Demo实例对象可…

2024新版68套Axure RP大数据可视化大屏模板及通用组件+PSD源文件

Axure RP数据可视化大屏模板及通用组件库2024新版重新制作了这套新的数据可视化大屏模板及通用组件库V2版。新版本相比于V1版内容更加丰富和全面&#xff0c;但依然秉承“敏捷易用”的制作理念&#xff0c;这套作品也同样延续着我们对细节的完美追求&#xff0c;整个设计制作过…

关于binlog文件恢复数据库的方法

今天给大家讲解下&#xff0c;binlog日志恢复数据库的方法&#xff0c;之前由于数据库中了勒索病毒&#xff0c;这期文章告诉你恢复的方法&#xff1a;下面这种千万不要支付&#xff0c;支付了也不会给恢复 找到binlog文件&#xff1a; 这里我只恢复00032和00033即可&#xff1…

鸿蒙开发初体验

文章目录 前言一、环境配置1.1 安装DevEco Studio1.2 安装相关环境 二、工程创建三、工程结构介绍四、代码实现4.1 初识ArkTs4.2 具体实现 参考资料 前言 HarmonyOS是华为公司推出的一种操作系统&#xff0c;旨在为不同设备提供统一的操作系统和开发平台。鸿蒙开发的出现为用户…

【深度学习】sdxl中的 text_encoder text_encoder_2 区别

镜像问题是&#xff1a;https://editor.csdn.net/md/?articleId135867689 代码仓库&#xff1a; https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/tree/main 截图&#xff1a; 为什么有两个CLIP编码器 text_encoder 和 text_encoder_2 &#xff1f; 在…

照片怎么弄成jpg格式文件?jpg图片格式转换器介绍

jpg图片格式作为最常用的图片格式类型之一&#xff0c;不管是平时下载还是拍摄的照片大多数都属于jpg格式&#xff0c;还有我们在制作证件照照片时&#xff0c;通常需要将照片转换成jpg格式&#xff0c;以便更好地保存、打印或上传至网站等&#xff0c;那么图片转换为jpg需要怎…

day31_HTML

今日内容 0 复习昨日 1 表格标签 2 表单标签【重要】 3 框架标签 0 复习昨日 Javaweb开发,前端,服务器,数据库 前端,要学习HTML,CSS,JavaScript,JQuery HTML是用来编写网页的一种编程语言 语法 由各种标签组成,标签是尖括号<>,一般都是成对儿出现,前面叫做开标签,后面…

SpringBoot自定义全局异常处理器

文章目录 一、介绍二、实现1. 定义全局异常处理器2. 自定义异常类 三、使用四、疑问 一、介绍 Springboot框架提供两个注解帮助我们十分方便实现全局异常处理器以及自定义异常。 ControllerAdvice 或 RestControllerAdvice&#xff08;推荐&#xff09;ExceptionHandler 二、…

学习gin框架知识的注意点

这几天重新学习了一遍gin框架&#xff1a;收获颇多 Gin框架的初始化 有些项目中 初始化gin框架写的是&#xff1a; r : gin.New() r.Use(logger.GinLogger(), logger.GinRecovery(true)) 而不是r : gin.Default() 为什么呢&#xff1f; 点击进入Default源码发现其实他也是…

如何在有或没有备份的 iPhone 上检索已删除的短信

iPhone 清理垃圾短信时不小心删除了一些重要短信&#xff1f;想知道如何找回 iPhone 上已删除的短信吗&#xff1f;如果您已将设备备份到 iCloud 或 iTunes&#xff0c;则可以从备份恢复 iPhone 上的短信。如果没有备份&#xff0c;您可以尝试第三方iPhone短信恢复程序来恢复它…

记一个信息泄露到RCE

打点 开局一个登录框 信息收集 发现了一处接口泄露了部分信息 不过只有支付宝密钥的信息无法扩大危害&#xff0c;此时尝试寻找了一下其他同类型系统同样的接口&#xff0c;查看一下是否泄露的信息相同 因为如果相同就说明是静态的&#xff0c;没有价值横向收集 此时访问其他…

RabbitMQ概念

一 、RabbitMQ概念 1 架构图 2 相关概念 Publisher - ⽣产者&#xff1a;发布消息到RabbitMQ中的Exchange Consumer - 消费者&#xff1a;监听RabbitMQ中的Queue中的消息 Broker&#xff1a;接收和分发消息的应用&#xff0c;RabbitMQ Server就是 Message Broker&#xf…

力扣日记1.27-【回溯算法篇】131. 分割回文串

力扣日记&#xff1a;【回溯算法篇】131. 分割回文串 日期&#xff1a;2023.1.27 参考&#xff1a;代码随想录、力扣 131. 分割回文串 题目描述 难度&#xff1a;中等 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可…

Android App开发-简单控件(2)——视图基础

2.2 视图基础 本节介绍视图的几种基本概念及其用法&#xff0c;包括如何设置视图的宽度和高度&#xff0c;如何设置视图的外部间距和内部间距&#xff0c;如何设置视图的外部对齐方式和内部对齐方式等等。 2.2.1 设置视图的宽高 手机屏幕是块长方形区域&#xff0c;较短的那…

Unknown encoder ‘libmp3lame

环境&#xff1a; macos m1 &#xff0c; python3.10.x 背景 做视频切片&#xff0c; 使用moviepy 中VideoFileClip进行截取视频。 报错&#xff1a; Unknown encoder libmp3lameThe audio export failed because FFMPEG didnt find the specified codec for audio encoding …