【iOS ARKit】ARWorldMap

      ARWorldMap 用于存储 ARSession 检测扫描到的空间信息数据,包括地标(Landmark)、特征点(Feature Point)、平面(Plane)等,以及使用者的操作信息,如使用者添加的 ARAnchor 和开发者自定义的一些信息。ARWorldMap 可以看作 ARSession 运行时的一次状态快照。

      在技术上,每个具备世界跟踪的 ARSession 都会时刻维护一个内部的世界地图(internal world map),ARKit 正是利用这个地图定位跟踪用户设备的姿态,利用 getCurrent WorldMap(completionHandler:)方法获取的 ARWroldMap只是特定时刻内部世界地图的一个快照。    

ARWorldMap 概述

     持久化地存储应用进程数据,ARKit 提供了 ARWorldMap 功能,ARWorldMap 本质是将 AR 场景状态信息转换为可存储可传输的形式(即序列化)保存到文件系统或者数据库中,当使用者再次加载这些景状态信息后即可恢复应用进程。ARWorldMap 不仅保存了应用进程状态信息,还保存了场景特征点云息,在使用者再次加载这些状态数据后,ARKit 可通过保存的特征点云信息与当前用户摄像头获取的特点云信息进行对比匹配从而更新当前用户的坐标,确保两个坐标系的匹配。

存储与加载 ARWorldMap

     存储 ARWorldMap最重要的是从 ARSession 中获取场景的 ARWorldMap 并序列化之,然后保件系统中。加载 ARWorldMap 则首先要从文件系统中获取ARWorldMap 并反序列化之,然后利用这个ARWorldMap 重启 ARSession。存储与加载 ARWorldMap 完整代码如下所示,稍后我们将对代码中所用技术进行详细解析。

//
//  ARWorldMapSaveAndLoad.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/2/20.
//

import SwiftUI
import ARKit
import RealityKit
import Combine

struct ARWorldMapSaveAndLoad: View {
    var viewModel: ViewModel = ViewModel()
    
    var body: some View {
        ARWorldMapSaveAndLoadContainer(viewModel: viewModel)
            .overlay(
                VStack{
                    Spacer()
                    
                    HStack{
                        Button(action: {loadWorldMap()}) {
                            Text("加载信息")
                                .frame(width:150,height:50)
                                .font(.system(size: 17))
                                .foregroundColor(.black)
                                .background(Color.white)
                            
                                .opacity(0.6)
                        }
                        .cornerRadius(10)
                        
                        Button(action: {saveWorldMap()}) {
                            Text("保存信息")
                                .frame(width:150,height:50)
                                .font(.system(size: 17))
                                .foregroundColor(.black)
                                .background(Color.white)
                            
                                .opacity(0.6)
                        }
                        .cornerRadius(10)
                    }
                    Spacer().frame(height: 40)
                }
            ).edgesIgnoringSafeArea(.all).navigationTitle("保存与加载ARWorldMap")
    }
    var mapSaveURL: URL = {
        do {
            return try FileManager.default
                .url(for: .documentDirectory,
                     in: .userDomainMask,
                     appropriateFor: nil,
                     create: true)
                .appendingPathComponent("arworldmap.arexperience")
        } catch {
            fatalError("获取路径出错: \(error.localizedDescription)")
        }
    }()

    func saveWorldMap() {
        print("save:\(String(describing: viewModel.arView))")
        
        self.viewModel.arView?.session.getCurrentWorldMap(completionHandler: { loadWorld, error in
            guard let worldMap = loadWorld else {
                print("当前无法获取ARWorldMap:\(error!.localizedDescription)")
                return
            }
           
            do {
                let data = try NSKeyedArchiver.archivedData(withRootObject: worldMap, requiringSecureCoding: true)
                try data.write(to: mapSaveURL, options: [.atomic])
                print("ARWorldMap保存成功")
            } catch {
                fatalError("无法保存ARWorldMap: \(error.localizedDescription)")
            }
        })
    }
    func loadWorldMap() {
        print("load:\(String(describing: viewModel.arView))")
        guard let data = try? Data(contentsOf: mapSaveURL) else {
            print("load world map faile")
            return
        }
        var worldMap: ARWorldMap?
        do {
            worldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data)
        } catch let error {
            print("ARWorldMap文件格式不正确:\(error)")
        }
        guard let worldMap = worldMap else {
            print("无法解压ARWorldMap")
            return
        }
        let config = ARWorldTrackingConfiguration()
        config.planeDetection = .horizontal
        config.initialWorldMap = worldMap
        
