ARKit 從零到一 第2部分:平面檢測(cè)與視覺(jué)效果

ARKit——檢測(cè)平面并繪制地板

在第一個(gè) hello world ARKit app 里我們給項(xiàng)目做了初始設(shè)置,并在真實(shí)世界里渲染了一個(gè) 3D 立方體,在用戶移動(dòng)時(shí)也能保持追蹤。

這篇文章中,我會(huì)嘗試從真實(shí)世界中獲取 3D 幾何體并給它添加視覺(jué)效果。檢測(cè)幾何體對(duì)于增強(qiáng)現(xiàn)實(shí) app 來(lái)說(shuō)非常重要,因?yàn)橐層脩舾杏X(jué)在和真實(shí)世界交互,就必須要知道用戶是否敲擊了桌面,或是正在看向地板,亦或是與其它表面進(jìn)行像生活中一樣的交互。本文會(huì)搞定平面檢測(cè),后面的文章則會(huì)使用這些平面并在真實(shí)世界里放置虛擬物體。

ARKit 可以檢測(cè)水平面(我猜測(cè) ARKit 未來(lái)能夠檢測(cè)更復(fù)雜的 3D 幾何體,但應(yīng)該要等到深度感應(yīng)攝像頭的發(fā)布,也許就是 iPhone 8 吧…)。檢測(cè)到平面后,我會(huì)給它加上視覺(jué)效果來(lái)顯示平面的尺寸和角度。下面的視頻展示了實(shí)際效果:

視頻

注意:本文需要你參考此處的代碼:https://github.com/josephchang10/ARCube/tree/part2/ARCube

計(jì)算機(jī)視覺(jué)概念

寫代碼前,有必要了解一下 ARKit 的背后原理,因?yàn)檫@項(xiàng)技術(shù)還不完美,在某些情況下 app 的表現(xiàn)還會(huì)受到影響。

增強(qiáng)現(xiàn)實(shí)的目標(biāo)是往真實(shí)世界中的特定點(diǎn)插入虛擬內(nèi)容,并且在真實(shí)世界中移動(dòng)時(shí)還能對(duì)此虛擬內(nèi)容保持追蹤。ARKit 的基本流程包括從 iOS 設(shè)備攝像頭中讀取視頻幀,對(duì)每一幀的圖片進(jìn)行處理并獲得特征點(diǎn)。特征有很多很多,但我們需要從圖片中找出能在多個(gè)幀中都被追蹤到的特征。特征可能是物體的某個(gè)角,或是有紋理的織物的某條邊等等。有很多種方式可以生成這些特征,可以在網(wǎng)上了解更多(例如搜索 SIFT)但目前我們只需知道,每張圖片里會(huì)產(chǎn)生多個(gè)唯一標(biāo)識(shí)的特征就足夠了。

獲得某張圖片的特征后,就可以從多個(gè)幀中追蹤這些特征,隨著用戶在世界中移動(dòng),就可以利用相應(yīng)的特征點(diǎn)來(lái)估算 3D 姿態(tài)信息,例如當(dāng)前攝像頭的位置和特征的位置。用戶移動(dòng)地越多,就會(huì)獲得越多的特征,并優(yōu)化這些估算的 3D 姿態(tài)信息。

至于平面檢測(cè),就是在獲得一定數(shù)量的 3D 特征點(diǎn)后,嘗試在這些點(diǎn)中安裝一些平面,然后根據(jù)尺度、方向和位置找出最匹配的那個(gè)。ARKit 會(huì)不斷分析 3D 特征點(diǎn)并在代碼中報(bào)告找到的平面。

下面是我的 iPad 看向窗簾時(shí)的截圖??梢钥吹娇椢镉辛己玫募y理,所以追蹤到了大量的唯一特征,每個(gè)十字都是 ARKit 找到的唯一特征。

ARKit 檢測(cè)特征點(diǎn)——織物窗簾

下一張圖是我的桌子,注意并沒(méi)有多少特征點(diǎn):

ARKit 檢測(cè)特征點(diǎn)——反光的桌子上特征很少

