ARKit 从零到一 第2部分:平面检测与视觉效果

ARKit 从零到一 第2部分:平面检测与视觉效果_第1张图片
ARKit——检测平面并绘制地板

在第一个 hello world ARKit app 里我们给项目做了初始设置,并在真实世界里渲染了一个 3D 立方体,在用户移动时也能保持追踪。

这篇文章中,我会尝试从真实世界中获取 3D 几何体并给它添加视觉效果。检测几何体对于增强现实 app 来说非常重要,因为要让用户感觉在和真实世界交互,就必须要知道用户是否敲击了桌面,或是正在看向地板,亦或是与其它表面进行像生活中一样的交互。本文会搞定平面检测,后面的文章则会使用这些平面并在真实世界里放置虚拟物体。

ARKit 可以检测水平面(我猜测 ARKit 未来能够检测更复杂的 3D 几何体,但应该要等到深度感应摄像头的发布,也许就是 iPhone 8 吧…)。检测到平面后,我会给它加上视觉效果来显示平面的尺寸和角度。下面的视频展示了实际效果:

视频

注意:本文需要你参考此处的代码:https://github.com/josephchang10/ARCube/tree/part2/ARCube

计算机视觉概念

写代码前,有必要了解一下 ARKit 的背后原理,因为这项技术还不完美,在某些情况下 app 的表现还会受到影响。

增强现实的目标是往真实世界中的特定点插入虚拟内容,并且在真实世界中移动时还能对此虚拟内容保持追踪。ARKit 的基本流程包括从 iOS 设备摄像头中读取视频帧,对每一帧的图片进行处理并获得特征点。特征有很多很多,但我们需要从图片中找出能在多个帧中都被追踪到的特征。特征可能是物体的某个角,或是有纹理的织物的某条边等等。有很多种方式可以生成这些特征,可以在网上了解更多(例如搜索 SIFT)但目前我们只需知道,每张图片里会产生多个唯一标识的特征就足够了。

获得某张图片的特征后,就可以从多个帧中追踪这些特征,随着用户在世界中移动,就可以利用相应的特征点来估算 3D 姿态信息,例如当前摄像头的位置和特征的位置。用户移动地越多,就会获得越多的特征,并优化这些估算的 3D 姿态信息。

至于平面检测,就是在获得一定数量的 3D 特征点后,尝试在这些点中安装一些平面,然后根据尺度、方向和位置找出最匹配的那个。ARKit 会不断分析 3D 特征点并在代码中报告找到的平面。

下面是我的 iPad 看向窗帘时的截图。可以看到织物有良好的纹理,所以追踪到了大量的唯一特征,每个十字都是 ARKit 找到的唯一特征。

ARKit 从零到一 第2部分:平面检测与视觉效果_第2张图片
ARKit 检测特征点——织物窗帘

下一张图是我的桌子,注意并没有多少特征点:

ARKit 从零到一 第2部分:平面检测与视觉效果_第3张图片
ARKit 检测特征点——反光的桌子上特征很少

所以一定要注意 ARKit 需要看向能检测出许多有用特征点的内容。可能检测不出特征点的情况如下:

  1. 光线差——没有足够的光或光线过强的镜面反光。尝试避免这些光线差的环境。
  2. 缺少纹理——如果摄像头指向一面白墙,那也没法获得特征,ARKit 也去无法找到并追踪用户。尝试避免看向纯色、反光表面等地方。
  3. 快速移动——通常情况下检测和估算 3D 姿态只会借助图片,如果摄像头移动太快图片就会糊,从而导致追踪失败。但 ARKit 会利用视觉惯性里程计,综合图片信息和设备运动传感器来估计用户转向的位置。因此 ARKit 在追踪方面非常强大。

在后面的文章里我会测试不同的环境,以便了解追踪的效果。

添加 Debug 的视觉效果

开始前有必要给应用添加一些 debug 信息,比如渲染 ARKit 报告的世界原点以及渲染 ARKit 检测到的特征点,有助于了解当前区域追踪是否良好。所以,为我们的 ARSCNView 实例开启 debug 选项:

sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]

检测平面几何体

如果想在 ARKit 里检测水平面,可以通过设置 session configuration 对象的 planeDetection 属性来指定。这个值可以被设置为 ARPlaneDetectionHorizontal 或 ARPlaneDetectionNone。

设置该属性后,就会开始收到 ARSCNViewDelegate 协议 delegate 方法的回调。这其中有很多方法,首先要使用的是:

/**
有新的 node 被映射到给定的 anchor 时调用。

@param renderer 将会用于渲染 scene 的 renderer。
@param node 映射到 anchor 的 node。
@param anchor 新添加的 anchor。
*/
- (void)renderer:(id )renderer
      didAddNode:(SCNNode *)node
       forAnchor:(ARAnchor *)anchor {
}�

每次 ARKit 自认为检测到了平面时都会调用此方法。其中有两个信息,node 和 anchor。SCNNode 实例是 ARKit 创建的 SceneKit node,它设置了一些属性如 orientation(方向)和 position(位置),然后还有一个 anchor 实例,包含此锚点的更多信息,例如尺寸和平面的中心点。

anchor 实例实际上是 ARPlaneAnchor 类型,从中我们可以得到平面的 extent(范围)和 center(中心点)信息。

渲染平面

有了上述信息,现在可以在虚拟世界里绘制 SceneKit 3D 平面了。创建一个继承自 SCNNode 的 Plane 类。在构造方法中创建平面并相应调整其大小:

// 用 ARPlaneAnchor 实例中的尺寸来创建 3D 平面几何体
planeGeometry = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z))

let planeNode = SCNNode(geometry: planeGeometry)

// 将平面 plane 移动到 ARKit 报告的位置
planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)

// SceneKit 里的平面默认是垂直的,所以需要旋转90度来匹配 ARKit 中的平面
planeNode.transform = SCNMatrix4MakeRotation(Float(Double.pi/2), 1, 0, 0)

// 因为继承自 SCNNode,所以将新的 node 添加给自己
addChildNode(planeNode)

现在有了 Plane 类,回到 ARSCNViewDelegate 的回调方法,每次 ARKit 报告新的 Anchor 时都可以创建新的平面了:

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let anchor = anchor as? ARPlaneAnchor else {
            return
        }
        
        let plane = Plane(withAnchor: anchor)
        
        node.addChildNode(plane)
    } 

注意:实际的代码里为了让视觉效果更好看,我还给 SCNPlane 几何体设置了网格 material,我精简了上面为了代码,这样看起来更简洁。

更新平面

如果运行上面的代码,走来走去看看,可以发现虚拟世界里会渲染新的平面,但是平面不会正确扩大。ARKit 会持续分析场景,如果发现 Plane 比预想的更大/更小,就会更新平面的范围 extent 值。所以需要更新 SceneKit 已渲染的 Plane。

从从一个 ARSCNViewDelegate 方法中获取更新信息:

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        
        // 查看此平面当前是否正在渲染
        guard let plane = planes[anchor.identifier] else {
            return
        }
        
        plane.update(anchor: anchor as! ARPlaneAnchor)
    }

在 Plane 类的 update 方法里,更新 plane 的宽度和高度:

func update(anchor: ARPlaneAnchor) {

        planeGeometry.width = CGFloat(anchor.extent.x);
        planeGeometry.height = CGFloat(anchor.extent.z);
        
        // plane 刚创建时中心点 center 为 0,0,0,node transform 包含了变换参数。
        // plane 更新后变换没变但 center 更新了,所以需要更新 3D 几何体的位置
        position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
    }

现在有了平面渲染和更新,打开 app 看看吧。我给 SCNPlane 几何体添加了科幻风的网格纹理,这里省略了此部分,但你可以在源代码里查看。

获取结果

下面贴了几张文章开头视频的截图,这些是我在房子里走来走去时检测到的平面:

这是楼梯上的灭火器箱,ARKit 正确找出边缘,而且平面的角度也完全正确,符合其高出地板的表面。

ARKit 从零到一 第2部分:平面检测与视觉效果_第4张图片

这是楼梯的地面,可以看到在我移动时 ARKit 也在不断发现新平面,这挺有意思,因为如果你在开发某个 app,用户需要先在空间里转一圈,然后才能放东西,所以应该在几何体成为可用状态时为用户提供良好的视觉提示。

ARKit 从零到一 第2部分:平面检测与视觉效果_第5张图片

下面这张图和上一张图是同一个场景,但几秒之后,ARKit 就把两个平面合并为同一个平面。注意,在 ARSCNViewDelegate 回调里你要处理 ARKit 删除某个 ARPlaneAnchor 实例的情况,也就是说该平面被合并掉了。

ARKit 从零到一 第2部分:平面检测与视觉效果_第6张图片

这儿很有意思,因为我站在这层楼梯的上一层,距离有3米远并且光线不好,但 ARKit 还是找出来这个平面,好厉害!

ARKit 从零到一 第2部分:平面检测与视觉效果_第7张图片

这是在小小的窗台上捕获的平面。注意平面的边缘超出了实际的表面。

ARKit 从零到一 第2部分:平面检测与视觉效果_第8张图片

识别心得

这是我对平面检测的几点心得:

  1. 不要期望平面会完全贴合表面,从视频中可以看到,虽然检测到了平面但角度可能不完全正确,所以如果你在开发的 AR app 需要获得非常精确的几何体来提供更好的效果,你可能会失望。
  2. 边缘检测不是特别好,实际的平面范围有时会太大或大小,所以不要尝试做需要准确边缘的 app
  3. 追踪功能很强,速度也很快。可以看到我在视频中移动时,对真实世界的平面检测相当有效,即使快速移动摄像头,效果也同样很好
  4. 我被特征捕获惊艳到,哪怕光线不足、距离3-5米远,ARKit 仍然能找到那些平面。

示例代码

所有的示例代码都在这里:https://github.com/josephchang10/ARCube/tree/part2/ARCube

接下来

下篇文章中我会用检测到的平面在真实世界中放置 3D 物体,并且尝试对 app 做一些鲁棒化(robustification)改进。

你可能感兴趣的:(ARKit 从零到一 第2部分:平面检测与视觉效果)