当前位置: 首页 > news >正文

ARKit读取LiDAR点云

ARKit 是 Apple 强大的增强现实框架,允许开发人员制作专为 iOS 设备设计的沉浸式交互式 AR 体验。

对于配备 LiDAR 的设备,ARKit 充分利用了深度感应功能,大大提高了环境扫描精度。与许多体积庞大且价格昂贵的传统 LIDAR 系统不同,iPhone 的 LiDAR 结构紧凑、经济高效,并可无缝集成到消费设备中,使更广泛的开发人员和应用程序能够使用高级深度感应。

LiDAR 允许创建点云,点云是一组数据点,表示 3D 空间中物体的表面。

在本文的第一部分中,我们将构建一个应用程序,演示如何提取 LiDAR 数据并将其转换为 AR 3D 环境中的单个点。

第二部分将解释如何将从 LiDAR 传感器连续接收的点合并为统一的点云。最后,我们将介绍如何将这些数据导出为广泛使用的 .PLY 文件格式,以便在各种应用程序中进行进一步分析和利用。

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

1、先决条件

我们将使用:

  • Xcode 16 和 Swift 6。
  • SwiftUI 用于应用程序的用户界面
  • Swift Concurrency 用于高效的多线程。

请确保你可以使用配备 LiDAR 传感器的 iPhone 或 iPad 来跟进。

2、设置和创建 UI

创建一个新项目 ProjectCloudExample 并删除我们不会使用的所有不必要的文件,只保留 ProjectCloudExampleApp.swift。

空项目

接下来,让我们创建一个带有参与者的 ARManager.swift 来管理 ARSCNView 并处理相关的 AR 会话。由于 SwiftUI 目前缺乏对 ARSCNView 的原生支持,我们将它与 UIKit 桥接。

在 ARManager 的初始化程序中,我们将其作为 ARSession 的委托,并使用 ARWorldTrackingConfiguration 启动会话。鉴于我们的目标是配备 LiDAR 技术的设备,将 .sceneDepth 属性设置为框架语义至关重要。