所以一定要注意 ARKit 需要看向能檢測(cè)出許多有用特征點(diǎn)的內(nèi)容。可能檢測(cè)不出特征點(diǎn)的情況如下:

  1. 光線差——沒(méi)有足夠的光或光線過(guò)強(qiáng)的鏡面反光。嘗試避免這些光線差的環(huán)境。
  2. 缺少紋理——如果攝像頭指向一面白墻,那也沒(méi)法獲得特征,ARKit 也去無(wú)法找到并追蹤用戶。嘗試避免看向純色、反光表面等地方。
  3. 快速移動(dòng)——通常情況下檢測(cè)和估算 3D 姿態(tài)只會(huì)借助圖片,如果攝像頭移動(dòng)太快圖片就會(huì)糊,從而導(dǎo)致追蹤失敗。但 ARKit 會(huì)利用視覺(jué)慣性里程計(jì),綜合圖片信息和設(shè)備運(yùn)動(dòng)傳感器來(lái)估計(jì)用戶轉(zhuǎn)向的位置。因此 ARKit 在追蹤方面非常強(qiáng)大。

在后面的文章里我會(huì)測(cè)試不同的環(huán)境,以便了解追蹤的效果。

添加 Debug 的視覺(jué)效果

開(kāi)始前有必要給應(yīng)用添加一些 debug 信息,比如渲染 ARKit 報(bào)告的世界原點(diǎn)以及渲染 ARKit 檢測(cè)到的特征點(diǎn),有助于了解當(dāng)前區(qū)域追蹤是否良好。所以,為我們的 ARSCNView 實(shí)例開(kāi)啟 debug 選項(xiàng):

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

檢測(cè)平面幾何體

如果想在 ARKit 里檢測(cè)水平面,可以通過(guò)設(shè)置 session configuration 對(duì)象的 planeDetection 屬性來(lái)指定。這個(gè)值可以被設(shè)置為 ARPlaneDetectionHorizontal 或 ARPlaneDetectionNone。

設(shè)置該屬性后,就會(huì)開(kāi)始收到 ARSCNViewDelegate 協(xié)議 delegate 方法的回調(diào)。這其中有很多方法,首先要使用的是:

/**
有新的 node 被映射到給定的 anchor 時(shí)調(diào)用。

@param renderer 將會(huì)用于渲染 scene 的 renderer。
@param node 映射到 anchor 的 node。
@param anchor 新添加的 anchor。
*/
- (void)renderer:(id <SCNSceneRenderer>)renderer
      didAddNode:(SCNNode *)node
       forAnchor:(ARAnchor *)anchor {
}?

每次 ARKit 自認(rèn)為檢測(cè)到了平面時(shí)都會(huì)調(diào)用此方法。其中有兩個(gè)信息,node 和 anchor。SCNNode 實(shí)例是 ARKit 創(chuàng)建的 SceneKit node,它設(shè)置了一些屬性如 orientation(方向)和 position(位置),然后還有一個(gè) anchor 實(shí)例,包含此錨點(diǎn)的更多信息,例如尺寸和平面的中心點(diǎn)。

anchor 實(shí)例實(shí)際上是 ARPlaneAnchor 類型,從中我們可以得到平面的 extent(范圍)和 center(中心點(diǎn))信息。

渲染平面

有了上述信息,現(xiàn)在可以在虛擬世界里繪制 SceneKit 3D 平面了。創(chuàng)建一個(gè)繼承自 SCNNode 的 Plane 類。在構(gòu)造方法中創(chuàng)建平面并相應(yīng)調(diào)整其大小:

// 用 ARPlaneAnchor 實(shí)例中的尺寸來(lái)創(chuàng)建 3D 平面幾何體
planeGeometry = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z))

let planeNode = SCNNode(geometry: planeGeometry)

// 將平面 plane 移動(dòng)到 ARKit 報(bào)告的位置
planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)

// SceneKit 里的平面默認(rèn)是垂直的,所以需要旋轉(zhuǎn)90度來(lái)匹配 ARKit 中的平面
planeNode.transform = SCNMatrix4MakeRotation(Float(Double.pi/2), 1, 0, 0)

// 因?yàn)槔^承自 SCNNode,所以將新的 node 添加給自己
addChildNode(planeNode)

現(xiàn)在有了 Plane 類,回到 ARSCNViewDelegate 的回調(diào)方法,每次 ARKit 報(bào)告新的 Anchor 時(shí)都可以創(chuàng)建新的平面了:

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)
    } 