        self.viewModel.arView?.session.run(config,options: [.resetTracking, .removeExistingAnchors])
             
    }
    
    class ViewModel: NSObject,ARSessionDelegate{
        var arView: ARView? = nil
        
      
        var planeEntity : ModelEntity? = nil
        var raycastResult : ARRaycastResult?
        var isPlaced = false
        var robotAnchor: AnchorEntity?
        let robotAnchorName = "drummerRobot"
        var planeAnchor = AnchorEntity()
        
        func createPlane()  {
           
            guard let arView = arView else {
                return
            }
          
            if let an = arView.scene.anchors.first(where: { an in
                an.name == "setModelPlane"
            }){
                arView.scene.anchors.remove(an)
            }
            do {
                let planeMesh = MeshResource.generatePlane(width: 0.15, depth: 0.15)
                var planeMaterial = SimpleMaterial(color: SimpleMaterial.Color.red, isMetallic: false)
                planeMaterial.color =  try SimpleMaterial.BaseColor(tint:UIColor.yellow.withAlphaComponent(0.9999), texture: MaterialParameters.Texture(TextureResource.load(named: "AR_Placement_Indicator")))
                planeEntity = ModelEntity(mesh: planeMesh, materials: [planeMaterial])
                
                planeAnchor = AnchorEntity(plane: .horizontal)
                planeAnchor.addChild(planeEntity!)
                planeAnchor.name = "setModelPlane"
                
                arView.scene.addAnchor(planeAnchor)
            } catch let error {
                print("加载文件失败:\(error)")
            }
        }
        
        func setupGesture(){
            
            let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
            self.arView?.addGestureRecognizer(tap)
        }
        @objc func handleTap(sender: UITapGestureRecognizer){
            sender.isEnabled = false
            sender.removeTarget(nil, action: nil)
            isPlaced = true
            let anchor = ARAnchor(name: robotAnchorName, transform: raycastResult?.worldTransform ?? simd_float4x4())
            self.arView?.session.add(anchor: anchor)
            
            robotAnchor = AnchorEntity(anchor: anchor)
            
            
            do {
                let robot =  try ModelEntity.load(named: "toy_drummer")
                robotAnchor?.addChild(robot)
                robot.scale = [0.01,0.01,0.01]
                self.arView?.scene.addAnchor(robotAnchor!)
                print("Total animation count : \(robot.availableAnimations.count)")
                robot.playAnimation(robot.availableAnimations[0].repeat())
            } catch {
                print("找不到USDZ文件")
            }
            
//            var cancellable: Cancellable?
//            cancellable = ModelEntity.loadModelAsync(named: "toy_drummer.usdz")
//                .sink(receiveCompletion: { error in
//                    print("laod error:\(error)")
//                    cancellable?.cancel()
//                }, receiveValue: {[weak self] model in
//                    guard let robotAnchor = self?.robotAnchor else {
//                        return
//                    }
//                    robotAnchor.addChild(model)
//                    model.scale = [0.01,0.01,0.01]
//                    self?.arView?.scene.addAnchor(robotAnchor)
//                    //用异步方法加载模型开启骨骼动画会crash,不知到是啥原因
//                    //model.playAnimation(model.availableAnimations[0].repeat())
//                    cancellable?.cancel()
//                })
            planeEntity?.removeFromParent()
            planeEntity = nil
        }
        
        func session(_ session: ARSession, didUpdate frame: ARFrame) {
            guard !isPlaced, let arView = arView else{
                return
            }
            //射线检测
            guard let result = arView.raycast(from: arView.center, allowing: .estimatedPlane, alignment: .horizontal).first else {
                return
            }
            raycastResult = result
            planeEntity?.setTransformMatrix(result.worldTransform, relativeTo: nil)
        }
        
        func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            guard !anchors.isEmpty,robotAnchor == nil else {
               
                return
            }
            var panchor: ARAnchor? = nil
            for anchor in anchors {
                if anchor.name == robotAnchorName {
                    panchor = anchor
                    break
                }
            }
            guard let pAnchor = panchor else {
                return
            }
            //放置虚拟元素
            robotAnchor = AnchorEntity(anchor: pAnchor)
            do {
                let robot =  try ModelEntity.load(named: "toy_drummer")
                robotAnchor?.addChild(robot)
                robot.scale = [0.01,0.01,0.01]
                self.arView?.scene.addAnchor(robotAnchor!)
                print("Total animation count : \(robot.availableAnimations.count)")
                robot.playAnimation(robot.availableAnimations[0].repeat())
            } catch {
                print("找不到USDZ文件")
            }
            isPlaced = true
            planeEntity?.removeFromParent()
            planeEntity = nil
            print("加载模型成功")
        }
        
    }
}

