【Augmented Reality】增强现实中的光学透射式头盔显示器的标定进阶
前言
上次在“增强现实中的光学透射式头盔显示器的标定初步”一文中,我们讲到了基于视觉跟踪的光学透射式头盔显示系统的一般标定方法,即单点主动对齐算法(SPAAM),在分析完基本理论后,给出了编写软件的思路。但是对于大部分没有编程经验,或者编程基础比较弱的同学来说,还是会有一些困难。于是,本文将继续上篇博客的内容,介绍软件编写的详细流程。
上一篇相关博客地址是:http://blog.csdn.net/zzlyw/article/details/53215105
--------------------------------------------------------------------------------------
1 环境配置
软件需求:
(1)Windows 10(64-bit)
(2)MATLAB R2015b
(3)Unity 3D 5.4.1f1 64-bit
(4)Vuforia 6 SDK
硬件需求:
(1)PC机一台
(2)光学透射式头盔显示器一台
(3)微软高清摄像头
2 硬件设备的制作
首先,你需要有一个光学透射式头盔显示器,单目或者双目都可以。我用的是一个比较简陋的单目设备,分辨率是800*600。摄像头绑定到头盔显示器上。虽然我们叫头盔显示器,但是我用的其实只是一个镜片和一个微投影器件,然后用金属结构固定在了一起。但是原理都是相同的。如果已有一个现成的带有摄像头的商品级头盔显示器就更好了。
请确保你的摄像头可以正常连接电脑捕获图像,同时你的头盔显示器可以连接电脑并正常显示。
3 建立一个Unity工程
Unity3D是一个集成度很高的游戏开发引擎,但是它在科研中扮演的角色同样很重要。很多仿真都可以使用Unity来完成。
打开unity,新建一个工程叫做“HMD_tutorials”,并建立一个叫做“AR”的scene。导入Vuforia SDK,并且把ARCamera和ImageTarget拖到场景中,设置自己的要跟踪的标志板图案。如果对于Vuforia使用方法不熟悉,具体的使用方法可以参考另一篇教程:
地址是 http://blog.csdn.net/zzlyw/article/details/53215172
我使用的标志图案是这幅图:
你可以使用自己的图案,也可以直接把这幅图另存到你的本地计算机使用。建立好之后,在ImageTarget下设置一个子物体,这个子物体是一个名为CubeMarker的空物体,只需要有transform组件就够了。把它的位置拖动到和上述标志图案最中央那个十字叉重合,这样最中央的那个十字叉就可以作为我们标定时使用的marker了。
生成一个叫做EyeCamera的摄像机,作为ARCamera的子物体,然后生成一个脚本“GetCalibrationData.cs”挂载到EyeCamera物体上。
该脚本的代码如下:
using UnityEngine; using System.Collections; using System.IO; using System.Runtime.InteropServices; public class GetCalibrationData : MonoBehaviour { public Camera arCamera; //跟踪摄像机 public Camera renderCamera;//渲染摄像机,显示在眼前 public Texture2D cursor; //鼠标光标 public GameObject calibMarker;//标志点 public static bool trackState; //存储Vuforia跟踪状态 bool calibrating; //标志着是否正处于标定任务中 float[,] calibCameraCoord; //标志点在跟踪摄像机下的三维坐标 float[,] calibUV; //标志点在屏幕上的二维坐标 int calibCount;//存储当前已经获得的标定点数 const int CalibrationCount = 12; //总共需要的标定点数 float[,] ProjectionMatrix; //标定出的投影矩阵 //测试模块 public GameObject[] markerPosition; Vector2[] screenPos; bool displayResult = false; void Start() { Cursor.visible = false; trackState = false; calibrating = false; calibCameraCoord = new float[CalibrationCount, 3]; calibUV = new float[CalibrationCount, 2]; calibCount = 0; ProjectionMatrix = new float[3, 4]; readProjectionMatrix(); //读取投影矩阵 screenPos = new Vector2[markerPosition.Length]; } void Update() { //进行test1的实验 for (int i = 0; i < markerPosition.Length; i++) { Vector3 p = arCamera.transform.InverseTransformPoint(markerPosition[i].transform.position); //转换成右手坐标 p = new Vector3(p.x, p.y, p.z); float _u = ProjectionMatrix[0, 0] * p.x + ProjectionMatrix[0, 1] * p.y + ProjectionMatrix[0, 2] * p.z + ProjectionMatrix[0, 3]; float _v = ProjectionMatrix[1, 0] * p.x + ProjectionMatrix[1, 1] * p.y + ProjectionMatrix[1, 2] * p.z + ProjectionMatrix[1, 3]; float _w = ProjectionMatrix[2, 0] * p.x + ProjectionMatrix[2, 1] * p.y + ProjectionMatrix[2, 2] * p.z + ProjectionMatrix[2, 3]; if (_w != 0) { float u = _u / _w; float v = _v / _w; screenPos[i] = new Vector2(u, v); //直接得到目标点的屏幕坐标 } } // 按下C,选择是进行标定还是显示 if (Input.GetKeyDown(KeyCode.C)) { displayResult = !displayResult; } // 按下F12键进行标定 if (Input.GetKeyDown(KeyCode.F12)) { calibrating = true; calibCount = 0; Debug.Log("开始标定!"); Debug.Log("#########请标定calibCount:" + calibCount); } if (calibrating == true) { if (Input.GetMouseButtonDown(0) ) { if (calibCount < CalibrationCount) { //获取calibMarker在跟踪摄像机坐标系下的坐标 Vector3 temp = arCamera.transform.InverseTransformPoint(calibMarker.transform.position); //将获取的三维坐标存储到数组中 calibCameraCoord[calibCount, 0] = temp.x; calibCameraCoord[calibCount, 1] = temp.y; calibCameraCoord[calibCount, 2] = temp.z; //获取相应的二维图像点坐标 calibUV[calibCount, 0] = Input.mousePosition.x; calibUV[calibCount, 1] = Input.mousePosition.y; calibCount++; //基数增加 Debug.Log("#########请标定calibCount:" + calibCount); }else if (calibCount >= CalibrationCount) { Debug.Log("标定完成!"); calibrating = false; calibCount = 0; //输出标定点 CreateFile(Application.dataPath, "最新标定坐标.txt", "##########################"); CreateFile(Application.dataPath, "最新标定坐标.txt",System.DateTime.Now.ToLocalTime().ToString() ); CreateFile(Application.dataPath, "最新标定坐标.txt", "--------------------------"); for (int i = 0; i < CalibrationCount; i++) { CreateFile(Application.dataPath, "最新标定坐标.txt", calibCameraCoord[i, 0].ToString()+" "+calibCameraCoord[i, 1].ToString()+" "+calibCameraCoord[i, 2].ToString()); } CreateFile(Application.dataPath, "最新标定坐标.txt", "--------------------------"); for (int i = 0; i < CalibrationCount; i++) { CreateFile(Application.dataPath, "最新标定坐标.txt", calibUV[i, 0].ToString() + " " + calibUV[i, 1].ToString() ); } } } } } void OnGUI() { if (displayResult) { for (int i = 0; i < screenPos.Length; i++) { Rect rect0 = new Rect(screenPos[i].x - 25, Screen.height - screenPos[i].y - 25, 50, 50); GUI.DrawTexture(rect0, cursor); } } else { //绘制鼠标位置的十字叉丝 Vector3 msPos = Input.mousePosition; Rect _rect = new Rect(msPos.x - 25, Screen.height - msPos.y - 25, 50, 50); GUI.DrawTexture(_rect, cursor); //绘制当前跟踪状态 if (trackState == false) { GUI.color = Color.red; Rect rect0 = new Rect(300, 10, 300, 20); GUI.Label(rect0, "trackState:" + trackState); } else { GUI.color = Color.green; Rect rect0 = new Rect(300, 10, 300, 20); GUI.Label(rect0, "trackState:" + trackState); } //显示标定状态 if (calibrating == true) { GUI.color = Color.red; Rect rect0 = new Rect(50, 10, 300, 20); GUI.Label(rect0, "In calibration..."); Rect rect1 = new Rect(50, 30, 300, 40); GUI.Label(rect1, "Please select calibCount:" + calibCount); } else { GUI.color = Color.red; Rect rect0 = new Rect(50, 10, 300, 40); GUI.Label(rect0, "Not in Calibration!"); } } } //读取txt void readProjectionMatrix() { FileStream fs = new FileStream("ProjectionMatrix.txt", FileMode.Open); StreamReader sr = new StreamReader(fs, System.Text.UnicodeEncoding.Default); string str; for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { str = sr.ReadLine(); if (str != null) { float result; result = float.Parse(str); ProjectionMatrix[i, j] = (result); } } } fs.Close(); sr.Close(); } void CreateFile(string path, string name, string info) { StreamWriter sw; FileInfo t = new FileInfo(path + "//" + name); if (!t.Exists) { sw = t.CreateText(); } else { sw = t.AppendText(); } sw.WriteLine(info); sw.Close(); sw.Dispose(); } }
为了能够实时监测跟踪状态,我们需要做下面的操作:
在工程中,找到并打开DefaultTrackableEventHandler.cs脚本,将“GetCalibrationData.trackState= true;”加入到OnTrackingFound()函数中,将“GetCalibrationData.trackState = false;”加入到OnTrackingLost()函数中。这样,trackState变量就可以监测系统跟踪状态了。
在ImageTarget下建立四个空物体,分别将它们的位置拖动到标志图案的某个十字处,以备测试使用。将场景中的物体与GetCalibrationData.cs脚本的相应变量绑定,如下图:
其中脚本中的TrackingCamera变量绑定的是场景中ARCamera下的Camera物体,RenderCamera变量绑定的是场景中ARCamera下的EyeCamera物体,Cursor绑定的是一张十字叉丝的PNG图像(该图像需要自己制作),CalibMarker绑定场景中的ImageTarget下的CubeMarker物体。MarkerPosition有4个元素,对应ImageTarget下的其他四个物体。对于EyeCamera物体,还有一些东西需要设置。点击EyeCamera,在其Inspector面板中会有Camera组件如下图。将ClearFlags设置为“SolidColor”,Background设置为纯黑色。Depth值应超过场景中所有其他摄像机。
在运行程序前,请转到工程的根目录,创建一个叫做“ProjectionMatrix.txt”的文件,里面随意写上12行数字,比如12个零。这是因为程序会从这个地方读取投影矩阵,没有该文件会出问题。然后点击unity正上方的小三角按钮,在Game窗口中会显示为纯黑色背景,以及一些提示信息。
将Game窗口拖到已经连接在电脑上的头盔显示器界面上全屏显示,效果如下:
按F12,开始标定。移动标志板,使其在空间中离散地进行位置变化,鼠标每次点击到最中央的那个十字叉上,尽量使得屏幕上的点位置也要分散开。大概收集12组对应点,程序会自动将收集到的点信息写到Assets目录下一个叫做“最新标定坐标.txt”的文件中。
得到的内容格式大概是这样的:
4 使用MATLAB计算标定数据
新建一个MATLAB脚本“SVD.m”,将下面的代码拷贝进去。
clc clear format long; A = load('world.txt'); F = load('screen.txt'); [m1 n1] = size(A); B=[0 0 0 0 ]; F=[F(:,1:2) ones(m1,1)]'; F=[F(1:2,:)]'; A = [A ones(m1,1)]; [m2 n2] = size(F); if m1~=m2 disp('xyz and uv are not the same in number of rows.'); return; end C=[ A(1,:) B -F(1,1)*A(1,:) B A(1,:) -F(1,2)*A(1,:)]; for i= 2:m1 temp=[ A(i,:) B -F(i,1)*A(i,:) B A(i,:) -F(i,2)*A(i,:) ]; C=[C;temp]; end [U, S, V]=svd(C); target= V(:,12)
程序中调用了world.txt和screen.txt,所以我们还需要在MATLAB当前目录先生成两个TXT文件。将“最新标定坐标.txt”中的三列的数据拷贝到world.txt中,将两列的数据拷贝到screen.txt中。然后运行SVD.m,可以在输出窗口中看到target变量的值为12个浮点数。
然后将target的值拷贝到unity工程根目录下的ProjectionMatrix.txt中,然后再次运行unity程序。此时,程序已经可以正常将标定结果显示了,你需要按C键将程序切换为显示标定结果模式。把刚才使用的标定板放在你的眼前,相应的四个测试用的CubeMarker(1~4)所在的位置会绘制蓝色十字。
好了,但目前头盔已经标定完成并且成功地显示了标定结果。
---------------------------------------------------------------------------------------------------------
小结
标定只是第一步,后面可以做的东西可以多到令人发指的地步。只要有创意,一切皆有可能。这个标定是光学透射式增强现实所特有的,对于视频透射式增强现实设备,不需要这些步骤,但是视频透射式设备永远不能带给最贴近真实环境的AR体验,因为它提供的全部都是视频流。而光学透射式增强现实可以使用户看到真实的环境,而非拍摄后再呈现出来的视频影像。后续的系列文章可能也会涉及到视频透射式增强现实,希望可以和大家分享。