前言
對ARKit感興趣的同學,可以訂閱ARKit教程專題
源代碼地址在這里
正文
本章重點介紹如何檢測占位符并首先顯示一些位置標記,以及之后的平面。
平面檢測與物體檢測
不要將平面檢測與物體檢測混淆;他們是兩個不同的東西。平面檢測內置于ARKit中,以幫助程序員將對象放入場景中。
ARKit的平面檢測不是RazeAd所需的工具。相反,我們將使用Vision Framework來檢測有限形狀,然后我們將ARKit平面基于該形狀。
檢測矩形
iOS的一個更好的方面是不同框架之間的互操作性。在這種情況下,我們正在利用Vision框架。視覺框架是一種圖像分析和計算機視覺框架,用于識別和分類現實世界的對象。
我們將使用Vision框架檢測矩形,然后使用VNDetectRectanglesRequest將該矩形轉換為ARKit對象,顧名思義,它檢測矩形形狀。
Vision靜態檢測對象,因此我們必須為每個請求提供圖像。
現在,我們可以使用屏幕上的點按來觸發圖像分析。
在ARKit導入后再導入Vision框架:
import Vision
我們添加以下代碼:
// 1
guard let currentFrame = sceneView.session.currentFrame else {
return
}
// 2
DispatchQueue.global(qos: .background).async {
// 3
do {
// 4
let request = VNDetectRectanglesRequest {(request, error) in
// Access the first result in the array,
// after converting to an array
// of VNRectangleObservation
// 5
guard let results = request.results?.compactMap({ $0 as? VNRectangleObservation }),
// 6
let result = results.first else {
print ("[Vision] VNRequest produced no result")
return
}
}
} catch(let error) {
print( "An error occurred during rectangle detection: \(error)")
}
}
上面的代碼作用如下:
- 1: ARSession有一個currentFrame屬性,它是ARFrame的一個實例。幀是視頻源的單個捕獲。 ARKit分析并將其與設備的運動感應相結合。與其他數據一起,它包含相機捕獲的圖像。由于你需要框架,我們必須防止零幀,在這種情況下你跳過處理,只需提前返回。
- 2: 圖像處理對CPU消耗比較大 - 我們需要使用后臺線程。
- 3: 很快就會需要do/catch塊。
- 4: 我們創建一個矩形檢測請求,它將閉包作為唯一參數。在完成圖像分析時調用閉包,提供剛創建的VNDetectRectanglesRequest實例和可選錯誤。結果可以作為請求參數的屬性進行訪問。
- 5: results屬性將是VNRectangleObservation實例的數組,每個檢測到的矩形一個。在這里,我們將使用compactMap()從[Any]轉換為[VNRectangleObservation]。然后,我們將結果數組存儲到結果變量中。
- 6: 如果請求沒有產生任何結果,我們可以提前退出,因為沒有其他事可做。否則,如果結果數組包含至少一個檢測到的矩形,則選擇第一個。
請求將返回最多一個值,因為VNDetectRectanglesRequest的maximumObservations屬性默認為1。
在let result = results.first else ... block之后,添加以下代碼:
// 1
let coordinates: [matrix_float4x4] = [
result.topLeft,
result.topRight,
result.bottomRight,
result.bottomLeft ].compactMap {
// 2
guard let hitFeature = currentFrame.hitTest( $0, types: .featurePoint).first else { return nil }
// 3
return hitFeature.worldTransform
}
// 4
guard coordinates.count == 4 else { return }
// 5
DispatchQueue.main.async {
// 6
self.removeBillboard()
let (topLeft, topRight, bottomRight, bottomLeft) = (coordinates[0], coordinates[1], coordinates[2], coordinates[3])
// 7
self.createBillboard(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft)
}
上面的代碼作用如下:
- 1: 結果是VNRectangleObservation的一個實例,它有四個屬性,用于標識檢測到的矩形的四個頂點。
注意:Vision適用于2D圖像,因此它不了解3D世界。結果點始終是位圖圖像中的2D坐標。
??我們將這四個坐標轉換為一個地圖友好的數組,以便更容易按順序處理它們。
- 2:還記得包含ARKit當前處理的圖像的currentFrame變量嗎?它公開了一個有用的hitTest()方法,用于通過將其投影到對象或3D世界中的錨點來消除2D圖像的點。
?hitTest()返回一個ARHitTestResult列表,按距離排序,從最近到最遠,因此,再次,我們首先將其存儲到hitFeature變量中。稍后會詳細介紹。 - 3:在它的屬性中,我們只需要worldTransform,這就是我們返回的內容。
- 4:現在坐標包含一個4x4矩陣的數組,這些矩陣取自worldTransform屬性。由于我們正在處理矩形,因此請確保正好有四個坐標。如果在任何角落中將矩形的2D點更改為ARKit世界中的3D點的過程失敗,則可以提前退出。
- 5: 由于代碼將添加和刪除UI元素,因此我們需要返回主線程。
- 6:如果已顯示先前的廣告牌,請將其刪除。此方法尚未實現。
- 7: 最后,通過調用createBillboard()創建一個新的廣告牌,然后將前面步驟中找到的四個世界坐標傳遞給它。此方法尚未實現。
currentFrame.hitTest(_:types :)將一個點投射到一個3D對象。 types參數確定它們是什么類型的對象,包括:
- featurePoint:曲面的一個點部分,但沒有錨點。
- estimatedHorizo??ntalPlane:搜索檢測到的水平曲面,但沒有相應的錨點。
- existingPlane:具有關聯錨點的平面,不考慮平面的大小。
- existingPlaneUsingExtent:具有關聯錨點的平面,與平面的大小相關。
為了檢測表面上的矩形,我們可能認為最后三個中的任何一個都可能是一個不錯的選擇。事實上,ARKit中的平面有一個對齊。由于我們希望檢測任何表面上的矩形 - 因此不限于水平或垂直 - 只留下一個選項:featurePoint。
還有一個缺失部分需要完成touchesBegan()實現:使用VNDetectRectanglesRequest實例。
在catch回調之前,我們添加下面的代碼:
// 1
let handler = VNImageRequestHandler( cvPixelBuffer: currentFrame.capturedImage)
// 2
try handler.perform([request])
- 1: 執行VNDetectRectanglesRequest的方法是創建一個負責執行實際圖像處理的圖像請求處理程序。它通過cvPixelBuffer參數獲取要分析的圖像。
- 2: 創建請求處理程序后,我們必須讓它發揮其魔力。所以我們調用它的perform方法,傳遞一個只包含前面步驟中創建的請求的數組。
注意如何為單幀定義處理程序實例,但它可用于在同一幀上執行多個請求,例如文本識別,條形碼檢測等。如果需要執行多個分析,則創建一個請求每個分析,但只有一個處理程序。
創建廣告牌
現在我們有四個矩陣確定四個矩形頂點中每個矩形頂點的位置和方向。我們將使用這些來幫助定位廣告牌。
世界變換矩陣包含通過從相機向相反方向投影2D點及其方向而得到的交點。方向取決于從攝像機到矩形頂點的虛線之間的角度,如下圖中的紅線所示,矩形方向由垂直于平面的直線確定,如綠線所示。
現在我們需要添加一個擴展:
func createBillboard(
topLeft: matrix_float4x4, topRight: matrix_float4x4, bottomRight: matrix_float4x4, bottomLeft: matrix_float4x4) {
// 1
let plane = RectangularPlane( topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
// 2
let anchor = ARAnchor(transform: plane.center)
// 3
billboard = BillboardContainer(billboardAnchor: anchor, plane: plane)
// 4
sceneView.session.add(anchor: anchor)
print("New billboard created")
}
上面的代碼作用如下:
- 1: 將四個矩陣存儲到名為RectangularPlane的數據容器中,該容器還計算矩形大小及其中心。
- 2: 為平面創建錨點,位于矩形的中心。
- 3: 將錨點和平面存儲到容器中,以便以后可以訪問它們。
- 4: 最后,將錨添加到ARKit會話。
BillboardContainer是一個實用程序數據結構,用于存儲有關在BillboardContainer.swift中實現的廣告牌的數據。現在我們將擴展BillboardContainer。
我們需要添加的最后一段代碼是刪除廣告牌。在createBillboard()之后立即添加以下函數:
func removeBillboard() {
// 1
if let anchor = billboard?.billboardAnchor {
// 2
sceneView.session.remove(anchor: anchor)
// 3
billboard?.billboardNode?.removeFromParentNode() billboard = nil
}
}
上面代碼作用如下:
- 1: 我們可以使用保護聲明對此進行預先處理,以確保廣告牌屬性不為零。但它不需要,因為我們只需要檢查錨是否有值。
- 2: 如果有錨,請將其從ARKit會話中刪除。
- 3: 最后,刪除SceneKit節點。
下面的物體都是可以檢測到的矩形形狀:
- 外部觸控板。
- A4紙或者白色的信紙。
- 筆記本
- 層
- 海報
- 書
重要的是它的顏色必須與它背后的表面形成鮮明對比,所以白色桌子上的白紙有可能識別不出來。
在Xcode啟動應用程序后,將iPhone的相機指向我們選擇檢測的對象并點按屏幕。
如果你沒有可以掃描的對象,那就對著屏幕掃描下面這個黑色的正方形吧:
如果你可以掃描到,那么控制臺會有如下提示:
如果Vision無法檢測到形狀,我們將在控制臺中看到如下消息:
[Vision] VNRequest produced no result
檢測到之后會有如下提示:
New billboard created
顯示地標
我們可能希望在檢測點時顯示占位符。這可以幫助我們直觀地調試應用程序。
我們在ViewController.swift文件中的touchesBegan()方法中在self.createBillboard調用之后加入如下代碼:
for coordinate in coordinates {
// 1
let box = SCNBox(width: 0.01, height: 0.01, length: 0.001, chamferRadius: 0.0)
// 2
let node = SCNNode(geometry: box)
// 3
node.transform = SCNMatrix4(coordinate)
// 4
self.sceneView.scene.rootNode.addChildNode(node)
}
- 1: 首先,我們創建一個10×10×1 mm的小型SceneKit框。
- 2: 然后,使用該框創建SceneKit節點。
- 3: 接下來,在轉換為SCNMatrix4之后,為新節點設置轉換矩陣。
- 4: 最后,將節點添加到場景的根節點。
構建并運行并嘗試檢測矩形形狀;你會看到類似的東西:
注意每個矩形頂點的小白色矩形。當完成視覺測試時,可以注釋掉該代碼。
注意:我們還可以選擇不同的形狀,大小,顏色,方向以及可能需要的任何內容,以使每個地標在擁擠的場景中脫穎而出。
添加SceneKit節點
在createBillboard中,我們創建了一個ARKit錨點并將其添加到ARKit會話中。下一步是將ARKit錨轉換為SceneKit節點。
接下來我們需要完善ARSCNViewDelegate方法:
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
// 1
guard let billboard = billboard else { return nil }
var node: SCNNode? = nil
// 2
//DispatchQueue.main.sync { switch anchor {
// 3
case billboard.billboardAnchor:
let billboardNode = addBillboardNode()
node = billboardNode
default:
break
}
//}
return node
}
當手動將新錨點添加到ARKit會話時,ARKit會調用此方法,讓我們有機會通過在方法結束時返回它來為新創建的錨點提供SceneKit節點。
上面的代碼作用如下:
- 1: 驗證是否有廣告牌,否則退出,返回nil。
- 2: 這是注釋掉的,以提醒我們通常不會在主線程中調用此方法。我們我們有在這里進行任何與UI相關的處理,因此任何線程都可以。
- 3: 在這里檢查錨點是否是廣告牌的錨點。如果是這樣,則調用addBillboardNode(),它返回一個SCNNode。然后將其設置為返回值。
在createBillboard()后面,我們添加如下代碼:
func addBillboardNode() -> SCNNode? {
guard let billboard = billboard else { return nil }
// 1
let rectangle = SCNPlane(width: billboard.plane.width, height: billboard.plane.height)
// 2
let rectangleNode = SCNNode(geometry: rectangle)
self.billboard?.billboardNode = rectangleNode
return rectangleNode
}
上面的代碼作用如下:
- 1: 使用之前在RectangularPlane結構中計算的大小創建SCNPlane。
- 2: 創建一個SCNNode,將平面作為幾何體傳遞。然后,將節點添加到廣告牌容器并返回它。
運行程序,效果如下:
位置有一些偏差,不過后面我們會做優化的。
處理中斷
除了這個方向問題,一切看起來都很棒。但是,在將應用程序置于后臺之前,有一個小問題可能會被忽視。比如下面這些:
- 1: 運行app
- 2: 檢測矩形。
- 3: ARKit顯示檢測到的平面后,按Home鍵。
- 4: 應用程序進入后臺后,請更改設備的方向。
- 5: 恢復應用程序。
無論新方向是什么,我們都可以在將應用程序發送到后臺之前的同一屏幕位置找到該平面。但是,如果我們在應用程序處于活動狀態時更改方向,則該平面將移動到其新位置。
當ARKit會話中斷時,設備停止向ARKit饋送用于確定相對于當前手機位置和方向的節點位置的硬件傳感器信息。
雖然可以使用后臺處理作為解決方法,但這不是一個合理的解決方案 - 除非只想使用它幾秒鐘,例如當用戶暫時被外部事件分心并且他們在幾個內部返回應用程序時秒。但是定期執行此操作會對設備的資源造成巨大損失。
當用戶恢復應用程序時,他們必須重復平面檢測過程。
實施需要在會話中斷時刪除廣告牌。在ARSCNViewDelegate中有一個SceneKit委托方法:sessionWasInterrupted。
此方法已包含在代碼中但它是空的。將這行代碼添加到其正文中:
removeBillboard()
這將刪除廣告牌,以便當我們從后臺恢復應用程序時,它將返回到相同的狀態。
上一章 | 目錄 | 下一章 |
---|