說明
本文是Ray Wenderlich上《ARKit by Tutorials》的讀書筆記,主要講內容概要和讀後感
ARKit by Tutorials中講到了影像識別觸發AR場景互動的一種特殊方法:利用Vision Framework來識別一些物體,然後在上面展示一些圖片或動畫.
還有利用地理定位和iBeacon觸發AR互動的方法.
為什麼用Vision
可能你會覺得奇怪:為什麼不用ARKit自帶的圖片檢測功能? 只要把參考圖片的素材放好,設定好物理尺寸,ARKit就可以檢測到圖片,在WWDC2018上ARKit 2更是增加了圖片追蹤功能,效果非常好,識別率高,追蹤穩定.
那是因為,ARKit目前自帶的圖片檢測和追蹤功能,有幾點要求不太好滿足:
- 圖片不能過於相似;
- 圖片的色彩直方圖分佈要均勻(不能是黑白的);
- 圖片不能有大片相同顏色的區域;
- 圖片的物理尺寸必須是已知且準確的.
比如下面的圖片就不滿足要求,雖然也能檢測到,但追蹤效果會差很多.
更麻煩的是:二維碼.
- 雖然不同內容的二維碼圖片本身並不相同,但是仍然太相似了,尤其是文字很長的時候;
- 二維碼一般是黑白的;
- 二維碼中有大片相同顏色的區域;
- 二維碼的尺寸往往是不同的;
而Vision框架可以識別的內容就很多,可以識別矩形,二維碼等等.我們可以把它們兩個結合起來使用,達到神奇的效果.
識別任意矩形
Vision框架的使用本身並不難,在AR專案中,寫個touchesBegan()
方法,在其中寫上:
// 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
}
// 得到識別結果,稍後在這裡新增處理程式碼.
}
let handler = VNImageRequestHandler(cvPixelBuffer: currentFrame.capturedImage)
try handler.perform([request])
} catch(let error) {
print("An error occurred during rectangle detection: \(error)")
}
}
複製程式碼
可以看到,在上面第6步之後,已經得到了識別出的矩形的結果,繼續通過hitTest方法,根據二維的螢幕座標上矩形的四個角的位置(二維座標),找到三維空間裡矩形的四個角的位置(三維座標).
// 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)
}
複製程式碼
利用hitTest方法得到了四個featurePoint,後建立一個三維的平面Billboard.
三維平面的位置由錨點決定,錨點位置則由4個點的中心點確定:let anchor = ARAnchor(transform: plane.center)
sceneView.session.add(anchor: anchor)
複製程式碼
同時還可以建立四個SCNBox來標識矩形的四個角,效果如下
座標系的處理
但這樣建立出的平面有個問題,朝向不正確
這是因為,我們是根據4個點來建立的平面,這4個點是在世界座標下的點,本身只有位置座標,沒有旋轉和縮放資訊,列印print(coordinates[0])
結果如下:
simd_float4x4([
[1.0, 0.0, 0.0, 0.0)],
[0.0, 1.0, 0.0, 0.0)],
[0.0, 0.0, 1.0, 0.0)],
[-0.0293431, -0.238044, -0.290515, 1.0)]
])
複製程式碼
用這樣的4個點去建立平面,過程如下:
func addBillboardNode() -> SCNNode? {
guard let billboard = billboard else { return nil }
// 1 寬和高是從4個點的位置計算出來的
let rectangle = SCNPlane(width: billboard.plane.width,
height: billboard.plane.height)
// 2 無法得到transform資訊,不能正確顯示方向,而SCNPlane的預設方向是在x-y平面上,也就是垂直於地面(沿y軸方向),與手機的初始化方向平行(x-y平面方向平行)
let rectangleNode = SCNNode(geometry: rectangle)
self.billboard?.billboardNode = rectangleNode
return rectangleNode
}
複製程式碼
這裡就能看出問題:建立平面只利用了4個點的寬高資訊,朝向資訊沒有設定使用了預設方向.
書中給出了一種處理方式:更改ARKit配置項ARConfiguration中的worldAlignment
屬性.這個屬性有三個值:
- gravity:座標系的y軸是與重力方向平行的,座標原點及x-z軸則是裝置初始化時的位置和朝向.也就是預設設定項
- gravityAndHeading:y軸與重力方向平行,而x軸指向東,z軸指向南.和現實世界保持一致.
- camera:座標系始終跟隨攝像機(也就是手機)的位置和朝向,伴隨移動.
如果我們採用第三種配置,那麼建立出的平面是平行於x-y平面的,即平行於手機螢幕的.但是由於正常情況下,識別過程中手機是正對著要識別物件的,所以得到的結果就是幾乎是正確的.
configuration.worldAlignment = .camera
複製程式碼
個人認為:這種做法很扯蛋,根本沒有解決問題,只是當使用者垂直於矩形進行識別時,效果較好(遠遠算不上完美)而已.
我認為可以這樣解決,歡迎大家討論:
- 利用4個點,計算所在平面的法線A,法線A的方向就取指向攝像機(手機)方向為正.考慮到4個點可能不共面(畢竟立體幾何中3點確定一個平面),可以用排列組合的方式輪流取3個點求法向量,總共求出4個法向量再求平均值做為法線A;(求平面的法向量可以用兩條邊向量的叉乘)
- 將建立出的平面的法線B對準剛才計算出的法線A.如何對準呢?變換矩陣是什麼? 這就是一個數學問題:已知初始法線B,和目標法線A,求變換矩陣;可以藉助四元數進行求解,或者直接設定四元數來處理旋轉;
- 將建立出的平面SCNNode的矩陣屬性設定為求出的變換矩陣就可以了;
// 可以先求出從法線B到法線A的四元數
extension simd_quatf {
/// A quaternion whose action rotates the vector `from` onto the vector `to`.
public init(from: float3, to: float3)
}
// 從四元數中得到變換矩陣或直接使用四元數
extension simd_float4x4 {
/// Construct a 4x4 matrix from `quaternion`.
public init(_ quaternion: simd_quatf)
}
複製程式碼
Vision能實現的其它功能
除了矩形之外,Vision還能識別出其它物體:
- Horizon:
VNDetectHorizonRequest
類可以得到畫面的水平角度. - Faces:
VNDetectFaceRectanglesRequest
類可以實現人臉識別; - Text:
VNDetectTextRectanglesRequest
類可以識別文字和區域; - Rectangle和object追蹤:
VNTrackRectangleRequest
和VNTrackObjectRequest
類可以追蹤識別出的物體.
例如,上面的例子想改成識別二維碼,並在二維碼上顯示圖片或視訊,只需要更改Vision部分的程式碼就行了:
let request = VNDetectBarcodesRequest { (request, error) in
// Access the first result in the array,
// after converting to an array
// of VNBarcodeObservation
guard let results = request.results?.compactMap({
$0 as? VNBarcodeObservation }),
let result = results.first else {
print ("[Vision] VNRequest produced no result")
return
}
...
}
複製程式碼
效果如下:
後續還可以在識別出二維碼的內容後,在上面展示圖片,開啟網頁或播放視訊等
地理定位相關
除了Vision識別來觸發場景外,還講到了利用地理定位和iBeacon來觸發AR場景,其實核心程式碼非常簡單,如果你做過地圖開發或iBeacon開發的話,就知道其實就是下面幾個代理方法:
// MARK: - LocationManagerDelegate
extension AdViewController: LocationManagerDelegate {
// MARK: Location
func locationManager(_ locationManager: LocationManager,
didEnterRegionId regionId: String) {
}
func locationManager(_ locationManager: LocationManager,
didExitRegionId regionId: String) {
}
// MARK: Beacons
func locationManager(_ locationManager: LocationManager,
didRangeBeacon beacon: CLBeacon) {
}
func locationManager(_ locationManager: LocationManager,
didLeaveBeacon beacon: CLBeacon) {
}
}
複製程式碼
具體業務邏輯沒有什麼太大的難點,不再贅述了.
需要注意的是,提到了地理定位的測試方法:
- 用 .gpx檔案來做虛擬定位測試;
- 用lightblue等藍芽軟體來模擬iBeacon定位;
第三部分讀書筆記結束!