说明
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.
let anchor = ARAnchor(transform: plane.center)
sceneView.session.add(anchor: anchor)
复制代码
同时还可以创建四个SCNBox来标识矩形的四个角,效果如下
坐标系的处理
但这样创建出的平面有个问题,朝向不正确
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定位;
第三部分读书笔记结束!