Leap Motion的工作原理简介
Leap motion controller 是Leap Motion出品的一款具备计算机视觉的手势识别设备。在其设备内部,有两个用于识别的球面摄像头,用于接收红外光的反射回馈;另有四个红外LED负责提供光源。当光源透过顶层的滤光片以后,被障碍物(例如人手)反射回摄像头,再经由内部集成的算法计算得出主要部位(如关节)的相关坐标数据。这些数据可以经由Leap Service转发给用户的应用,做进一步处理。Leap Motion最顶层的滤光片起到了很重要的作用,它可以过滤除波长在940纳米以外的其他杂波,只容许自带红外LED产生的红外光波进出,所以Leap Motion控制器并不需要其他的滤波器,就能得到一个去除掉复杂背景的红外成像。
图一 Leap Motion的结构
图二 Leap Motion的红外LED(绿色方框内)
Leap Motion的SDK
从Leap Motion的SDK角度,其对深度图像的构建和计算是基于一个由红外LED定义的识别区域,并在一个3D坐标系统内完成对手势的识别和跟踪。如下图:
图三 Leap motion的识别区域
由图三可见,其识别区域完全是依靠位于中间的两个和位于左右两边各一个的红外LED所构建。红外光线在透过滤光片时会发生一定角度的折射。所以,这个识别区域的范围外延并不是垂直于滤光片表面的,而是带有一定角度的。根据官方的文档,这个角度范围大致是左右两LED识别区域半径夹角150°;中间的两个红外LED识别半径夹角为120°。
图四 Leap Motion的坐标系
Leap Motion采用的右手定则坐标系,原点位于Leap Motion控制器上表面的中心。X轴和Z轴位于控制器的上表面, X轴与控制器的长边水平,Z轴和控制器的短边水平;Y轴垂直于控制器上表面,并且其坐标值距离设备上表面渐远而递增。Z轴则靠用户的距离越近,其坐标值越大。这个坐标系里的测量单位是毫米(millimeter),因此使用这个坐标系测量的数据可以近似地估算物体的大小和远近等属性。
当控制器在对处于识别范围内的物体(手或者手指)跟踪的时候,会输出一组实时刷新的数据帧。每一帧数据都包含被追踪对象(例如手)的细节信息,尤其是在物体运动时的属性。这样的数据帧构成了Leap Motion控制器应用的基础。
在Leap Motion SDK中,Hand模型定义了关于手的识别和跟踪信息,例如手的位置、手的跟踪特征、与手掌相连的手臂以及手指等众多信息。控制器的识别引擎在追踪的时候会自动识别这些信息,在使用SDK编程的时候,(Leap Service)自动填充这些对象的数据结构,从而用户可以读取这些数据结构,进行进一步处理。例如针对手掌,可以根据其相应的Hand对象的属性来判断左右手、手掌的方向,手掌的角度等等诸多属性。例如:
图五手掌心和手掌方向的示意图
创建Hand对象,并通过读取对象的属性来获得关于“手心”的“属性”。
Leap::HandList hands = frame.hands();
Leap::Hand firstHand = hands[0];
Leap::Vector handCenter = firstHand.palmPosition();
其返回值是一个Leap Motion SDK中的数据结构Vector(Vector定义为在LeapMotion空间中的一个三维点的坐标)。通过读取相关的数据结构,还可以得到手掌法线和手的指向向量。
Leap::Vector palmNormal = firstHand.palmNormal();
Leap::Vector palmDirection = firstHand.direction();
palmPosition()函数和direction()函数的返回值同样是结构体Vector。接下来,我们了解关于手指(Finger)的定义。在Leap Motion SDK中,Finger类对象可以装载控制器对每只手的每根手指(甚至每根手指上的每一个关节)的识别数据。如果整根手指或者部分手指不在识别区或者不可见,那么手指的特征数据则会根据左后一次可见时的数据和解剖学特征来估算。手指在SDK中时以拇指(Thumb)、食指(index)、中指(Middle)、无名指(ring)和小拇指(pinky)来加以区分。
图六手指方向的示意图
Finger_tipPosition()函数和Finger_direction()函数可以用来获得手指尖端的位置信息(Vectors)和手指的方向信息(Vectors)。
每一根手指的最小识别单位在Leap Motion看来,是每一个根小的关节,这些关节的识别对于手势的判断具有关键的作用。
图七远端、中间、近端指骨和掌骨示意图
关于Finger的所有的特征数据可以使用Bone对象来加载,在实际编程的时候可以使用Hands对象访问其中的fingers对象,逐层深入,最后到Bones对象。例如:
List
foreach(Fingerfinger in fingers){
Bone bone;
foreach (Bone.BoneTypeboneType in (Bone.BoneType[])Enum.GetValues(typeof(Bone.BoneType)))
{
bone = finger.Bone(boneType);
// ... Use bone
}
}
}
但有一个部位需要注意,在获取拇指掌骨的长度的时候,这个值永远为零(由于进化而出现的特殊现象)。以上例程,从Fingers开始,使用foreach进行枚举;在手指的foreach内部,再对每一个Finger上的bones进行枚举。因为每个手指上有4类指骨,因此内嵌的foreach循环4次,分别枚举每一根手指上的这四类指骨的名称。
bone = finger.Bone(boneType);
在了解完手、手指和指骨的SDK定义以后,我们分析(或者猜想)一下手势是如何定义的。最简单的手势,例如,“五指抓”可以在定义指尖的初始位置之后,判断动作结束时指尖的位置,通过两个位置的比较最终判断完成的动作是何种手势。另外通过加入时间参数,还可以判断手指尖移动的速度、甚至估算出手指捏合的力度大小。再例如,攥拳的手势,所有的手指都是向手掌的法向量移动,根据手指尖和法向量的位置变换可以判断出是否是攥拳的手势。由此可见,众多的手势判断都是和几个特殊的点之间的相对位置关系有关的,例如手掌心和手指尖的Vector坐标。
使用Helix 3D toolkit渲染3D模型文件
本文在介绍完Leap Motion的工作原理和其SDK基本应用之后,会通过一个手势控制3D模型的demo来具体介绍leap motion SDK编程的实际应用。文本不会对Helix 3D toolkit的使用做详细介绍,但打开一个3D模型(stl文件)也是整个demo的一部分,因此这里只给出部分代码作简单说明。关于Helix 3D的使用详尽文档,可以参考:http://docs.helix-toolkit.org/en/latest/wpf/getting-started.html#adding-a-viewport-control
使用Helix 3D toolkit可以分为如下大致的步骤:
添加“HelixToolkit.wpf”的NuGet安装包
按照官方文档,笔者用Visual Studio 2017社区版在使用NuGet安装Helix控件的时候,无法搜索到相关的内容,故只能Git其源码自行编译后使用。https://github.com/helix-toolkit/helix-toolkit.git
添加HelixViewPort3D的引用
下载完成之后,直接向工程中手动添加引用或者直接在XAML文件中的Windows节点下添加对Helix 3Dtoolkit的应用。
<Window x:Class="YourNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:HelixToolkit="clr-namespace:HelixToolkit.Wpf;assembly=HelixToolkit.Wpf"
xmlns:local="clr-namespace:YourNamespace"
mc:Ignorable="d"
Title="MainWindow"Height="350" Width="525">
添加3D内容的显示
首先在窗口中添加两个视口(由于是在VR头显中使用),并在视口中加载相同的3D内容,所以在MainViewModel的构造函数中添加:
…
this.viewport =viewport;
this.viewport2= viewport2;
…
并将打开文件对话框与相关的委托相对应
…
this.FileOpenCommand=newDelegateCommand(this.FileOpen);
this.FileExportCommand=newDelegateCommand(this.FileExport);
this.FileExitCommand=newDelegateCommand(FileExit);
…
因为Viewport和viewport2还没有加载任何内容,所以在窗口初始化的时候两个视口的内容还是空白的。
接下来打开STL文件并渲染
privateasyncvoid FileOpen()
{
this.CurrentModelPath =this.fileDialogService.OpenFileDialog("models",null,OpenFileFilter,".3ds");
this.CurrentModel=awaitthis.LoadAsync(this.CurrentModelPath,false);
this.ApplicationTitle =string.Format(TitleFormatString,this.CurrentModelPath);
this.viewport.ZoomExtents(500);
this.viewport2.ZoomExtents(500);
}
在加载3D模型数据的时候,采用了异步加载的方式,这有助于当3D模型文件体积巨大的时候,提供更好的用户体验。
privateasyncTask<Model3DGroup>LoadAsync(string model3DPath, bool freeze)
{
returnawaitTask.Factory.StartNew(()=>
{
var mi =newModelImporter();
if (freeze)
{
// Alt 1. - freeze themodel
returnmi.Load(model3DPath,null,true);
}
// Alt. 2 - create themodel on the UI dispatcher
returnmi.Load(model3DPath,this.dispatcher);
});
}
根据上述的代码片段可知,LoadAsync函数返回一个Modal3DGroup对象给CurrentModel,再由Helix 3D组件在XAML中完成渲染。
<helix:HelixViewport3D x:Name="view1"CameraRotationMode="Trackball" ModelUpDirection="0,1,0" BorderBrush="Black"BorderThickness="2" HorizontalContentAlignment="Stretch" MouseWheel="view1_MouseWheel"VerticalContentAlignment="Stretch" Width="432"CoordinateSystemHorizontalPosition="Stretch"CoordinateSystemVerticalPosition="Stretch">
<ModelVisual3D x:Name="root1" Content="{Binding CurrentModel}">
<helix:GridLinesVisual3D/>
<helix:DefaultLights/>
ModelVisual3D>
helix:HelixViewport3D>
最后的渲染效果如下图所示: