NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
在探索创建点云的第二部分中,我们将在第一部分建立的基础之上。
捕获并处理单个彩色点后,我们的下一个目标是将这些点合并为统一的点云。之后,我们将在 AR 视图中可视化点云,并最终将其导出到 .PLY 文件。
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)
}
}
}
}
5、最后的想法
在这篇由两部分组成的文章中,我们构建了一个基本的 AR 应用程序,该应用程序能够使用 ARKit 和 LiDAR 在 Swift 中生成和呈现 3D 点云。我们发现了如何提取 LiDAR 数据,将其转换为 3D 空间中的点,并将其合并为单个点云,以及将其导出并共享为 .PLY 文件的能力。
这个应用程序只是一个开始。你可以通过添加更高级的过滤等功能来进一步增强它,允许用户调整点云密度,或者通过根据距离或其他因素替换网格字典中的点来提高云质量。
iPhone LiDAR 的紧凑和经济高效特性使开发人员能够使用高级深度感应,并为创新应用程序开辟了无限可能。
原文链接:ARKit & LiDAR: Building Point Clouds in Swift (part 2)
BimAnt翻译整理,转载请标明出处