前言
對ARKit感興趣的同學(xué)昂秃,可以訂閱ARKit教程專題
源代碼地址在這里
正文
本章重點(diǎn)介紹如何檢測占位符并首先顯示一些位置標(biāo)記,以及之后的平面冬竟。
平面檢測與物體檢測
不要將平面檢測與物體檢測混淆;他們是兩個(gè)不同的東西贼穆。平面檢測內(nèi)置于ARKit中页畦,以幫助程序員將對象放入場景中改艇。
ARKit的平面檢測不是RazeAd所需的工具收班。相反,我們將使用Vision Framework來檢測有限形狀谒兄,然后我們將ARKit平面基于該形狀摔桦。
檢測矩形
iOS的一個(gè)更好的方面是不同框架之間的互操作性。在這種情況下承疲,我們正在利用Vision框架邻耕。視覺框架是一種圖像分析和計(jì)算機(jī)視覺框架,用于識(shí)別和分類現(xiàn)實(shí)世界的對象燕鸽。
我們將使用Vision框架檢測矩形兄世,然后使用VNDetectRectanglesRequest將該矩形轉(zhuǎn)換為ARKit對象,顧名思義啊研,它檢測矩形形狀碘饼。
Vision靜態(tài)檢測對象,因此我們必須為每個(gè)請求提供圖像悲伶。
現(xiàn)在,我們可以使用屏幕上的點(diǎn)按來觸發(fā)圖像分析住涉。
在ARKit導(dǎo)入后再導(dǎo)入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有一個(gè)currentFrame屬性麸锉,它是ARFrame的一個(gè)實(shí)例。幀是視頻源的單個(gè)捕獲舆声。 ARKit分析并將其與設(shè)備的運(yùn)動(dòng)感應(yīng)相結(jié)合花沉。與其他數(shù)據(jù)一起,它包含相機(jī)捕獲的圖像媳握。由于你需要框架碱屁,我們必須防止零幀,在這種情況下你跳過處理蛾找,只需提前返回娩脾。
- 2: 圖像處理對CPU消耗比較大 - 我們需要使用后臺(tái)線程。
- 3: 很快就會(huì)需要do/catch塊打毛。
- 4: 我們創(chuàng)建一個(gè)矩形檢測請求柿赊,它將閉包作為唯一參數(shù)俩功。在完成圖像分析時(shí)調(diào)用閉包,提供剛創(chuàng)建的VNDetectRectanglesRequest實(shí)例和可選錯(cuò)誤碰声。結(jié)果可以作為請求參數(shù)的屬性進(jìn)行訪問诡蜓。
- 5: results屬性將是VNRectangleObservation實(shí)例的數(shù)組,每個(gè)檢測到的矩形一個(gè)胰挑。在這里蔓罚,我們將使用compactMap()從[Any]轉(zhuǎn)換為[VNRectangleObservation]。然后瞻颂,我們將結(jié)果數(shù)組存儲(chǔ)到結(jié)果變量中豺谈。
- 6: 如果請求沒有產(chǎn)生任何結(jié)果,我們可以提前退出蘸朋,因?yàn)闆]有其他事可做核无。否則,如果結(jié)果數(shù)組包含至少一個(gè)檢測到的矩形藕坯,則選擇第一個(gè)团南。
請求將返回最多一個(gè)值,因?yàn)?strong>VNDetectRectanglesRequest的maximumObservations屬性默認(rèn)為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: 結(jié)果是VNRectangleObservation的一個(gè)實(shí)例,它有四個(gè)屬性辐马,用于標(biāo)識(shí)檢測到的矩形的四個(gè)頂點(diǎn)拷橘。
注意:Vision適用于2D圖像,因此它不了解3D世界喜爷。結(jié)果點(diǎn)始終是位圖圖像中的2D坐標(biāo)冗疮。
??我們將這四個(gè)坐標(biāo)轉(zhuǎn)換為一個(gè)地圖友好的數(shù)組,以便更容易按順序處理它們檩帐。
- 2:還記得包含ARKit當(dāng)前處理的圖像的currentFrame變量嗎术幔?它公開了一個(gè)有用的hitTest()方法,用于通過將其投影到對象或3D世界中的錨點(diǎn)來消除2D圖像的點(diǎn)湃密。
?hitTest()返回一個(gè)ARHitTestResult列表诅挑,按距離排序,從最近到最遠(yuǎn)泛源,因此拔妥,再次,我們首先將其存儲(chǔ)到hitFeature變量中达箍。稍后會(huì)詳細(xì)介紹没龙。 - 3:在它的屬性中锰瘸,我們只需要worldTransform桐款,這就是我們返回的內(nèi)容。
- 4:現(xiàn)在坐標(biāo)包含一個(gè)4x4矩陣的數(shù)組,這些矩陣取自worldTransform屬性潮售。由于我們正在處理矩形想括,因此請確保正好有四個(gè)坐標(biāo)儡陨。如果在任何角落中將矩形的2D點(diǎn)更改為ARKit世界中的3D點(diǎn)的過程失敗司致,則可以提前退出。
- 5: 由于代碼將添加和刪除UI元素肛鹏,因此我們需要返回主線程逸邦。
- 6:如果已顯示先前的廣告牌,請將其刪除在扰。此方法尚未實(shí)現(xiàn)缕减。
- 7: 最后,通過調(diào)用createBillboard()創(chuàng)建一個(gè)新的廣告牌芒珠,然后將前面步驟中找到的四個(gè)世界坐標(biāo)傳遞給它桥狡。此方法尚未實(shí)現(xiàn)。
currentFrame.hitTest(_:types :)將一個(gè)點(diǎn)投射到一個(gè)3D對象皱卓。 types參數(shù)確定它們是什么類型的對象裹芝,包括:
- featurePoint:曲面的一個(gè)點(diǎn)部分,但沒有錨點(diǎn)娜汁。
- estimatedHorizo??ntalPlane:搜索檢測到的水平曲面嫂易,但沒有相應(yīng)的錨點(diǎn)。
- existingPlane:具有關(guān)聯(lián)錨點(diǎn)的平面掐禁,不考慮平面的大小怜械。
- existingPlaneUsingExtent:具有關(guān)聯(lián)錨點(diǎn)的平面,與平面的大小相關(guān)傅事。
為了檢測表面上的矩形缕允,我們可能認(rèn)為最后三個(gè)中的任何一個(gè)都可能是一個(gè)不錯(cuò)的選擇。事實(shí)上蹭越,ARKit中的平面有一個(gè)對齊灼芭。由于我們希望檢測任何表面上的矩形 - 因此不限于水平或垂直 - 只留下一個(gè)選項(xiàng):featurePoint。
還有一個(gè)缺失部分需要完成touchesBegan()實(shí)現(xiàn):使用VNDetectRectanglesRequest實(shí)例般又。
在catch回調(diào)之前,我們添加下面的代碼:
// 1
let handler = VNImageRequestHandler( cvPixelBuffer: currentFrame.capturedImage)
// 2
try handler.perform([request])
- 1: 執(zhí)行VNDetectRectanglesRequest的方法是創(chuàng)建一個(gè)負(fù)責(zé)執(zhí)行實(shí)際圖像處理的圖像請求處理程序巍佑。它通過cvPixelBuffer參數(shù)獲取要分析的圖像茴迁。
- 2: 創(chuàng)建請求處理程序后,我們必須讓它發(fā)揮其魔力萤衰。所以我們調(diào)用它的perform方法堕义,傳遞一個(gè)只包含前面步驟中創(chuàng)建的請求的數(shù)組。
注意如何為單幀定義處理程序?qū)嵗捎糜谠谕粠蠄?zhí)行多個(gè)請求倦卖,例如文本識(shí)別洒擦,條形碼檢測等。如果需要執(zhí)行多個(gè)分析怕膛,則創(chuàng)建一個(gè)請求每個(gè)分析熟嫩,但只有一個(gè)處理程序。
創(chuàng)建廣告牌
現(xiàn)在我們有四個(gè)矩陣確定四個(gè)矩形頂點(diǎn)中每個(gè)矩形頂點(diǎn)的位置和方向褐捻。我們將使用這些來幫助定位廣告牌掸茅。
世界變換矩陣包含通過從相機(jī)向相反方向投影2D點(diǎn)及其方向而得到的交點(diǎn)。方向取決于從攝像機(jī)到矩形頂點(diǎn)的虛線之間的角度柠逞,如下圖中的紅線所示昧狮,矩形方向由垂直于平面的直線確定,如綠線所示板壮。
現(xiàn)在我們需要添加一個(gè)擴(kuò)展:
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: 將四個(gè)矩陣存儲(chǔ)到名為RectangularPlane的數(shù)據(jù)容器中逗鸣,該容器還計(jì)算矩形大小及其中心。
- 2: 為平面創(chuàng)建錨點(diǎn)绰精,位于矩形的中心撒璧。
- 3: 將錨點(diǎn)和平面存儲(chǔ)到容器中,以便以后可以訪問它們茬底。
- 4: 最后沪悲,將錨添加到ARKit會(huì)話。
BillboardContainer是一個(gè)實(shí)用程序數(shù)據(jù)結(jié)構(gòu)阱表,用于存儲(chǔ)有關(guān)在BillboardContainer.swift中實(shí)現(xiàn)的廣告牌的數(shù)據(jù)〉钊纾現(xiàn)在我們將擴(kuò)展BillboardContainer。
我們需要添加的最后一段代碼是刪除廣告牌最爬。在createBillboard()之后立即添加以下函數(shù):
func removeBillboard() {
// 1
if let anchor = billboard?.billboardAnchor {
// 2
sceneView.session.remove(anchor: anchor)
// 3
billboard?.billboardNode?.removeFromParentNode() billboard = nil
}
}
上面代碼作用如下:
- 1: 我們可以使用保護(hù)聲明對此進(jìn)行預(yù)先處理涉馁,以確保廣告牌屬性不為零。但它不需要爱致,因?yàn)槲覀冎恍枰獧z查錨是否有值烤送。
- 2: 如果有錨,請將其從ARKit會(huì)話中刪除糠悯。
- 3: 最后帮坚,刪除SceneKit節(jié)點(diǎn)。
下面的物體都是可以檢測到的矩形形狀:
- 外部觸控板互艾。
- A4紙或者白色的信紙试和。
- 筆記本
- 層
- 海報(bào)
- 書
重要的是它的顏色必須與它背后的表面形成鮮明對比,所以白色桌子上的白紙有可能識(shí)別不出來纫普。
在Xcode啟動(dòng)應(yīng)用程序后阅悍,將iPhone的相機(jī)指向我們選擇檢測的對象并點(diǎn)按屏幕。
如果你沒有可以掃描的對象,那就對著屏幕掃描下面這個(gè)黑色的正方形吧:
如果你可以掃描到节视,那么控制臺(tái)會(huì)有如下提示:
如果Vision無法檢測到形狀拳锚,我們將在控制臺(tái)中看到如下消息:
[Vision] VNRequest produced no result
檢測到之后會(huì)有如下提示:
New billboard created
顯示地標(biāo)
我們可能希望在檢測點(diǎn)時(shí)顯示占位符。這可以幫助我們直觀地調(diào)試應(yīng)用程序寻行。
我們在ViewController.swift文件中的touchesBegan()方法中在self.createBillboard調(diào)用之后加入如下代碼:
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: 首先霍掺,我們創(chuàng)建一個(gè)10×10×1 mm的小型SceneKit框。
- 2: 然后寡痰,使用該框創(chuàng)建SceneKit節(jié)點(diǎn)抗楔。
- 3: 接下來,在轉(zhuǎn)換為SCNMatrix4之后拦坠,為新節(jié)點(diǎn)設(shè)置轉(zhuǎn)換矩陣连躏。
- 4: 最后,將節(jié)點(diǎn)添加到場景的根節(jié)點(diǎn)贞滨。
構(gòu)建并運(yùn)行并嘗試檢測矩形形狀;你會(huì)看到類似的東西:
注意每個(gè)矩形頂點(diǎn)的小白色矩形入热。當(dāng)完成視覺測試時(shí),可以注釋掉該代碼晓铆。
注意:我們還可以選擇不同的形狀勺良,大小,顏色骄噪,方向以及可能需要的任何內(nèi)容尚困,以使每個(gè)地標(biāo)在擁擠的場景中脫穎而出。
添加SceneKit節(jié)點(diǎn)
在createBillboard中链蕊,我們創(chuàng)建了一個(gè)ARKit錨點(diǎn)并將其添加到ARKit會(huì)話中事甜。下一步是將ARKit錨轉(zhuǎn)換為SceneKit節(jié)點(diǎn)。
接下來我們需要完善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
}
當(dāng)手動(dòng)將新錨點(diǎn)添加到ARKit會(huì)話時(shí)滔韵,ARKit會(huì)調(diào)用此方法逻谦,讓我們有機(jī)會(huì)通過在方法結(jié)束時(shí)返回它來為新創(chuàng)建的錨點(diǎn)提供SceneKit節(jié)點(diǎn)。
上面的代碼作用如下:
- 1: 驗(yàn)證是否有廣告牌陪蜻,否則退出邦马,返回nil。
- 2: 這是注釋掉的宴卖,以提醒我們通常不會(huì)在主線程中調(diào)用此方法滋将。我們我們有在這里進(jìn)行任何與UI相關(guān)的處理,因此任何線程都可以症昏。
- 3: 在這里檢查錨點(diǎn)是否是廣告牌的錨點(diǎn)耕渴。如果是這樣,則調(diào)用addBillboardNode()齿兔,它返回一個(gè)SCNNode。然后將其設(shè)置為返回值。
在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結(jié)構(gòu)中計(jì)算的大小創(chuàng)建SCNPlane添诉。
- 2: 創(chuàng)建一個(gè)SCNNode,將平面作為幾何體傳遞医寿。然后栏赴,將節(jié)點(diǎn)添加到廣告牌容器并返回它。
運(yùn)行程序靖秩,效果如下:
位置有一些偏差须眷,不過后面我們會(huì)做優(yōu)化的。
處理中斷
除了這個(gè)方向問題沟突,一切看起來都很棒花颗。但是,在將應(yīng)用程序置于后臺(tái)之前惠拭,有一個(gè)小問題可能會(huì)被忽視扩劝。比如下面這些:
- 1: 運(yùn)行app
- 2: 檢測矩形。
- 3: ARKit顯示檢測到的平面后职辅,按Home鍵棒呛。
- 4: 應(yīng)用程序進(jìn)入后臺(tái)后,請更改設(shè)備的方向域携。
- 5: 恢復(fù)應(yīng)用程序簇秒。
無論新方向是什么,我們都可以在將應(yīng)用程序發(fā)送到后臺(tái)之前的同一屏幕位置找到該平面秀鞭。但是趋观,如果我們在應(yīng)用程序處于活動(dòng)狀態(tài)時(shí)更改方向,則該平面將移動(dòng)到其新位置气筋。
當(dāng)ARKit會(huì)話中斷時(shí)拆内,設(shè)備停止向ARKit饋送用于確定相對于當(dāng)前手機(jī)位置和方向的節(jié)點(diǎn)位置的硬件傳感器信息。
雖然可以使用后臺(tái)處理作為解決方法宠默,但這不是一個(gè)合理的解決方案 - 除非只想使用它幾秒鐘麸恍,例如當(dāng)用戶暫時(shí)被外部事件分心并且他們在幾個(gè)內(nèi)部返回應(yīng)用程序時(shí)秒。但是定期執(zhí)行此操作會(huì)對設(shè)備的資源造成巨大損失搀矫。
當(dāng)用戶恢復(fù)應(yīng)用程序時(shí)抹沪,他們必須重復(fù)平面檢測過程。
實(shí)施需要在會(huì)話中斷時(shí)刪除廣告牌瓤球。在ARSCNViewDelegate中有一個(gè)SceneKit委托方法:sessionWasInterrupted融欧。
此方法已包含在代碼中但它是空的。將這行代碼添加到其正文中:
removeBillboard()
這將刪除廣告牌卦羡,以便當(dāng)我們從后臺(tái)恢復(fù)應(yīng)用程序時(shí)噪馏,它將返回到相同的狀態(tài)麦到。
上一章 | 目錄 | 下一章 |
---|