16-《ARKit by Tutorials》讀書筆記3:互動操作

蘋果API搬運工發表於2018-12-21

說明

ARKit系列文章目錄

本文是Ray Wenderlich上《ARKit by Tutorials》的讀書筆記,主要講內容概要和讀後感 

ARKit by Tutorials中講到了影像識別觸發AR場景互動的一種特殊方法:利用Vision Framework來識別一些物體,然後在上面展示一些圖片或動畫.

還有利用地理定位和iBeacon觸發AR互動的方法.

為什麼用Vision

可能你會覺得奇怪:為什麼不用ARKit自帶的圖片檢測功能? 只要把參考圖片的素材放好,設定好物理尺寸,ARKit就可以檢測到圖片,在WWDC2018上ARKit 2更是增加了圖片追蹤功能,效果非常好,識別率高,追蹤穩定.

那是因為,ARKit目前自帶的圖片檢測和追蹤功能,有幾點要求不太好滿足:

  • 圖片不能過於相似;
  • 圖片的色彩直方圖分佈要均勻(不能是黑白的);
  • 圖片不能有大片相同顏色的區域;
  • 圖片的物理尺寸必須是已知且準確的.

比如下面的圖片就不滿足要求,雖然也能檢測到,但追蹤效果會差很多.

16-《ARKit by Tutorials》讀書筆記3:互動操作

更麻煩的是:二維碼.

  • 雖然不同內容的二維碼圖片本身並不相同,但是仍然太相似了,尤其是文字很長的時候;
  • 二維碼一般是黑白的;
  • 二維碼中有大片相同顏色的區域;
  • 二維碼的尺寸往往是不同的;

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.

16-《ARKit by Tutorials》讀書筆記3:互動操作
三維平面的位置由錨點決定,錨點位置則由4個點的中心點確定:

let anchor = ARAnchor(transform: plane.center)
sceneView.session.add(anchor: anchor)
複製程式碼

同時還可以建立四個SCNBox來標識矩形的四個角,效果如下

16-《ARKit by Tutorials》讀書筆記3:互動操作

座標系的處理

但這樣建立出的平面有個問題,朝向不正確

16-《ARKit by Tutorials》讀書筆記3:互動操作
這是因為,我們是根據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軸則是裝置初始化時的位置和朝向.也就是預設設定項
    16-《ARKit by Tutorials》讀書筆記3:互動操作
  • gravityAndHeading:y軸與重力方向平行,而x軸指向東,z軸指向南.和現實世界保持一致.
    16-《ARKit by Tutorials》讀書筆記3:互動操作
  • camera:座標系始終跟隨攝像機(也就是手機)的位置和朝向,伴隨移動.
    16-《ARKit by Tutorials》讀書筆記3:互動操作

如果我們採用第三種配置,那麼建立出的平面是平行於x-y平面的,即平行於手機螢幕的.但是由於正常情況下,識別過程中手機是正對著要識別物件的,所以得到的結果就是幾乎是正確的.

configuration.worldAlignment = .camera
複製程式碼

16-《ARKit by Tutorials》讀書筆記3:互動操作

個人認為:這種做法很扯蛋,根本沒有解決問題,只是當使用者垂直於矩形進行識別時,效果較好(遠遠算不上完美)而已.
我認為可以這樣解決,歡迎大家討論:

  1. 利用4個點,計算所在平面的法線A,法線A的方向就取指向攝像機(手機)方向為正.考慮到4個點可能不共面(畢竟立體幾何中3點確定一個平面),可以用排列組合的方式輪流取3個點求法向量,總共求出4個法向量再求平均值做為法線A;(求平面的法向量可以用兩條邊向量的叉乘)
  2. 將建立出的平面的法線B對準剛才計算出的法線A.如何對準呢?變換矩陣是什麼? 這就是一個數學問題:已知初始法線B,和目標法線A,求變換矩陣;可以藉助四元數進行求解,或者直接設定四元數來處理旋轉;
  3. 將建立出的平面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類可以識別文字和區域;
  • Rectangleobject追蹤:VNTrackRectangleRequestVNTrackObjectRequest類可以追蹤識別出的物體.

例如,上面的例子想改成識別二維碼,並在二維碼上顯示圖片或視訊,只需要更改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
    }

  ...
}
複製程式碼

效果如下:

16-《ARKit by Tutorials》讀書筆記3:互動操作

後續還可以在識別出二維碼的內容後,在上面展示圖片,開啟網頁或播放視訊等

16-《ARKit by Tutorials》讀書筆記3:互動操作
16-《ARKit by Tutorials》讀書筆記3:互動操作

地理定位相關

除了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定位;

第三部分讀書筆記結束!

相關文章