百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

ARKit可视化LiDAR点云

connygpt 2024-11-18 10:41 5 浏览

在探索创建点云的第二部分中,我们将在第一部分建立的基础之上。

捕获并处理单个彩色点后,我们的下一个目标是将这些点合并为统一的点云。之后,我们将在 AR 视图中可视化点云,并最终将其导出到 .PLY 文件。

NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模

1、存储顶点

现在,我们从深度图中获得了每个点的颜色和 3D 位置。下一步是将这些点存储在点云中,但简单地附加我们捕获的每个点效率不高,并且可能导致不必要的大数据集。为了有效地处理这个问题,我们需要过滤来自不同深度图的点。

我们将使用基于网格的算法,而不是处理每个点。这种方法涉及将 3D 空间划分为均匀的网格单元或预定义大小的“框”。对于每个网格单元,我们只存储一个代表点,从而有效地对数据进行下采样。通过调整这些框的大小,我们可以控制点云的密度。

这种方法不仅减少了存储的数据量,还使我们能够根据应用程序的要求灵活地微调点云密度——无论我们需要更高的精度来获取详细的数据,还是需要更轻的数据集来加快处理速度。

网格中的过滤点云

让我们定义一个 Vertex、GridKey 和一个相应的字典作为 PointCloud 参与者的一部分。

actor PointCloud {

    //--
    struct GridKey: Hashable {
        
        static let density: Float = 100
    
        private let id: Int
        
        init(_ position: SCNVector3) {
            var hasher = Hasher()
            for component in [position.x, position.y, position.z] {
                hasher.combine(Int(round(component * Self.density)))
            }
            id = hasher.finalize()
        }
    }
    
    struct Vertex {
        let position: SCNVector3
        let color: simd_float4
    }
    
    private(set) var vertices: [GridKey: Vertex] = [:]
    //--

}

GridKey 的设计目的是根据指定的密度对点坐标进行舍入,这样我们就可以将彼此靠近的点分配给同一个键。

我们现在可以完成 process函数了。

func process(frame: ARFrame) async {
    guard let depth = (frame.smoothedSceneDepth ?? frame.sceneDepth),
          let depthBuffer = PixelBuffer<Float32>(pixelBuffer: depth.depthMap),
          let confidenceMap = depth.confidenceMap,
          let confidenceBuffer = PixelBuffer<UInt8>(pixelBuffer: confidenceMap),
          let imageBuffer = YCBCRBuffer(pixelBuffer: frame.capturedImage) else { return }
       
    let rotateToARCamera = makeRotateToARCameraMatrix(orientation: .portrait)
    let cameraTransform = frame.camera.viewMatrix(for: .portrait).inverse * rotateToARCamera
    
    // iterate through pixels in depth buffer
    for row in 0..<depthBuffer.size.height {
        for col in 0..<depthBuffer.size.width {
            // get confidence value
            let confidenceRawValue = Int(confidenceBuffer.value(x: col, y: row))
            guard let confidence = ARConfidenceLevel(rawValue: confidenceRawValue) else {
                continue
            }
                        
            // filter by confidence
            if confidence != .high { continue }
                        
            // get distance value from
            let depth = depthBuffer.value(x: col, y: row)
                        
            // filter points by distance
            if depth > 2 { return }
                        
            let normalizedCoord = simd_float2(Float(col) / Float(depthBuffer.size.width),
                                              Float(row) / Float(depthBuffer.size.height))
                        
            let imageSize = imageBuffer.size.asFloat
            let screenPoint = simd_float3(normalizedCoord * imageSize, 1)

            // Transform the 2D screen point into local 3D camera space
            let localPoint = simd_inverse(frame.camera.intrinsics) * screenPoint * depth
                
            // Converts the local camera space 3D point into world space.
            let worldPoint = cameraTransform * simd_float4(localPoint, 1)
                
            // Normalizes the result.
            let resulPosition = (worldPoint / worldPoint.w)
                
            let pointPosition = SCNVector3(x: resulPosition.x, 
                                           y: resulPosition.y, 
                                           z: resulPosition.z)

            let key = PointCloud.GridKey(pointPosition)
                
            if vertices[key] == nil {
                let pixelRow = Int(round(normalizedCoord.y * imageSize.y))
                let pixelColumn = Int(round(normalizedCoord.x * imageSize.x))
                let color = imageBuffer.color(x: pixelColumn, y: pixelRow)


                vertices[key] = PointCloud.Vertex(position: pointPosition,
                                                  color: color)
            }
        }
    }
}

2、点云可视化

为了在 AR 视图中可视化点云,我们将在每次处理帧时创建一个 SCNGeometry,并使用 SCNNode 显示。这种方法使我们能够实时动态渲染点云。

但是,渲染高密度点云会严重消耗设备的资源,可能会导致性能问题。为了解决这个问题并保持流畅的渲染,我们将实施一个微小的优化,即仅从云中绘制每 10 个点。这种选择性渲染将有助于平衡视觉保真度和性能,确保应用程序保持响应,同时点云仍能传达扫描环境的基本细节。

