本文旨在介绍增强现实技术和应用,重点是分享笔者在学习和工作中遇到的一些问题和总结的一些经验,所以在介绍中会有不全面的论述,也免不了有一些疏漏,但是一定尽力做到最好。下面就进入主题,讲解一下增强现实中的光学透射式头盔显示器的标定技术。 一、什么是光学透射式头盔显示器,又是怎样应用到增强现实中呢? 头盔显示器,也叫头戴式显示器,顾名思义就是一种戴在用户头上使用的显示器。它的主要部件就是一个放置在人眼前面的显示装置,此外还有用于将显示器固定在头上的固定结构等。头盔显示器主要分为两大类,沉浸式头盔和透射式头盔。沉浸式头盔主要是虚拟现实头盔,用于构建沉浸式的虚拟现实环境,典型的设备如Oculus Rift、HTC Vive、暴风魔镜等。透射式头盔主要用于增强现实,也是我们要重点讲到的一类设备,典型的如Hololens、Magic Leap等等。在透射式头盔中,又分为两个小类,视频透射式头盔和光学透射式头盔。视频透射式头盔显示器是利用摄像头捕获场景的视频流,并将虚拟的信息叠加到视频流中,最后把加工后的视频流逐帧渲染在显示器上供用户观看。这种显示器就如同手机一样。而光学透射式头盔显示器(有的地方叫“光学透视型头盔显示器”,英文为“optical see-through head-mounted display”),有一个半透半反的光学系统,它一方面像普通眼镜一样可以透过外部的环境光,使用户可以看到眼前的真实世界,另一方面可以反射来自微型显示器的图像,叠加到人的视野中。 上图摘自一篇介绍光学透射式头盔显示器的论文,大致能表明这类显示器的基本结构。当然这款是自由曲面棱镜组成的,还有其他的类型如光波导镜片等。 之所以要应用光学透射式头盔显示器,是因为它可以将虚拟信息直接叠加到真实环境中,给人更加自然的交互体验,而不需要让人去看加工后的视频,如同看电视一般乏味无新意。 理想情况下,佩戴上头盔显示器后应该看到如下的效果。 上图截图自电影《钢铁侠》,主人公可以看到虚拟的物体叠加在空中。我们先不管电影中的效果可以用哪些现实手段实现(全息投影也好,真三维光学全息也罢),我们只考虑如何采用光学透射式增强现实来实现它。当然唯一不同的是,采用光学透射式头盔显示器以后,我们必须佩戴它才能从用户视角看到这些炫酷的效果。实际看到的可能是下面这样子的。该图选自一篇做人机交互的论文。 二、光学透射式头盔显示器的标定原理 (1)所谓标定,到底是标定什么,怎么进行? 首先,必须明确几个用到的坐标系统。世界坐标系使用W表示,跟踪摄像机(跟踪摄像机用来建立本系统与外部环境之间的联系)坐标系使用C表示,头盔显示器的像面是一个2D的平面,采用S表示其平面坐标系,人眼与头盔显示器的像面组成一个针孔模型的虚拟摄像机,坐标系使用V表示。如果读者没有计算机视觉基础,可以参考清华大学出版社出版的《计算机视觉:算法与应用》一书,或者自行百度,也可搜索到相关介绍。针孔模型介绍可参考博文 http://blog.csdn.net/lixianjun913/article/details/10032019。 标定主要是通过一些测量和计算,来确定一些参数。这里主要标定的是跟踪摄像机坐标系到头盔显示器屏幕坐标系的映射关系。 假设空间中的任意一点P,在世界坐标系W中的坐标为Pw,在跟踪摄像机坐标系下的坐标为Pc,通过屏幕看该点,该点在屏幕上的坐标为ps,则得到两个等式
其中,Pw,Pc是三维位置的齐次坐标,因此都是四维向量。Ps是二维位置的齐次坐标,因此是三维向量。每一个出现的R都是3*3的旋转矩阵,每个T都是3*1的平移向量,它是包含3个元素的列向量。K是表示包含人眼的虚拟摄像机的内参数矩阵,是3*3矩阵。令Ps = [u v 1]T,Pc = [xc yc zc 1]T,G=[g11 g12 g13 g14 ; g21 g22 g23 g24 ; g31 g32 g33 g34 ],则得到 所以我们要标定的就是G。有了G就有了从跟踪摄像机三维坐标系到显示器屏幕二维坐标的映射关系,进而可以在屏幕上对现实中的物体做虚拟信息标注。当然,G可以直接分解成内参数矩阵和旋转、平移矩阵(平移向量看成是n*1的矩阵),这样,我们不仅可以进行文字标注,还可以根据虚拟摄像机和跟踪摄像机之间的空间位置关系,进行三维虚拟物体的叠加。这也就是最后我们看到的在真实世界中叠加一些虚拟的人物、建筑等物体的效果了。 (2)软件怎么写? 了解了标定原理之后,你也许就迫不及待地要进行实验了。 其实要想求解上述的G,还是比较容易的。首先观察Ps=G Pc,左边是三维的,右边是四维的,很明显G应当是3*4的矩阵。对于这12个未知数(实际上只有11个独立的,剩余一个是尺度缩放因子,在齐次坐标系统中不影响最终结果),可以由至少6组对应点求解得到。因为每组对应点是可以得到两个独立的方程的。忘了说了,每组对应点指的是某一个头部位姿状态下,P点的跟踪摄像机坐标Pc和屏幕坐标Ps。 软件只需要获取不同的至少6个头部位姿下的6组对应点即可。在求解时,所有的未知数移动到等式左边,那么右边全是0,直接求解可能得到零解。因此我们对系数矩阵进行奇异值分解。假设取了n组对应点,则只需要把2n*12的矩阵使用matlab的SVD函数进行分解就行了。右奇异矩阵中对应最小特征值的向量就是要求的解。直接拷贝出来就是要求的G。 具体的程序代码还需要各位读者自己写写看,只有两个步骤哦,第一步读取一系列(不少于6组,建议10组以上)对应点位置坐标,第二步使用matlab的SVD函数进行求解。之后,得到G就能知道任意的三维位置对应的屏幕位置了。这时,你还不能在你想要的位置上叠加想要的虚拟信息吗? 看看,是不是很简单。其实很多事情都是看上去复杂难解,实则脉络清晰,求解容易。当然了,在实现的过程中还有很多小技巧。相信初次接触的朋友也会遇到很多难题。如果有问题可以在评论中留言进行交流。
具体实现:
【Augmented Reality】增强现实中的光学透射式头盔显示器的标定进阶
前言 上次在“增强现实中的光学透射式头盔显示器的标定初步”一文中,我们讲到了基于视觉跟踪的光学透射式头盔显示系统的一般标定方法,即单点主动对齐算法(SPAAM),在分析完基本理论后,给出了编写软件的思路。但是对于大部分没有编程经验,或者编程基础比较弱的同学来说,还是会有一些困难。于是,本文将继续上篇博客的内容,介绍软件编写的详细流程。 -------------------------------------------------------------------------------------- 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物体上。 csharp] view plain copy
- 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.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 = 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.x - 25, Screen.height - screenPos.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, "lease 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("rojectionMatrix.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”,将下面的代码拷贝进去。 [plain] view plain copy
- 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体验,因为它提供的全部都是视频流。而光学透射式增强现实可以使用户看到真实的环境,而非拍摄后再呈现出来的视频影像。后续的系列文章可能也会涉及到视频透射式增强现实,希望可以和大家分享。
|