import Foundation
import ARKitactor ARManager: NSObject, ARSessionDelegate, ObservableObject {@MainActor let sceneView = ARSCNView()@MainActoroverride init() {super.init()sceneView.session.delegate = self// start sessionlet configuration = ARWorldTrackingConfiguration()configuration.frameSemantics = .sceneDepthsceneView.session.run(configuration)}
}

现在让我们打开主 ProjectCloudExampleApp.swift,创建 ARManager 的一个实例作为状态对象,并将我们的 AR 视图呈现给 SwiftUI。我们将使用 UIViewWrapper 来实现后者。

struct UIViewWrapper<V: UIView>: UIViewRepresentable {let view: UIViewfunc makeUIView(context: Context) -> some UIView { view }func updateUIView(_ uiView: UIViewType, context: Context) { }
}@main
struct PointCloudExampleApp: App {@StateObject var arManager = ARManager()var body: some Scene {WindowGroup {UIViewWrapper(view: arManager.sceneView).ignoresSafeArea()}}
}

3、获取 LiDAR 深度数据

让我们回到 ARManager.swift

AR 会话不断生成包含深度和相机图像数据的帧,可以使用委托函数进行处理。

为了保持实时性能,由于时间限制,处理每一帧是不切实际的。相反,我们会在处理一帧时跳过一些帧。

此外,由于我们的 ARManager 是作为参与者实现的,我们将在单独的线程上处理处理。这可以防止在密集操作期间 UI 出现任何潜在的冻结,从而确保流畅的用户体验。

添加 isProcessing 属性来管理正在进行的帧操作,并添加委托函数来处理传入的帧。实现专门用于帧处理的函数。

还添加 isCapturing 属性,我们稍后将在 UI 中使用它来切换捕获。

actor ARManager: NSObject, ARSessionDelegate, ObservableObject {//...@MainActor private var isProcessing = false@MainActor @Published var isCapturing = false// an ARSessionDelegate function for receiving an ARFrame instancesnonisolated func session(_ session: ARSession, didUpdate frame: ARFrame) {Task { await process(frame: frame) }}// process a frame and skip frames that arrive while processing@MainActorprivate func process(frame: ARFrame) async {guard !isProcessing else { return }isProcessing = true//processing code hereisProcessing = false}//...
}

由于我们的处理函数和 isProcessing 属性是独立的,因此我们无需担心线程之间的任何额外同步。

现在让我们创建一个 PointCloud.swift,其中包含一个用于处理 ARFrame 的参与者。

ARFrame 提供 depthMap、 confidenceMap 和 caughtImage,它们都由 CVPixelBuffer 表示,具有不同的格式:

  • depthMap - Float32 缓冲区
  • confidenceMap - UInt8 缓冲区
  • capturedImage - YCbCr 格式的像素缓冲区

你可以将深度图视为 LiDAR 捕获的照片,其中每个像素都包含从相机到表面的距离(以米为单位)。这与捕获的图像提供的相机反馈一致。我们的目标是从捕获的图像中提取颜色并将其用于深度图中的相应像素。

置信度图与深度图共享相同的分辨率,包含从 [1, 3] 开始的值,表示每个像素深度测量的置信度。

actor PointCloud {func process(frame: ARFrame) async {if 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) {//process buffers}}
}

4、从 CVPixelBuffer 访问像素数据

要从 CVPixelBuffer 中提取像素数据,我们将为每种特定格式创建一个类,例如深度、置信度和颜色图。对于深度和置信度图,我们可以设计一个通用类,因为它们都遵循类似的结构。

4.1 深度和置信度缓冲区

从 CVPixelBuffer 读取的核心概念相对简单:我们需要锁定缓冲区以确保对其数据的独占访问。锁定后,我们可以通过计算要访问的像素的正确偏移量直接读取内存。

Value = Y * bytesPerRow + X
//struct for storing CVPixelBuffer resolution
struct Size {let width: Intlet height: Intvar asFloat: simd_float2 {simd_float2(Float(width), Float(height))}
}final class PixelBuffer<T> {let size: Sizelet bytesPerRow: Intprivate let pixelBuffer: CVPixelBufferprivate let baseAddress: UnsafeMutableRawPointerinit?(pixelBuffer: CVPixelBuffer) {self.pixelBuffer = pixelBuffer// lock the buffer while we are getting its valuesCVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)guard let baseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else {CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)return nil}self.baseAddress = baseAddresssize = .init(width: CVPixelBufferGetWidth(pixelBuffer),height: CVPixelBufferGetHeight(pixelBuffer))bytesPerRow =  CVPixelBufferGetBytesPerRow(pixelBuffer)}// obtain value from pixel buffer in specified coordinatesfunc value(x: Int, y: Int) -> T {// move to the specified address and get the value bounded to our typelet rowPtr = baseAddress.advanced(by: y * bytesPerRow)return rowPtr.assumingMemoryBound(to: T.self)[x]}deinit {CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)}
}

4.2 YCbCr 捕获图像缓冲区

与使用典型的 RGB 缓冲区相比,从 YCbCr 格式的像素缓冲区中提取颜色值需要付出更多努力。YCbCr 颜色空间将亮度 (Y) 与色度 (Cb 和 Cr) 分开,这意味着我们必须将这些组件转换为更熟悉的 RGB 格式。

为了实现这一点,我们首先需要访问像素缓冲区内的 Y 和 Cb/Cr 平面。这些平面保存每个像素的必要数据。一旦我们从各自的平面获得值,我们就可以将它们转换为 RGB 值。转换依赖于一个众所周知的公式,其中 Y、Cb 和 Cr 值通过某些偏移量进行调整,然后乘以特定系数以产生最终的红色、绿色和蓝色值。

final class YCBCRBuffer {let size: Sizeprivate let pixelBuffer: CVPixelBufferprivate let yPlane: UnsafeMutableRawPointerprivate let cbCrPlane: UnsafeMutableRawPointerprivate let ySize: Sizeprivate let cbCrSize: Sizeinit?(pixelBuffer: CVPixelBuffer) {self.pixelBuffer = pixelBufferCVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)guard let yPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0),let cbCrPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else {CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)return nil}self.yPlane = yPlaneself.cbCrPlane = cbCrPlanesize = .init(width: CVPixelBufferGetWidth(pixelBuffer),height: CVPixelBufferGetHeight(pixelBuffer))ySize = .init(width: CVPixelBufferGetWidthOfPlane(pixelBuffer, 0),height: CVPixelBufferGetHeightOfPlane(pixelBuffer, 0))cbCrSize = .init(width: CVPixelBufferGetWidthOfPlane(pixelBuffer, 1),height: CVPixelBufferGetHeightOfPlane(pixelBuffer, 1))}func color(x: Int, y: Int) -> simd_float4 {let yIndex = y * CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) + xlet uvIndex = y / 2 * CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) + x / 2 * 2// Extract the Y, Cb, and Cr valueslet yValue = yPlane.advanced(by: yIndex).assumingMemoryBound(to: UInt8.self).pointeelet cbValue = cbCrPlane.advanced(by: uvIndex).assumingMemoryBound(to: UInt8.self).pointeelet crValue = cbCrPlane.advanced(by: uvIndex + 1).assumingMemoryBound(to: UInt8.self).pointee// Convert YCbCr to RGBlet y = Float(yValue) - 16let cb = Float(cbValue) - 128let cr = Float(crValue) - 128let r = 1.164 * y + 1.596 * crlet g = 1.164 * y - 0.392 * cb - 0.813 * crlet b = 1.164 * y + 2.017 * cb// normalize rgb componentsreturn simd_float4(max(0, min(255, r)) / 255.0,max(0, min(255, g)) / 255.0,max(0, min(255, b)) / 255.0, 1.0)}deinit {CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)}
}

