阅读 221

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

说明

ARKit系列文章目录

本文是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
复制代码

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

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

第三部分读书笔记结束!

关注下面的标签,发现更多相似文章
评论