注意:實(shí)際的代碼里為了讓視覺(jué)效果更好看,我還給 SCNPlane 幾何體設(shè)置了網(wǎng)格 material,我精簡(jiǎn)了上面為了代碼,這樣看起來(lái)更簡(jiǎn)潔。

更新平面

如果運(yùn)行上面的代碼,走來(lái)走去看看,可以發(fā)現(xiàn)虛擬世界里會(huì)渲染新的平面,但是平面不會(huì)正確擴(kuò)大。ARKit 會(huì)持續(xù)分析場(chǎng)景,如果發(fā)現(xiàn) Plane 比預(yù)想的更大/更小,就會(huì)更新平面的范圍 extent 值。所以需要更新 SceneKit 已渲染的 Plane。

從從一個(gè) ARSCNViewDelegate 方法中獲取更新信息:

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        
        // 查看此平面當(dāng)前是否正在渲染
        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 剛創(chuàng)建時(shí)中心點(diǎn) center 為 0,0,0,node transform 包含了變換參數(shù)。
        // plane 更新后變換沒(méi)變但 center 更新了,所以需要更新 3D 幾何體的位置
        position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
    }

現(xiàn)在有了平面渲染和更新,打開(kāi) app 看看吧。我給 SCNPlane 幾何體添加了科幻風(fēng)的網(wǎng)格紋理,這里省略了此部分,但你可以在源代碼里查看。

獲取結(jié)果

下面貼了幾張文章開(kāi)頭視頻的截圖,這些是我在房子里走來(lái)走去時(shí)檢測(cè)到的平面:

這是樓梯上的滅火器箱,ARKit 正確找出邊緣,而且平面的角度也完全正確,符合其高出地板的表面。

這是樓梯的地面,可以看到在我移動(dòng)時(shí) ARKit 也在不斷發(fā)現(xiàn)新平面,這挺有意思,因?yàn)槿绻阍陂_(kāi)發(fā)某個(gè) app,用戶需要先在空間里轉(zhuǎn)一圈,然后才能放東西,所以應(yīng)該在幾何體成為可用狀態(tài)時(shí)為用戶提供良好的視覺(jué)提示。

下面這張圖和上一張圖是同一個(gè)場(chǎng)景,但幾秒之后,ARKit 就把兩個(gè)平面合并為同一個(gè)平面。注意,在 ARSCNViewDelegate 回調(diào)里你要處理 ARKit 刪除某個(gè) ARPlaneAnchor 實(shí)例的情況,也就是說(shuō)該平面被合并掉了。

這兒很有意思,因?yàn)槲艺驹谶@層樓梯的上一層,距離有3米遠(yuǎn)并且光線不好,但 ARKit 還是找出來(lái)這個(gè)平面,好厲害!

這是在小小的窗臺(tái)上捕獲的平面。注意平面的邊緣超出了實(shí)際的表面。

識(shí)別心得

這是我對(duì)平面檢測(cè)的幾點(diǎn)心得:

  1. 不要期望平面會(huì)完全貼合表面,從視頻中可以看到,雖然檢測(cè)到了平面但角度可能不完全正確,所以如果你在開(kāi)發(fā)的 AR app 需要獲得非常精確的幾何體來(lái)提供更好的效果,你可能會(huì)失望。
  2. 邊緣檢測(cè)不是特別好,實(shí)際的平面范圍有時(shí)會(huì)太大或大小,所以不要嘗試做需要準(zhǔn)確邊緣的 app
  3. 追蹤功能很強(qiáng),速度也很快。可以看到我在視頻中移動(dòng)時(shí),對(duì)真實(shí)世界的平面檢測(cè)相當(dāng)有效,即使快速移動(dòng)攝像頭,效果也同樣很好
  4. 我被特征捕獲驚艷到,哪怕光線不足、距離3-5米遠(yuǎn),ARKit 仍然能找到那些平面。

示例代碼

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

接下來(lái)

下篇文章中我會(huì)用檢測(cè)到的平面在真實(shí)世界中放置 3D 物體,并且嘗試對(duì) app 做一些魯棒化(robustification)改進(jìn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容