让我们在 ARManager 中创建一个 geometryNode 并将其附加到初始化程序中的 rootNode。

接下来,我们将添加一个异步 updateGeometry 函数,该函数将顶点从点云转换为 SCNGeometry 并替换 geometryNode 中的几何图形。

最后,我们将此 updateGeometry 函数集成到我们的处理管道中。

actor ARManager: NSObject, ARSessionDelegate, ObservableObject {
    
    //-- 
    @MainActor let geometryNode = SCNNode()
    //-- 
    
    @MainActor
    override init() {
        //--          
        sceneView.scene.rootNode.addChildNode(geometryNode)
    }
    
    @MainActor
    private func process(frame: ARFrame) async {
        guard !isProcessing && isCapturing else { return }
        
        isProcessing = true
        await pointCloud.process(frame: frame)
        await updateGeometry() // <- add here the geometry update
        isProcessing = false
    }

    func updateGeometry() async {
        // make an array of every 10th point
        let vertices = await pointCloud.vertices.values.enumerated().filter { index, _ in
                index % 10 == 9
            }.map { $0.element }
        
        // create a vertex source for geometry
        let vertexSource = SCNGeometrySource(vertices: vertices.map { $0.position } )
        
        // create a color source
        let colorData = Data(bytes: vertices.map { $0.color }, 
                             count: MemoryLayout<simd_float4>.size * vertices.count)

        let colorSource = SCNGeometrySource(data: colorData,
                                            semantic: .color,
                                            vectorCount: vertices.count,
                                            usesFloatComponents: true,
                                            componentsPerVector: 4,
                                            bytesPerComponent: MemoryLayout<Float>.size,
                                            dataOffset: 0,
                                            dataStride: MemoryLayout<SIMD4<Float>>.size)

        // as we don't use proper geometry, we can pass just an array of 
        // indices to our geometry element
        let pointIndices: [UInt32] = Array(0..<UInt32(vertices.count))
        let element = SCNGeometryElement(indices: pointIndices, primitiveType: .point)
        
        // here we can customize the size of the point, rendered in ARView
        element.maximumPointScreenSpaceRadius = 15
        
        let geometry = SCNGeometry(sources: [vertexSource, colorSource], 
                                   elements: [element])
        geometry.firstMaterial?.isDoubleSided = true
        geometry.firstMaterial?.lightingModel = .constant
        
        Task { @MainActor in
            geometryNode.geometry = geometry
        }
    }

3、将点云导出到 .PLY 文件

最后,我们将捕获的点云导出到 .PLY 文件,利用 Transferable 协议在 SwiftUI ShareLink 中实现无缝数据处理。

.PLY 文件格式相对简单,由一个文本文件组成,其中包含一个指定数据内容和结构的标题,后面是顶点列表及其相应的颜色成分。

struct PLYFile: Transferable {
    
    let pointCloud: PointCloud
    
    enum Error: LocalizedError {
        case cannotExport
    }
    
    func export() async throws -> Data {
        let vertices = await pointCloud.vertices
        
        var plyContent = """
        ply
        format ascii 1.0
        element vertex \(vertices.count)
        property float x
        property float y
        property float z
        property uchar red
        property uchar green
        property uchar blue
        property uchar alpha
        end_header
        """
        
        for vertex in vertices.values {
            // Convert position and color
            let x = vertex.position.x
            let y = vertex.position.y
            let z = vertex.position.z
            let r = UInt8(vertex.color.x * 255)
            let g = UInt8(vertex.color.y * 255)
            let b = UInt8(vertex.color.z * 255)
            let a = UInt8(vertex.color.w * 255)
            
            // Append the vertex data
            plyContent += "\n\(x) \(y) \(z) \(r) \(g) \(b) \(a)"
        }
        
        guard let data = plyContent.data(using: .ascii) else {
            throw Error.cannotExport
        }
        return data
    }
    
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .data) {
            try await $0.export()
        }.suggestedFileName("exported.ply")
    }
}

4、完成 UI

现在我们已经构建了应用的核心功能,我们可以完成 UI 了。

UI 将包含一个用于启动和停止点云捕获的按钮,以及一个用于导出和共享生成的点云文件的选项。

在我们的主视图中,我们将创建一个 ZStack,它将覆盖 AR 视图,其中包含用于控制捕获过程和共享结果的按钮。

@main
struct PointCloudExampleApp: App {
    
    @StateObject var arManager = ARManager()
    
    var body: some Scene {
        WindowGroup {
            ZStack(alignment: .bottom) {
                UIViewWrapper(view: arManager.sceneView).ignoresSafeArea()
                
                HStack(spacing: 30) {
                    Button {
                        arManager.isCapturing.toggle()
                    } label: {
                        Image(systemName: arManager.isCapturing ? 
                                          "stop.circle.fill" : 
                                          "play.circle.fill")
                    }
                    
                    ShareLink(item: PLYFile(pointCloud: arManager.pointCloud), 
                                            preview: SharePreview("exported.ply")) {
                        Image(systemName: "square.and.arrow.up.circle.fill")
                    }
                }.foregroundStyle(.black, .white)
                    .font(.system(size: 50))
                    .padding(25)
            }
        }
    }
}

