Find me - 用 ARKit 找人

Find me - 用 ARKit 找人

引言

从 Apple 发布 ARKit 框架起,我就一直想学习并做点好玩的东西,后来就勾搭了滑滑鸡大佬来上海 Code<T> 沙龙第八次活动讲他做的 ARGitHubCommits,学习了一些 ARKit 的基础知识,后来持续跟进了一波,看了很多张嘉夫大佬的 ARKit 文章,也看了一些 ARKit 开源的项目。那会微博上有一个 ARKit 不停的弹 Windows 告警对话框的动态图特别火,看到的时候我就想,弹出来的这一堆对话框好像一条路径啊,这也是 Find me 这个 App 最初的灵感来源。

探索

后来就想利用这个特性做一些寻路的方面的探索,最开始是想做在家里找东西,脑洞如下:

  • 在家里找一个位置固定不变的物品(比如电视机)做为参考点。

  • 为每件物品录制一条路径到这个参考点。

  • 想找某件物品的时候先找到这个参考点,然后就可以通过之前录制的路径找到这件物品了。

  • 但是发现有两个特别大的痛点:

  • 东西位置经常会变,路径也要跟着变。

  • 找不到的东西往往是乱放的东西,所以当然也没有路径记录一说。

  • 所以这个脑洞就被我 Pass 了,项目也搁置了一段时间。

    然后有一天,我约了朋友去商场吃饭。他先到了店里,但是我确始终找不到这个店,问了半天才在一个很隐蔽的角落找到了这个店。约完回家之后灵感突发,这个场景完全可以用之前找东西的思路来做啊:

  • 先到商场的人找一个好找的位置,比如商场北门。

  • 从这个位置开始记录一条到门店的路径,并分享给后来的朋友。

  • 后来的朋友找到这条路径的起点,也就是之前说的商场北门,打开先到的人分享的路径即可沿着路径找到约好见面的门店。

  • 关键问题:从使用场景来说,没有痛点。

    这也是我今天文章内容的主角 - Find me 实现的功能。

    Find me - 用 ARKit 找人

    目前这个 App 已经发布上架:Find me,而且代码已经开源了,地址在:mmoaay/Findme(喜欢的话记得点个 Star),为什么选择开源呢?因为只是一个创意,并没有太多的技术壁垒,而且目前在技术上确实存在两个问题:

  • 路径记录和寻找过程中 ARKit 的 Session 不能被打断,因为恢复之后的虚拟坐标会产生极大偏差,导致虚拟路径进入不可控状态。

  • ARKit 目前不稳定,虚拟坐标系会抖动,这样就会导致虚拟路径有偏移,而且距离越远,偏差越大。

  • 产生这两个问题的原因主要都在虚拟世界坐标上,我们都知道,ARKit 初始化的时候,会基于你当前位置为世界原点,建立了一个虚拟世界坐标系:

    Find me - 用 ARKit 找人

    所以,Find me 记录并分享出来的路径,对 ARKit 虚拟世界的坐标系有两个基本要求:

  • 记录路径和根据路径找人时虚拟世界的原点一定要映射到现实世界中的同一个点,也就是分享和寻找的起点位置要是同一个。

  • 虚拟世界的水平垂直方向要和现实世界一样。

  • 优化

    对于这两个问题,我也做了一些优化。

    目前失败的两个优化

    基于定位优化

    思路很简单:根据定位将路径分段记录,并修正虚拟路径。相当于给虚拟路径加一个现实世界的坐标修正。

    实践之后发现:定位比 ARKit 还不准,尤其在商场内,基于 WiFi 的定位基本上能让你的位置到处跳。Pass!

    基于距离优化

    这个方案的思路是:根据你在虚拟世界移动的距离分段记录路径。

    实践之后问题也来了:ARKit 的初始化太慢了,在我的 iPhone 7 上需要的时间足足有 3 秒…而且 ARKit 的 Seesion 还不能并发,因为摄像头只有一个,只有上一个结束了,下一个才能开始,最后发现路径根本没法记录…

    成功优化

    ARKit 使用优化

    设置 ARWorldTrackingConfiguration 的 worldAlignment 为 .gravityAndHeading。

    首先我们来看一下 WorldAlignment 类型:

    /**

    Enum constants for indicating the world alignment.

    */

    @available(iOS 11.0, *)

    public enum WorldAlignment : Int {

            

        /** Aligns the world with gravity that is defined by vector (0, -1, 0). */

        case gravity

            

        /** Aligns the world with gravity that is defined by the vector (0, -1, 0)

         and heading (w.r.t. True North) that is given by the vector (0, 0, -1). */

        case gravityAndHeading

            

        /** Aligns the world with the camera’s orientation. */

        case camera

    }

  • .gravity:只有重力,也就是坐标系的垂直方向和真实世界一致,但是水平方向不定。

  • .gravityAndHeading:重力和指北,垂直和水平方向都和真实世界一致。

  • .camera:摄像头方向,也就是坐标系和手机保持一致。

  • 所以这就是我们选择 .gravityAndHeading 的原因。这样一来,根据路径找人的那个人就只需要找到路径起始点的真实位置即可,手机的方向就不重要了,极大降低的使用门槛。

    提供路径起始点图片

    这个优化的内容是:分享路径的时候提供一张起始点的照片。这样拿到路径的人就拿图片和自己所在的场景做一个大致的比对来确定分享路径的人当时的位置,看一下使用效果:

    Find me - 用 ARKit 找人

    这张照片我们直接用 ARKit 的 sceneView.session.currentFrame 属性获取,如下:

    if let currentFrame = sceneView.session.currentFrame, let image = UIImage(pixelBuffer: currentFrame.capturedImage, context:CIContext()) {

    }

    使用建议

    基于上面说的情况,如果你在日常生活中确实想使用 Find me,下面是一些非常重要的使用建议

  • 如果在记录或者寻路过程中,有新消息,千万不要切出去看,毕竟商场内导航也就是 10 分钟左右的事情,正常晚 10 分钟回复消息也没事。

  • 寻路的人一定要找准路径初始点!非常重要!这个直接决定了路径终点位置的准确度。

  • 其他一些比较有趣的技术点

    路径中的箭头实现

    先看一下效果:

    Find me - 用 ARKit 找人

    这个技术点包含两个部分:

  • 怎么绘制箭头?

  • 如果让箭头指向下一个点的位置?

  • 怎么绘制箭头?

    首先,我们生成一个如下图形状的 UIBezierPath:

    Find me - 用 ARKit 找人

    代码如下:

    private static func vertexCoordinates() -> [CGPoint] {

        return [CGPoint(x: 0, y: 0),

                CGPoint(x: 20, y: 0),

                CGPoint(x: 20, y: 10),

                CGPoint(x: 10, y: 10),

                CGPoint(x: 10, y: 20),

                CGPoint(x: 0, y: 20)

        ]

    }

        

    private static func arrowPath() -> UIBezierPath {

        let path = UIBezierPath()

        let points = NodeUtil.vertexCoordinates()

            

        var count = 0

        for point in points {

            if 0 == count {

                path.move(to: point)

            } else {

                path.addLine(to: point)

            }

            count += 1

        }

            

        path.close()

            

        return path

    }

    然后用下面的代码生成 SCNNode:

    let path = NodeUtil.arrowPath()

    let shape = SCNShape(path: path, extrusionDepth: 2)

    let node = SCNNode(geometry: shape)

    这样一个箭头节点就生成了。

    如果让箭头指向下一个点的位置?

    其实就是要把这个箭头旋转一定的角度。这里涉及到一个数学知识:根据两个点算它们的连线与某个坐标轴的角度。我数学不好,原理就不讲了,主要讲一下 SceneKit 中如何旋转 SCNNode:

    首先我们需要两个点,当前点和上一次的点,所以我们通过一个 SCNVector3 类型的 last 变量来记录上一次的点,SCNVector3 包含 x、y、z 三个属性,分别对应了 x、y、z 轴的值。

    然后根据当前点和上一次点的位置得到角度,用 SCNAction.rotateBy 来生成一个旋转动作,再使用 SCNNode 的 runAction 方法来执行这个动作即可。

    最终代码如下:

    node.runAction(SCNAction.rotateBy(x: CGFloat(Float.pi/2.0*3.0), y:CGFloat(Float.pi/4.0+atan2(current.x-last.x, current.z-last.z)), z: 0.0, duration: 0.0))

    文件分享的一个坑

    Find me 目前分享路径采用的方式是文件分享,分享出去采用的是 UIDocumentInteractionController,接受别人分享的路径主要通过 func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool 回调获取,这里它会通过 url 参数返回分享文件的路径,这里有个点,如果你直接打开这个文件,会发现这个文件并不存在…

    解决方法比较有趣:通过这个路径把文件拷贝到另外一个路径,在打开这个文件就可以了。

    总结

    ARKit 做为 Apple 新发布的框架,后期一定会进行更深入的优化,所以 Find me 路径的准确度未来还是很值得期待的,当然我也会对 Find me 做持续的改进,欢迎大家关注。

    另外个人感觉 ARKit 框架的学习门槛确实不高,主要门槛反而在 SceneKit 或者 SpriteKit 上,大家有兴趣也可以看看张嘉夫大佬的一些教程,质量很高。

    Find me - 用 ARKit 找人