struct ARWorldMapSaveAndLoadContainer: UIViewRepresentable {
    var viewModel: ARWorldMapSaveAndLoad.ViewModel
    
    func makeUIView(context: Context) -> some ARView {
        let arView = ARView(frame: .zero)
       
        
        return arView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
        let config = ARWorldTrackingConfiguration()
        config.planeDetection = .horizontal
        uiView.session.run(config)
        
        viewModel.arView = uiView
        uiView.session.delegate = viewModel
        
        viewModel.createPlane()
        viewModel.setupGesture()
        
    }
    
}
#Preview {
    ARWorldMapSaveAndLoad()
}

代码实现的功能如下:

(1)进行平面检测,在检测到可用平面时实例化一个指示图标用于指示放置位置。

(2)添加屏幕单击手势,在平面可用时通过单击屏幕会在指示图标位置放置虛拟机器人模型。

(3) 当用户单击“保存AR信息”按钮时会从当前 ARSesion 中获取 ARWorldMap 并序列化之,然后保存到文件系统中。

(4)当用户单击“加载AR信息”按钮时会从文件系统中加载ARWorldMap 并反序列化之,然后利用该ARWorldMap 重启 ARSession。

        在第(2)项功能中,即 bandleTap()方法中的代码,我们首先将屏幕单击手势禁用,以防止添加多个机器人模型,然后禁止显示指示图标。随后利用命申点的坐标生成了一个ARAnchor,并将其添加到 ARSession中,注意这里设置了 ARAnchor 的名字(name)属性,这步很关键,因为后续我们需要利用该ARAnchor 的名字来恢复虚拟元素。后续代码是使用异步方式加载机器人模型,不赘述。

       在第(3)项功能中,即 saveAR WorldMap()方法中代码,首先使用 getCurrent WorldMap()方法从 ARSession中获取 ARWorldVap,在闭包中,使用 let data = try NSKeyedArchiver. archivedData (with RootObject: map,requiringSecureCoding: true)语句对获取的ARWorldMap 进行序列化,然后使用 try data. write (to:mapSaveURL., options: [.atomic])方法将序列化后的 ARWorldMap 写人到文件系统中。

       在第(4)项功能中,即 loadARWorldMap()方法中代码,首先从文件系统中读取存储的 ARWorldMap文件,并使用 let worldMap = try NSKeyedUnarchiver. unarchivedObject (ofClass: AR WorldMap. self,from:data)语句将其反序列化。在得到反序列化后的ARWorldMap 后,就可以利用其作为配置文件的initialWorldMap 属性重启 ARSession,当用户设备所在的物理环境与 ARWorldMap 保存时的物理环境一致时(即环境特征点信息匹配时),ARKit 就会校正用户设备坐标信息,将当前用户设备的坐标信息与ARWorldMap 中存储的用户设备坐标信息关联起来,并恢复相应的ARAnchor 信息,这时恢复的ARAnchor 姿态与ARWorldMap 中存储的姿态就是一致的,即ARAnchor 在物理环境中的位置与方向是一致的,这就达到了应用进程数据存储与加载的目的。

      正如前文所述,ARWorldMap 并不会存储虚拟元素本身,因此,需要手动恢复虚拟元素,因为虚拟元素总是与ARAnchor 关联,利用 ARAnchor 的名字(name)属性我们就可以恢复关联的虚拟元素。在代码中,session(_:didAdd:)方法就用于恢复关联的虚拟元素,在该方法中,通过ARAnchor.name 进行ARAnchor的对比,如果名字一样且当前没有加载机器人模型则使用异步方法加载之。通过这种方式,我们就可以逐一地恢复所有的虚拟元素,从而恢复整个场景。

       运行案例,在检测到的平面上添加虚拟元素后单击“保存地图”按钮保存 AR WorldMap,稍后单击“加载地图”按钮,或者关闭应用,在重新运行后单击“加载地图”按钮,可以看到虚拟机器人模型会出现在物理世界中的固定位置,如图所示。

      事实上,在将 ARWorldMap 设置 ARWorldTrackingConfiguration. initialWorldMap 属性启动ARSession 时,ARKit 会进入重定位(relocalize)过程,在这个过程中,ARKit 会尝试将当前设备摄像头采集的环境信息与 ARWorldMap 中存储的环境特征信息进行匹配。因此,保持当前设备姿态与存储ARWorldMap 时的设备姿态一致时(即提高环境特征点匹配成功率)可以更快速地重定位。   