4.3 读取深度和颜色

现在我们已经设置了必要的缓冲区,我们可以返回 PointCloud 参与者中的核心处理功能。下一步是为我们的顶点数据创建一个结构,它将包括每个点的 3D 位置和颜色。

struct Vertex {let position: SCNVector3let color: simd_float4
}

接下来,我们需要遍历深度图中的每个像素,获取相应的置信度值和颜色。

我们将根据最佳置信度和距离筛选点,因为由于深度传感技术的性质,在较远距离捕获的点往往具有较低的准确性。

深度图和捕获的图像具有不同的分辨率。因此,为了正确地将深度数据映射到其相应的颜色,我们需要进行适当的坐标转换。

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 }// iterate through pixels in depth bufferfor row in 0..<depthBuffer.size.height {for col in 0..<depthBuffer.size.width {// get confidence valuelet confidenceRawValue = Int(confidenceBuffer.value(x: col, y: row))guard let confidence = ARConfidenceLevel(rawValue: confidenceRawValue) else {continue}// filter by confidenceif confidence != .high { continue }// get distance value fromlet depth = depthBuffer.value(x: col, y: row)// filter points by distanceif depth > 2 { return }let normalizedCoord = simd_float2(Float(col) / Float(depthBuffer.size.width),Float(row) / Float(depthBuffer.size.height))let imageSize = imageBuffer.size.asFloatlet pixelRow = Int(round(normalizedCoord.y * imageSize.y))let pixelColumn = Int(round(normalizedCoord.x * imageSize.x))let color = imageBuffer.color(x: pixelColumn, y: pixelRow)}}
}

5、将点转换为 3D 场景坐标

我们首先计算所拍摄照片上的点 2D 坐标:

let screenPoint = simd_float3(normalizedCoord * imageSize, 1)

使用相机内在函数,我们将此点转换为具有指定深度值的相机坐标空间中的 3D 点:

let localPoint = simd_inverse(frame.camera.intrinsics) * screenPoint *depth

iPhone 相机与手机本身不对齐,这意味着当你将 iPhone 保持在纵向时,相机会给我们一张实际上具有横向正确方向的图像。此外,为了正确地将点从相机的本地坐标转换为世界坐标,我们需要对 Y 轴和 Z 轴应用翻转变换。

让我们为此制作一个变换矩阵。

