基于ARFoundation的图像追踪定位
1 绪言
在AR世界中,定位是一个非常重要的议题,在滴滴AR实景导航的使用场景中,其团队调研了各大主流定位方式的性能和适用场景:
①导航卫星接收机:包括中国的北斗,美国的GPS,欧洲的Galileo、俄罗斯的GLONASS等;
②内置传感器:包括加速度计、陀螺仪、磁力计、气压计、光线传感器、相机等;
③射频信号:包括Wi-Fi、蓝牙、蜂窝无线通信信号等。
除了卫星导航接收机外,这些传感器和射频信号都不是为定位而设置的,尽管如此,这些传感器还是为我们提供了很多的室内定位源。
主流的全球卫星导航系统(Global Navigation Satellite System,GNSS)目前虽然已经被大规模商业应用,在室外开阔环境下定位精度已能解决大部分定位需求,但该类信号无法覆盖室内,难以形成定位。
室内环境复杂,无线电波通常会受到障碍物的遮挡,发生反射、折射或散射,改变传播路径到达接收机,形成非视距(non line-of-sight,NLOS)传播。NLOS传播会使定位结果产生较大的偏差,严重影响定位精度。
经过充分的调研与对比,考虑到Wi-Fi匹配方式定位精度2~5米,而蓝牙iBeacon作用距离短且布设成本较高等问题,因此滴滴团队选择视觉定位技术,该方法基于相机交互的方法,定位精度可达亚米级,鲁棒性较好,且只需利用手机摄像头、成本较低,无需布设额外设备,而且随着近年视觉定位技术的不断优化迭代,其精度与鲁棒性已完全能满足室内定位的需求。
但基于图像点云的特征匹配获取手机定位的方式需要较为先进的算法支持,且需要预先扫描特征点云进行三维重建,虽然目前算法正在逐步优化迭代,但还未落地到真实的室内导航项目中。
因此本文想要提出一个较为笨拙的方法,可以利用原有的BIM三维模型,通过特定的地点标志进行现实场景与虚拟场景的方位匹配与姿态估计,这种方法类似于二维码的定位模式,需要构建多处特征地点,前期工作量较大,实时性也不够高,也希望后续能够继续学习、改进。(注:本文作者目前是一名AR开发小白,正在学习中,希望能和大佬们一起学习讨论,交流意见)
2 功能描述
1.图像追踪产生对应大小的图像预制体并能购贴合真实场景中的图像;
2.在场景中同步放置与真实场景中同样物理参数的图像,利用makecontentappearat函数,以及变换content placement offset组件的参数,使追踪到图像的相机姿态在虚拟场景中AR camera完全一致,完成虚拟场景与真实场景在特征图片处的匹配;
3.设置图像追踪开关按钮,打开后追踪图像并记录相机姿态信息,关闭后追踪状态消失,将相机姿态信息传递,将虚拟场景中的AR camera组件完全对应。
B站演示视频: 【基于AR Foundation的图像识别追踪定位】 https://www.bilibili.com/video/BV1N84y1x7mE/?vd_source=d74642e3dbc041726923f34de14a6e91
3 实现方法
1.追踪图片状态时,记录此时世界原点的坐标和相机旋转信息;
2.将世界原点坐标和相机旋转信息转化为tracked image的相对坐标系下的坐标和旋转;(为什么是世界原点而不是相机坐标:相机坐标和旋转信息不会发生改变,移动ARsessionOrigin、content placement offset时,相机旋转位置信息仍然保留初始位置)
3.这里提一个坑,AR Foundation中识别的tracked image对象的相对坐标系与unity中图片对象的相对坐标系y轴旋转信息相差180度,所以在考虑相对坐标系时,需要记录一个新的transform组件,将tracked image的transform赋予他,并以rotate(0,180,0,space.self)
4.通过得到的相对位置旋转信息,再以虚拟场景中对应的图像的相对坐标系为参考,转化为世界坐标系;
5.旋转信息要考虑初始状态下产生的旋转误差,因此要* Quaternion.Inverse(worldSpaceCanvasCamera.transform.rotation);
6.位置信息已在上面选用世界原点坐标将误差消除;
7.再通过makecontentappearat移动ARsessionorigin,在考虑对cocntent placement affset做对应变换(用makecontentappearat会影响AR session origin组件的旋转,效果一致);
AR SessionOrigin类中MakeContentAppearAt的函数表述:
public void MakeContentAppearAt(Transform content, Vector3 position) { if (content == null) throw new ArgumentNullException("content"); // Adjust the "point of interest" transform to account // for the actual position we want the content to appear at. contentOffsetTransform.position += transform.position - position; // The ARSessionOrigin's position needs to match the content's pivot. This is so // the entire ARSessionOrigin rotates around the content (so the impression is that // the content is rotating, not the rig). transform.position = content.position; }
8.最后再用按钮控制tracked image组件的可见性,达到关闭开启图像追踪功能,并绑定移动重置脚本;
4 场景布置
(1)虚拟场景中放置与真实世界相同位置、相同大小的图片模型(此处用的较为简略的模型代替演示);

(2)建立AR Foundation图像识别库,将物理大小设置为与真实图像大小一致;
(3)AR SessionOrigin基本设置(平面检测、点云识别、射线管理以及图像识别管理)

(4)AR SessionOrigin挂载图像识别追踪定位代码(contenttransform :图片中心的target transform,其余的transform组件为临时储存位置的transform)

5 效果展示
对于录制的视频观看并不能看出图像追踪定位的效果和误差,因此通过对视频的逐帧分析,将追踪定位功能生效的前一帧以及实现后的第一帧进行对比;
通过分析图片边缘的拟合程度来分析相机定位上的误差;
以及场景资源和现实资源姿态的匹配程度来分析相机姿态的误差;






从逐帧分析的结果可以看出,相机的姿态和定位拟合效果较好;
而图片的长宽是20cm,从图像边缘来看位置误差不会超过1cm,因此基本可以实现cm级别的图像追踪定位。
6 代码
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; public class makecontenttest : MonoBehaviour { private Camera worldSpaceCanvasCamera; private ARSessionOrigin m_aRSessionOrigin; public Transform contenttransform; public Transform temp1; public Transform temp2; public Transform temp3; private ARTrackedImageManager m_TrackedImageManager; public Text debugtext; // Start is called before the first frame update void Awake() { m_aRSessionOrigin = GetComponent<ARSessionOrigin>(); worldSpaceCanvasCamera = m_aRSessionOrigin.camera; m_TrackedImageManager = GetComponent<ARTrackedImageManager>(); } void OnEnable() { m_TrackedImageManager.trackedImagesChanged += OnTrackedImagesChanged; } void OnDisable() { m_TrackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged; } // Update is called once per frame void Update() { } void UpdateInfo(ARTrackedImage trackedImage) { // Set canvas camera var canvas = trackedImage.GetComponentInChildren<Canvas>(); canvas.worldCamera = worldSpaceCanvasCamera; if (trackedImage.trackingState != TrackingState.None) { trackedImage.transform.localScale = new Vector3(trackedImage.size.x, 1f, trackedImage.size.y); temp3.position = trackedImage.transform.position; temp3.rotation = trackedImage.transform.rotation; temp3.localScale = new Vector3(1, 1, 1); temp3.Rotate(0, 180, 0, Space.Self); temp1.position = m_aRSessionOrigin.transform.position ; //此时检测到平面的相机世界位置(考虑相机初始位移造成的影响,因此选取坐标原点,抵消ARcamera位移影响) temp1.position = objectconvert(temp1.position, temp3); //转化为相对于照片的相对位置 temp2.position = worldconvert(temp1.position , contenttransform); //以真实照片为基础,将旋转角转化为世界坐标系下 temp2.position = temp2.position - contenttransform.position; //再得到的世界位置 temp1.rotation = worldSpaceCanvasCamera.transform.rotation; //此时检测平面的相机世界旋转 temp1.rotation = Quaternion.Inverse(temp3.rotation) * temp1.rotation; //转化为相对于照片的相对旋转角 temp2.rotation = contenttransform.rotation * temp1.rotation; //以真实照片为基础,将旋转角转化为世界坐标系下 temp2.rotation = temp2.rotation * Quaternion.Inverse(worldSpaceCanvasCamera.transform.rotation); //启动后造成的相机旋转属性 //减去启动后造成的AR camera相机的旋转和位置效应是为了保证检测到图像,makecontentappearat后以此时的新姿态作为世界原点。 var text = canvas.GetComponentInChildren<Text>(); text.text = string.Format( "{0}\ntrackingState: {1}\nGUID: {2}\nReference size: {3} cm\nDetected size: {4} cm\nSessionOrigin rotation: {5}\nTrackedImage rotation: {6}", trackedImage.referenceImage.name, trackedImage.trackingState, trackedImage.referenceImage.guid, trackedImage.referenceImage.size * 100f, trackedImage.size * 100f, m_aRSessionOrigin.transform.eulerAngles, trackedImage.transform.localEulerAngles); Vector3 distance = temp3.position - worldSpaceCanvasCamera.transform.position; debugtext.text = "相机追踪的状态: " + trackedImage.trackingState + " 相对于照片旋转角: " + temp1.eulerAngles + " 距离照片距离" + distance.magnitude; } } void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs) { foreach (var trackedImage in eventArgs.added) { // Give the initial image a reasonable default scale trackedImage.transform.localScale = new Vector3(0.01f, 1f, 0.01f); UpdateInfo(trackedImage); } foreach (var trackedImage in eventArgs.updated) UpdateInfo(trackedImage); } public void teleport() { m_aRSessionOrigin.MakeContentAppearAt(contenttransform, -temp2.position); m_aRSessionOrigin.transform.GetChild(0).transform.Rotate(temp2.eulerAngles); } public void contentreecover() { m_aRSessionOrigin.transform.position = new Vector3(0, 0, 0); m_aRSessionOrigin.transform.GetChild(0).transform.position = new Vector3(0,0,0); m_aRSessionOrigin.transform.GetChild(0).transform.rotation = Quaternion.identity; } public Vector3 objectconvert(Vector3 worldposition,Transform localobject) { Vector3 objectposition = new Vector3(); objectposition = localobject.InverseTransformPoint(worldposition); return objectposition; } public Vector3 worldconvert(Vector3 localposition,Transform localobject) { Vector3 worldposition = new Vector3(); worldposition = localobject.TransformPoint(localposition); return worldposition; } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.XR.ARFoundation; public class toggleimagetrack : MonoBehaviour { // Start is called before the first frame update public Text m_TogglePlaneDetectionText; private ARTrackedImageManager mARTrackedImageManager; public Button button; public makecontenttest contentappearat; void Awake() { m_TogglePlaneDetectionText.text = "禁用图像跟踪"; mARTrackedImageManager = GetComponent<ARTrackedImageManager>(); button.onClick.AddListener(ToggleImageTracking); } #region 启用与禁用图像跟踪 public void ToggleImageTracking() { mARTrackedImageManager.enabled = !mARTrackedImageManager.enabled; string planeDetectionMessage = ""; if (mARTrackedImageManager.enabled) { planeDetectionMessage = "禁用图像跟踪"; SetAllImagesActive(true); contentappearat.contentreecover(); } else { planeDetectionMessage = "启用图像跟踪"; SetAllImagesActive(false); contentappearat.teleport(); } if (m_TogglePlaneDetectionText != null) m_TogglePlaneDetectionText.text = planeDetectionMessage; } void SetAllImagesActive(bool value) { foreach (var img in mARTrackedImageManager.trackables) img.gameObject.SetActive(value); } #endregion }
7 结尾
本文为小白的第一篇博客,其部分来源于学习过程中参考的内容,如有侵权请联系博主删除,其主要内容均为原创,欢迎转载

浙公网安备 33010602011771号