具体代码地址:GitHub - duzhaoquan/ARkitDemo

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

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

相关文章

LabVIEW高精度闭式微小型循环泵性能测试

LabVIEW高精度闭式微小型循环泵性能测试 开发了一套基于LabVIEW的高精度闭式微小型循环泵性能测试系统,旨在通过先进的测试技术和虚拟仪器技术,对微小型循环泵的性能进行精确测量和分析,从而优化泵的设计和性能,提高其在航空、机…

助力智能化农田作物除草,基于DETR(DEtection TRansformer)模型开发构建农田作物场景下玉米苗、杂草检测识别分析系统

在我们前面的系列博文中,关于田间作物场景下的作物、杂草检测已经有过相关的开发实践了,结合智能化的设备可以实现只能除草等操作,玉米作物场景下的杂草检测我们则少有涉及,这里本文的主要目的就是想要基于DETR模型来开发构建玉米…

Vue-3

自定义指令 全局注册指令 文件路径:src/main.js import Vue from vue import App from ./App.vue Vue.config.productionTip false// 全局注册指令 Vue.directive(myFocus, {// inserted 会在指令所在的元素,被插入到页面中时触发inserted(el) {el.f…

Ps:颜色模式

Photoshop 中的颜色模式 Color Mode定义了图像中使用的颜色系统,这些模式影响图像的颜色表现、文件大小以及可适用的场景。 Ps菜单:图像/模式 Mode 在不同的颜色模式下,会基于不同的通道并使用不同的方式来混合颜色。 RGB、CMYK、Lab 颜色模…

Centos 7.5 上nginx设置开机自启动