func makeRotateToARCameraMatrix(orientation: UIInterfaceOrientation) -> matrix_float4x4 {// Flip Y and Z axes to align with ARKit's camera coordinate systemlet flipYZ = matrix_float4x4([1, 0, 0, 0],[0, -1, 0, 0],[0, 0, -1, 0],[0, 0, 0, 1])// Get rotation angle in radians based on the display orientationlet rotationAngle: Float = switch orientation {case .landscapeLeft: .picase .portrait: .pi / 2case .portraitUpsideDown: -.pi / 2default: 0}// Create a rotation matrix around the Z-axislet quaternion = simd_quaternion(rotationAngle, simd_float3(0, 0, 1))let rotationMatrix = matrix_float4x4(quaternion)// Combine flip and rotation matricesreturn flipYZ * rotationMatrix
}let rotateToARCamera = makeRotateToARCameraMatrix(orientation: .portrait)// the result transformation matrix for converting point from local camera coordinates to the world coordinates
let cameraTransform = frame.camera.viewMatrix(for: .portrait).inverse * rotateToARCamera

最后将局部点与变换矩阵相乘,然后进行归一化,即可得到结果点。

// Converts the local camera space 3D point into world space using the camera's transformation matrix.
let worldPoint = cameraTransform * simd_float4(localPoint, 1)
let resulPosition = (worldPoint / worldPoint.w)

6、结束语

在第一部分中,我们为使用 ARKit 和 LiDAR 创建点云奠定了基础。我们探索了如何从 LiDAR 传感器获取深度数据以及相应的图像,将每个像素转换为 3D 空间中的彩色点。我们还根据置信度过滤点以确保数据准确性。

在第二部分中,我们将研究如何将捕获的点合并为统一的点云,在我们的 AR 视图中将其可视化并导出为 .PLY 文件格式以供进一步使用。


原文链接:ARKit读取LiDAR点云 - BimAnt


http://www.mrgr.cn/news/63098.html

相关文章:

  • HTML 标签全解析:从基础到高级
  • 计算机毕业设计——ssm基于SSM框架的华建汽车出租系统设计与实现演示录像2021
  • 2024年【广东省安全员C证第四批(专职安全生产管理人员)】新版试题及广东省安全员C证第四批(专职安全生产管理人员)复审考试
  • C++音视频04:音视频编码、生成图片
  • 梯度下降算法
  • ElasticSearch - Bucket Script 使用指南
  • C语言数据结构之二叉树(BINARY TREE)链式存贮的简单实现
  • 猫头虎分享:Claude AI、ChatGPT 和 知乎直答的 AI 搜索大战
  • 深入探索C语言:fread函数的高效文件读取艺术
  • 2023-2024年教育教学改革、教学成果奖等项目申请书合集-最新出炉 附下载链接
  • 【如何获取股票数据31】Python、Java等多种主流语言实例演示获取股票行情api接口之沪深A股融资融券标的股数据获取实例演示及接口API说明文档
  • 2024年10月31日Day1
  • 基于Python的自然语言处理系列(50):Soft Prompt 实现
  • IEC104规约的秘密之十九----6号文中的一些问题
  • IDEA修改生成jar包名字的两种方法实现
  • 前端八股文第八篇
  • Vue v-on 简写 @, v-bind 简写 :
  • Vue v-html v-once v-if
  • 定制化视频生成新模范!零样本主体驱动,精确运动控制!复旦阿里等发布DreamVideo-2
  • 消息队列面试——打破沙锅问到底
  • 2024最新IntelliJ IDEA常用的小技巧汇总,JAVA 新手上路必备
  • 【Oracle APEX开发小技巧10】CSS样式控制交互式报表列宽和自动换行效果
  • Nginx 实现动态封禁IP,详细教程来了
  • 详细分析Vue3中的provide和inject基本知识(附Demo)
  • 华为OD机试 - 字符串消除 - 栈Stack(Python/JS/C/C++ 2024 C卷 100分)
  • 【Rust标准库中的convert(AsRef,From,Into,TryFrom,TryInto)】