16-《ARKit by Tutorials》读书笔记3:交互操做

说明

ARKit系列文章目录swift

本文是Ray Wenderlich上《ARKit by Tutorials》的读书笔记,主要讲内容概要和读后感 session

ARKit by Tutorials中讲到了图像识别触发AR场景交互的一种特殊方法:利用Vision Framework来识别一些物体,而后在上面展现一些图片或动画.框架

还有利用地理定位和iBeacon触发AR交互的方法.async

为何用Vision

可能你会以为奇怪:为何不用ARKit自带的图片检测功能? 只要把参考图片的素材放好,设置好物理尺寸,ARKit就能够检测到图片,在WWDC2018上ARKit 2更是增长了图片追踪功能,效果很是好,识别率高,追踪稳定.post

那是由于,ARKit目前自带的图片检测和追踪功能,有几点要求不太好知足:测试

  • 图片不能过于类似;
  • 图片的色彩直方图分布要均匀(不能是黑白的);
  • 图片不能有大片相同颜色的区域;
  • 图片的物理尺寸必须是已知且准确的.

好比下面的图片就不知足要求,虽然也能检测到,但追踪效果会差不少. 动画

更麻烦的是:二维码.ui

  • 虽然不一样内容的二维码图片自己并不相同,可是仍然太类似了,尤为是文本很长的时候;
  • 二维码通常是黑白的;
  • 二维码中有大片相同颜色的区域;
  • 二维码的尺寸每每是不一样的;

Vision框架能够识别的内容就不少,能够识别矩形,二维码等等.咱们能够把它们两个结合起来使用,达到神奇的效果.spa

识别任意矩形

Vision框架的使用自己并不难,在AR项目中,写个touchesBegan()方法,在其中写上:3d

// 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
复制代码

我的认为:这种作法很扯蛋,根本没有解决问题,只是当用户垂直于矩形进行识别时,效果较好(远远算不上完美)而已.
我认为能够这样解决,欢迎你们讨论:

  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
    }

  ...
}
复制代码

效果以下:

后续还能够在识别出二维码的内容后,在上面展现图片,打开网页或播放视频等

地理定位相关

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

第三部分读书笔记结束!