nginx的安装目录 : /usr/local/nginx 一、没有设置开机自启动前,需要执行/usr/local/nginx/sbin/nginx 启动 二、接下来,我们设置开机自启动,就不用手动启动nginx了 1、cd /usr/lib/systemd/system/ 2、vi nginx.service [un…

SpringMVC(1)

目录 SpringMVC简介入门案例启动服务器初始化过程单次请求过程bean加载控制 PostMan请求与响应设置请求映射路径请求参数五种类型参数传递JSON数据日期类型参数传递响应 RestRest 简介RESTful快速开发 SpringMVC是隶属于Spring框架的一部分,主要是用来进行Web开发&a…

vue + koa + 阿里云部署 + 宝塔:宝塔前后端部署

接上篇,我们已经完成了宝塔的基本配置,下面我们来看如何在宝塔中部署前后端 一、上传前后端代码文件 在www > wwwroot目录下创建了一个demo文件,用来存放前后端代码 进入demo中,点击上传 这里前端我用的打完包的 dist文件&am…

【踩坑】修复xrdp无法关闭Authentication Required验证窗口

转载请注明出处:小锋学长生活大爆炸[xfxuezhang.cn] 问题如下,时不时出现,有时还怎么都关不掉,很烦: 解决方法一:命令行输入 dbus-send --typemethod_call --destorg.gnome.Shell /org/gnome/Shell org.gn…

excel 实现分组排序功能

我们经常会遇到按照分组进行排序,在excel如何实现呢? 如下列的数据,需要按照分组,将得分从高到底排名 我们可以使用如下的公式操作即可实现 SUMPRODUCT((A$2:A$15A2)*(C$2:C$15>C2))1

【漏洞复现】大华智慧园区综合管理平台信息泄露漏洞

Nx01 产品简介 大华智慧园区综合管理平台是一款综合管理平台,具备园区运营、资源调配和智能服务等功能。该平台旨在协助优化园区资源分配,满足多元化的管理需求,同时通过提供智能服务,增强使用体验。 Nx02 漏洞描述 大华智慧园区…

腾轩科技传媒分享创建企业百度百科词条前期要点

百度百科是企业的重要名片之一,一个优秀的百度百科词条可以为企业增添无限魅力和影响力,如何创建一篇引人注目的企业百度百科词条呢?接下来,希望大家和腾轩科技传媒一起来学习如何创建企业百度百科词条吧! 1、精心准备…

记录一下 Unity团结引擎开发OpenHarmony Next 应用 环境搭建流程

原视频链接 记录环境搭建过程~,本文是图文版本 一、打开团结引擎官网下载对应的 团结引擎版本 官网地址:https://unity.cn/tuanjie/releases 根据各自的开发环境下载对应的软件版本,我是 windwos 环境,我就下载 windows 环境 …

使用R语言进行多元线性回归分析-多重共线的诊断

一、数据集 序号X1x2x3x4Y序号X1x2x3X4Y12666078.57831224472.51229155274.31954182293.12356850104.3111047426115.92143184787.6111140233483.8155263395.971266912113.311655922109.2111368812109.410771176102.73       1、从中选取主要变量,建立与因变…

理想滤波器、巴特沃斯滤波器、高斯滤波器实现(包含低通与高通,代码实现与分析)

本篇博客聚焦理想滤波器、巴特沃斯滤波器、高斯滤波器进行原理剖析、代码实现和结果总结,代码含有详细注释,希望帮助大家理解。 以下将从理想低通滤波器、理想高通滤波器、巴特沃斯低通滤波器、巴特沃斯高通滤波器、高斯低通滤波器、高斯高通滤波器六个…

B站UP视频播放数据分析之然冉创业说

【背景介绍】 几年前做过类似的分析,但是B站数据加密了,刚好最近在用selenium,就顺手用它爬一下数据。 df pd.read_excel("然冉创业说_13.2万_output.xlsx") df.head() 以上数据在视频播放页面就可以获取到。 【数据分析】 从数…

进程间通信学习笔记(有名管道和无名管道)

进程间通信方式: 无名管道(pipe)有名管道(fifo)信号(signal)共享内存(mmap)套接字(socket) 无名管道: 在内核里面开辟一片内存,进程1和进程2都可以通过这片内存进行通信 无名管道特点: 只能用于具有亲缘关系的进程之间的通信&am…

SocketError | Socket错误码一览表(每一种错误码的故障排查建议)

Socket错误码一览表 文章目录 Socket错误码一览表前言错误码表 前言 在软件开发和网络通信编程中,SocketError算是一个绕不开的坎。它可能因为各种原因而来,比如网络问题、用户搞错了、应用程序出错等等。本文整理一张SocketError排查建议表格就是为了帮…

Python打发无聊时光:8.用kivy库实现滑动控温的空调界面

第一步:装kivy库 在终端输入: pip install kivy 第二步:复制代码 from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.uix.slider import Slider from kivy.uix.label import Label from kivy.uix.togglebutton import T…

多模态表征—CLIP及中文版Chinese-CLIP:理论讲解、代码微调与论文阅读

我之前一直在使用CLIP/Chinese-CLIP,但并未进行过系统的疏导。这次正好可以详细解释一下。相比于CLIP模型,Chinese-CLIP更适合我们的应用和微调,因为原始的CLIP模型只支持英文,对于我们的中文应用来说不够友好。Chinese-CLIP很好地…

linux centos7.9改dns和ip

vi /etc/sysconfig/network-scripts/ifcfg-ens32 :wq后 重启网络服务 systemctl restart network —————————————————————————— 篇外话题 软件下载 xshell可以从腾讯软件中心下载