美文网首页
07_ARKit 相机:使用场景深度呈现点云

07_ARKit 相机:使用场景深度呈现点云

作者: Zhen斌iOS | 来源:发表于2022-09-27 10:32 被阅读0次

    代码下载

    使用场景的深度数据在现实世界中布点,以呈现物理环境的形状。

    概述

    Depth Cloud 是一款应用程序,它使用 Metal 根据来自设备的 LiDAR 扫描仪的深度信息,通过在物理环境中放置一组点来显示摄像头馈送。 对于会话的周期性深度读数 (depthMap) 中的每个距离样本,应用程序会在物理环境中的该位置放置一个虚拟点,最终结果类似于点云。 Depth Cloud 根据 ARKit 的相机图像 (capturedImage) 为云着色。

    对于深度图中的每个条目——因此,对于云中的每个点——示例应用程序检查相机图像中的相应像素并将像素的颜色分配给点。 当用户直接查看点云时,应用程序的显示看起来几乎与相机馈送相同。 为了演示云的 3D 形状,示例应用程序不断旋转云以改变其相对于用户的视角。

    下图说明了来自单帧数据的点云,在 y 轴上旋转以显示苹果背后的黑暗区域,当前传感器读数缺少颜色和深度信息。


    应用程序循环遍历深度置信度值(参见 confidenceMap),扩大深度缓冲区,并切换 ARKit 的平滑深度选项(ARFrameSemanticSmoothedSceneDepth)。 通过将用户的选择实时应用于点云,用户可以看到设置在整个体验中产生的差异。

    WWDC20 会议 10611:探索 ARKit 4 引用了此示例应用程序的先前版本,它在云中累积点。 对于会话中显示的应用程序的原始版本,从下载根文件夹中的 Git 存储库克隆初始提交。

    有关 ARKit 深度数据的实际应用,请参阅使用场景深度创建雾效果。

    设置相机源

    为了显示相机源,示例项目定义了一个 SwiftUI 场景,其主体包含一个窗口。 为了从窗口代码中抽象出视图代码,示例项目将其所有显示包装在一个名为 MetalDepthView 的视图中。

    @main
    struct PointCloudDepthSample: App {
        var body: some Scene {
            WindowGroup {
                MetalDepthView()
    

    因为 Depth Cloud 使用 Metal 绘制图形,所以示例项目通过定义自定义 GPU 代码来显示相机源。 示例项目在其 ARReceiver.swift 文件中访问 ARKit 的相机源,并将其包装在自定义 ARData 对象中,以便最终传输到 GPU。

    arData.colorImage = frame.capturedImage
    
    确保设备支持并开始会话

    设备需要激光雷达扫描仪才能访问场景的深度。 在深度可视化视图的主体定义中,应用程序通过检查设备是否支持场景深度来防止运行不受支持的配置。

    if !ARWorldTrackingConfiguration.supportsFrameSemantics([.sceneDepth, .smoothedSceneDepth]) {
        Text("Unsupported Device: This app requires the LiDAR Scanner to access the scene's depth.")
    

    为了将数据采集与显示分开,示例应用程序将 ARKit 调用包装在其 ARProvider 类中。

    var arProvider: ARProvider = ARProvider()
    

    AR 提供程序运行世界跟踪配置,并通过配置场景深度帧语义来请求有关场景深度的信息(请参阅 ARFrameSemanticSceneDepth 和 ARFrameSemanticSmoothedSceneDepth)。

    let config = ARWorldTrackingConfiguration()
    config.frameSemantics = [.sceneDepth, .smoothedSceneDepth]
    arSession.run(config)
    
    访问场景的深度

    为了响应配置的场景深度帧语义,框架在会话的 currentFrame 上定义了 sceneDepth 和 smoothedSceneDepth 的帧的 depthMap 属性。

    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        if(frame.sceneDepth != nil) && (frame.smoothedSceneDepth != nil) {
            arData.depthImage = frame.sceneDepth?.depthMap
            arData.depthSmoothImage = frame.smoothedSceneDepth?.depthMap
    

    由于示例项目使用 Metal 绘制图形,因此应用程序的 CPU 代码捆绑了其 GPU 代码显示体验所需的数据。 要使用点云对物理环境进行建模,该应用程序需要相机捕获数据来为每个点着色,并使用深度数据来定位它们。

    示例项目在 GPU 代码中定位每个点,因此 CPU 端将深度数据打包在 Metal 纹理中以供 GPU 使用。

    depthContent.texture = lastArData?.depthImage?.texture(withFormat: .r32Float, planeIndex: 0, addToCache: textureCache!)!
    

    示例项目为 GPU 代码中的每个点着色,因此 CPU 端将相机数据打包以在 GPU 上使用。

    colorYContent.texture = lastArData?.colorImage?.texture(withFormat: .r8Unorm, planeIndex: 0, addToCache: textureCache!)!
    colorCbCrContent.texture = lastArData?.colorImage?.texture(withFormat: .rg8Unorm, planeIndex: 1, addToCache: textureCache!)!
    
    转换相机数据

    在 pointCloudVertexShader 函数(参见示例项目的 shaders.metal 文件)中,示例项目为深度纹理中的每个值创建一个点,并通过对该深度纹理值在相机图像中的位置进行采样来确定该点的颜色。 每个顶点通过将其在一维顶点数组中的位置转换为深度纹理中的二维位置来计算其在相机图像中的 x 和 y 位置。

    uint2 pos;
    // 计算深度-纹理-宽度宽的行以确定 y 值。
    pos.y = vertexID / depthTexture.get_width();
    
    // x 位置是 y 值除法的余数。
    pos.x = vertexID % depthTexture.get_width();
    

    系统的相机捕获管道以 YUV 格式表示数据,示例项目使用亮度图 (colorYtexture) 和蓝色与红色色度图 (colorCbCrtexture) 对其进行建模。 GPU颜色格式为RGBA,需要样例工程转换相机数据才能显示。 着色器在顶点的 x、y 位置对亮度和色度纹理进行采样,并应用静态转换因子。

    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear);
    out.coor = { pos.x / (depthTexture.get_width() - 1.0f), pos.y / (depthTexture.get_height() - 1.0f) };
    half y = colorYtexture.sample(textureSampler, out.coor).r;
    half2 uv = colorCbCrtexture.sample(textureSampler, out.coor).rg - half2(0.5h, 0.5h);
    // 内联将 YUV 转换为 RGB。
    half4 rgbaResult = half4(y + 1.402h * uv.y, y - 0.7141h * uv.y - 0.3441h * uv.x, y + 1.772h * uv.x, 1.0h);
    

    为简洁起见,示例项目演示了 YUV 到 RGB 的内联转换。 要查看将静态转换因子提取到 4 x 4 矩阵的示例,请参阅使用 Metal 显示 AR 体验。

    设置点云视图

    为了使用点云显示相机源,该项目定义了一个 UIViewRepresentable 对象 MetalPointCloud,其中包含一个显示 Metal 内容的 MTKView。

    struct MetalPointCloud: UIViewRepresentable {
    

    该项目通过将点云视图嵌入到 MetalDepthView 布局中,将其插入到视图层次结构中。

    HStack() {
        Spacer()
        MetalPointCloud(arData: arProvider,
                        confSelection: $selectedConfidence,
                        scaleMovement: $scaleMovement).zoomOnTapModifier(
                            height: geometry.size.width / 2 / sizeW * sizeH,
                            width: geometry.size.width / 2, title: "")
    

    作为 UIView 的代表,Metal 纹理视图定义了一个协调器,CoordinatorPointCloud。

    func makeCoordinator() -> CoordinatorPointCloud {
        return CoordinatorPointCloud(arData: arData, confSelection: $confSelection, scaleMovement: $scaleMovement)
    }
    

    点云协调器扩展了 MTKCoordinator,该示例在显示金属内容的其他视图中共享它。

    final class CoordinatorPointCloud: MTKCoordinator {
    

    作为 MTKViewDelegate,MTKCoordinator 处理在整个 Metal 视图生命周期中发生的相关事件。

    class MTKCoordinator: NSObject, MTKViewDelegate {
    

    在 UIView 可表示的 makeUIView 实现中,示例项目将协调器分配为视图的委托。

    func makeUIView(context: UIViewRepresentableContext<MetalPointCloud>) -> MTKView {
        let mtkView = MTKView()
        mtkView.delegate = context.coordinator
    

    在运行时,显示链接然后调用 Metal 协调器的 drawInMTKView: 实现来发出 CPU 端渲染命令。

    override func draw(in view: MTKView) {
        content = arData.depthContent
        let confidence = (arData.isToUpsampleDepth) ? arData.upscaledConfidence:arData.confidenceContent
        guard arData.lastArData != nil else {
    
    使用 GPU 代码显示点云

    点云(英语:point cloud)是空间中的数据集,可以表示三维形状或对象,通常由三维扫描仪获取。

    示例项目在 GPU 上绘制点云。 点云视图将其相应的 GPU 代码作为输入所需的几个纹理打包在一起。

    encoder.setVertexTexture(content.texture, index: 0)
    encoder.setVertexTexture(confidence.texture, index: 1)
    encoder.setVertexTexture(arData.colorYContent.texture, index: 2)
    encoder.setVertexTexture(arData.colorCbCrContent.texture, index: 3)
    

    类似地,点云视图将其相应的 GPU 代码作为输入所需的几个计算属性打包。

    encoder.setVertexBytes(&pmv, length: MemoryLayout<matrix_float4x4>.stride, index: 0)
    encoder.setVertexBytes(&cameraIntrinsics, length: MemoryLayout<matrix_float3x3>.stride, index: 1)
    encoder.setVertexBytes(&confSelection, length: MemoryLayout<Int>.stride, index: 2)
    

    为了调用绘制点云的 GPU 函数,该示例定义了一个管道状态,该状态将其 pointCloudVertexShader 和 pointCloudFragmentShader 金属函数排队(请参阅项目的 shaders.metal 文件)。

    pipelineDescriptor.vertexFunction = library.makeFunction(name: "pointCloudVertexShader")
    pipelineDescriptor.fragmentFunction = library.makeFunction(name: "pointCloudFragmentShader")
    

    在 GPU 上,点云顶点着色器决定了每个点在屏幕上的颜色和位置。 在函数签名中,顶点着色器接收 CPU 代码发送的输入纹理和属性。

    vertex ParticleVertexInOut pointCloudVertexShader(
        uint vertexID [[ vertex_id ]],
        texture2d<float, access::read> depthTexture [[ texture(0) ]],
        texture2d<float, access::read> confTexture [[ texture(1) ]],
        constant float4x4& viewMatrix [[ buffer(0) ]],
        constant float3x3& cameraIntrinsics [[ buffer(1) ]],
        constant int &confFilterMode [[ buffer(2) ]],
        texture2d<half> colorYtexture [[ texture(2) ]],
        texture2d<half> colorCbCrtexture [[ texture(3) ]]
        )
    { // ...
    

    该代码将点的世界位置基于其在相机馈送中的位置和深度。

    float xrw = ((int)pos.x - cameraIntrinsics[2][0]) * depth / cameraIntrinsics[0][0];
    float yrw = ((int)pos.y - cameraIntrinsics[2][1]) * depth / cameraIntrinsics[1][1];
    float4 xyzw = { xrw, yrw, depth, 1.f };
    

    该点的屏幕位置是其世界位置和参数投影矩阵的乘积。

    float4 vecout = viewMatrix * xyzw;
    

    vertex 函数将点的屏幕位置以及点的颜色作为转换后的 RGB 结果输出。

    out.color = rgbaResult;
    out.clipSpacePosition = vecout;
    

    片段着色器在其函数签名中接收顶点函数输出。

    fragment half4 pointCloudFragmentShader(
        ParticleVertexInOut in [[stage_in]])
    

    在过滤任何离设备相机太近的点后,片段着色器通过返回每个顶点的颜色将剩余的点排队以供显示。

    if (in.depth < 1.0f)
        discard_fragment();
    else
    {
        return in.color;
    
    改变云的方向以传达深度

    当用户直接查看点云时,它在视觉上等同于 2D 相机图像。 但是,当示例应用程序稍微旋转点云时,点云的 3D 形状对用户来说变得很明显。


    image.png

    点云的屏幕位置是其投影矩阵的一个因素。 在示例项目的 calcCurrentPMVMatrix 函数(参见 MetalPointCloud.swift)中,该函数设置了一个基本矩阵。

    func calcCurrentPMVMatrix(viewSize: CGSize) -> matrix_float4x4 {
        let projection: matrix_float4x4 = makePerspectiveMatrixProjection(fovyRadians: Float.pi / 2.0,
                                                                          aspect: Float(viewSize.width) / Float(viewSize.height),
                                                                          nearZ: 10.0, farZ: 8000.0)
    

    为了调整点云相对于用户的方向,示例应用程序相反地为相机的姿势设置了平移和旋转偏移。

    // 随机化相机比例。
    translationCamera.columns.3 = [150 * sinf, -150 * cossqr, -150 * scaleMovement * sinsqr, 1]
    // 随机化相机移动。
    cameraRotation = simd_quatf(angle: staticAngle, axis: SIMD3(x: -sinsqr / 3, y: -cossqr / 3, z: 0))
    

    示例项目在返回调整后的结果之前将相机位姿偏移应用于原始投影矩阵。

    let rotationMatrix: matrix_float4x4 = matrix_float4x4(cameraRotation)
    let pmv = projection * rotationMatrix * translationCamera * translationOrig * orientationOrig
    return pmv
    
    扩大深度缓冲区

    ARKit 的深度图包含相机馈送中对象的精确、低分辨率深度值。 为了创建高分辨率深度图的错觉,示例应用程序提供了使用 Metal Performance Shaders (MPS) 放大深度图的 UI。 通过填充框架深度信息中的空白,扩大的深度缓冲区会在场景中产生更多深度信息的错觉。


    示例项目使用 MPS 扩大深度缓冲区; 查看 ARDataProvider.swift 文件。 ARProvider 类初始化器创建一个引导过滤器来扩大深度缓冲区。

    guidedFilter = MPSImageGuidedFilter(device: metalDevice, kernelDiameter: guidedFilterKernelDiameter)
    

    为了对齐相关视觉效果(相机图像和置信度纹理)的大小,AR 提供者使用 MPS 双线性比例过滤器。

    mpsScaleFilter = MPSImageBilinearScale(device: metalDevice)
    

    在 processLastARData 例程中,AR 提供程序为扩大深度缓冲区的计算通道创建额外的 Metal 命令缓冲区。

    if isToUpsampleDepth {
    

    AR 提供程序将输入深度数据转换为 RGB 格式,根据引导过滤器的要求。

    let convertYUV2RGBFunc = lib.makeFunction(name: "convertYCbCrToRGBA")
    pipelineStateCompute = try metalDevice.makeComputePipelineState(function: convertYUV2RGBFunc!)
    

    在对双线性比例和引导过滤器进行编码后,AR 提供者设置放大的深度缓冲区。

    depthContent.texture = destDepthTexture
    
    显示深度置信度

    除了点云可视化之外,示例应用程序还同时添加了深度距离和置信度可视化。 用户在体验过程中随时参考任一可视化,以更好地掌握激光雷达扫描仪读取物理环境的准确性。


    为了显示深度和置信度可视化,Depth Cloud 定义了一个 UIViewRepresentable 对象 MetalTextureView,它包含一个显示 Metal 内容的 MTKView(参见项目的 MetalTextureView.swift 文件)。 此设置类似于 MetalDepthView,只是示例应用程序将视图的可显示内容存储在单个纹理中。

    encoder.setFragmentTexture(content.texture, index: 0)
    

    该项目通过将深度可视化视图嵌入到项目的 MetalViewSample.swift 文件中的 MetalDepthView 布局中,将其插入到视图层次结构中。

    ScrollView(.horizontal) {
        HStack() {
            MetalTextureViewDepth(content: arProvider.depthContent, confSelection: $selectedConfidence)
                .zoomOnTapModifier(height: sizeH, width: sizeW, title: isToUpsampleDepth ? "Upscaled Depth" : "Depth")
    

    深度可视化视图的内容由包含来自 AR 会话当前帧的深度数据的纹理组成。

    depthContent.texture = lastArData?.depthImage?.texture(withFormat: .r32Float, planeIndex: 0, addToCache: textureCache!)!
    

    深度纹理视图的协调器 CoordinatorDepth 分配一个填充纹理的着色器。

    pipelineDescriptor.fragmentFunction = library.makeFunction(name: "planeFragmentShaderDepth")
    

    planeFragmentShaderDepth 着色器(请参阅 shaders.metal)根据需要将深度值转换为 RGB 以显示它们。

    fragment half4 planeFragmentShaderDepth(ColorInOut in [[stage_in]], texture2d<float, access::sample> textureDepth [[ texture(0) ]])
    {
        constexpr sampler colorSampler(address::clamp_to_edge, filter::nearest);
        float4 s = textureDepth.sample(colorSampler, in.texCoord);
        
        // Size the color gradient to a maximum distance of 2.5 meters.
        // The LiDAR Scanner supports a value no larger than 5.0; the
        // sample app uses a value of 2.5 to better distinguish depth
        // in smaller environments.
        half val = s.r / 2.5h;
        half4 res = getJetColorsFromNormalizedVal(val);
        return res;
    

    同样,该项目通过将置信度可视化视图嵌入到项目的 MetalViewSample.swift 文件中的 MetalDepthView 布局中,将其插入到视图层次结构中。

    MetalTextureViewConfidence(content: arProvider.confidenceContent)
        .zoomOnTapModifier(height: sizeH, width: sizeW, title: "Confidence")
    

    置信度可视化视图的内容由包含来自 AR 会话当前帧的置信度数据的纹理组成。

    confidenceContent.texture = lastArData?.confidenceImage?.texture(withFormat: .r8Unorm, planeIndex: 0, addToCache: textureCache!)!
    

    置信度纹理视图的协调器 CoordinatorConfidence 分配一个填充纹理的着色器。

    pipelineDescriptor.fragmentFunction = library.makeFunction(name: "planeFragmentShaderConfidence")
    

    planeFragmentShaderConfidence 着色器(请参阅 shaders.metal)根据需要将深度值转换为 RGB 以显示它们。

    fragment half4 planeFragmentShaderConfidence(ColorInOut in [[stage_in]], texture2d<float, access::sample> textureIn [[ texture(0) ]])
    {
        constexpr sampler colorSampler(address::clamp_to_edge, filter::nearest);
        float4 s = textureIn.sample(colorSampler, in.texCoord);
        float res = round( 255.0f*(s.r) ) ;
        int resI = int(res);
        half4 color = half4(0.0h, 0.0h, 0.0h, 0.0h);
        if (resI == 0)
            color = half4(1.0h, 0.0h, 0.0h, 1.0h);
        else if (resI == 1)
            color = half4(0.0h, 1.0h, 0.0h, 1.0h);
        else if (resI == 2)
            color = half4(0.0h, 0.0h, 1.0h, 1.0h);
        return color;
    

    相关文章

      网友评论

          本文标题:07_ARKit 相机:使用场景深度呈现点云

          本文链接:https://www.haomeiwen.com/subject/omcnortx.html