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

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 检测特征点——织物窗帘

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

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 <SCNSceneRenderer>)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 也在不断发现新平面,这挺有意思,因为如果你在开发某个 app,用户需要先在空间里转一圈,然后才能放东西,所以应该在几何体成为可用状态时为用户提供良好的视觉提示。

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

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

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

识别心得

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

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

示例代码

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

接下来

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容