捕捉点云

导出.ply文件预览

5、最后的想法

在这篇由两部分组成的文章中,我们构建了一个基本的 AR 应用程序,该应用程序能够使用 ARKit 和 LiDAR 在 Swift 中生成和呈现 3D 点云。我们发现了如何提取 LiDAR 数据,将其转换为 3D 空间中的点,并将其合并为单个点云,以及将其导出并共享为 .PLY 文件的能力。

这个应用程序只是一个开始。你可以通过添加更高级的过滤等功能来进一步增强它,允许用户调整点云密度,或者通过根据距离或其他因素替换网格字典中的点来提高云质量。

iPhone LiDAR 的紧凑和经济高效特性使开发人员能够使用高级深度感应,并为创新应用程序开辟了无限可能。


原文链接:ARKit可视化LiDAR点云 - BimAnt

相关推荐

自学Python,写一个挨打的游戏代码来初识While循环

自学Python的第11天。旋转~跳跃~,我~闭着眼!学完循环,沐浴着while的光芒,闲来无事和同事一起扯皮,我说:“编程语言好神奇,一个小小的循环,竟然在生活中也可以找到原理和例子”,同事也...

常用的 Python 工具与资源,你知道几个?

最近几年你会发现,越来越多的人开始学习Python,工欲善其事必先利其器,今天纬软小编就跟大家分享一些常用的Python工具与资源,记得收藏哦!不然下次就找不到我了。1、PycharmPychar...

一张思维导图概括Python的基本语法, 一周的学习成果都在里面了

一周总结不知不觉已经自学Python一周的时间了,这一周,从认识Python到安装Python,再到基本语法和基本数据类型,对于小白的我来说无比艰辛的,充满坎坷。最主要的是每天学习时间有限。只...

三日速成python?打工人,小心钱包,别当韭菜

随着人工智能的热度越来越高,许多非计算机专业的同学们也都纷纷投入到学习编程的道路上来。而Python,作为一种相对比较容易上手的语言,也越来越受欢迎。网络上各类网课层出不穷,各式广告令人眼花缭乱。某些...

Python自动化软件测试怎么学?路线和方法都在这里了

Python自动化测试是指使用Python编程语言和相关工具,对软件系统进行自动化测试的过程。学习Python自动化测试需要掌握以下技术:Python编程语言:学习Python自动化测试需要先掌握Py...

Python从放弃到入门:公众号历史文章爬取为例谈快速学习技能

这篇文章不谈江流所专研的营销与运营,而聊一聊技能学习之路,聊一聊Python这门最简单的编程语言该如何学习,我完成的第一个Python项目,将任意公众号的所有历史文章导出成PDF电子书。或许我这个Py...

【黑客必会】python学习计划

阅读Python文档从Python官方网站上下载并阅读Python最新版本的文档(中文版),这是学习Python的最好方式。对于每个新概念和想法,请尝试运行一些代码片段,并检查生成的输出。这将帮助您更...

公布了!2025CDA考试安排

CDA数据分析师报考流程数据分析师是指在不同行业中专门从事行业数据搜集、整理、分析依据数据作出行业研究评估的专业人员CDA证书分为1-3级,中英文双证就业面广,含金量高!!?报考条件:满18...

一文搞懂全排列、组合、子集问题(经典回溯递归)

原创公众号:【bigsai】头条号:程序员bigsai前言Hello,大家好,我是bigsai,longtimenosee!在刷题和面试过程中,我们经常遇到一些排列组合类的问题,而全排列、组合...

「西法带你学算法」一次搞定前缀和

我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。467.环绕字符串中唯一的子字符串[1](中等)795.区...

平均数的5种方法,你用过几种方法?

平均数,看似很简单的东西,其实里面包含着很多学问。今天,分享5种经常会用到的平均数方法。1.算术平均法用到最多的莫过于算术平均法,考试平均分、平均工资等等,都是用到这个。=AVERAGE(B2:B11...

【干货收藏】如何最简单、通俗地理解决策树分类算法?

决策树(Decisiontree)是基于已知各种情况(特征取值)的基础上,通过构建树型决策结构来进行分析的一种方式,是常用的有监督的分类算法。决策树算法是机器学习中的一种经典算法,它通过一系列的规则...

面试必备:回溯算法详解

我们刷leetcode的时候,经常会遇到回溯算法类型题目。回溯算法是五大基本算法之一,一般大厂也喜欢问。今天跟大家一起来学习回溯算法的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~什么是回溯...

「机器学习」决策树——ID3、C4.5、CART(非常详细)

决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。本文将分三篇介绍决策树,第一篇介绍基本树(包括ID3、C4.5、CART),第二篇介绍Ran...

大话AI算法: 决策树

所谓的决策树算法,通俗的说就是建立一个树形的结构,通过这个结构去一层一层的筛选判断问题是否好坏的算法。比如判断一个西瓜是否好瓜,有20条西瓜的样本提供给你,让你根据这20条(通过机器学习)建立起...