坐标系统
在使用LeapMotion控制器控制一个程序时一个基本的工作就是将从控制器获得的坐标值转换到程序的坐标系统。
LeapMotion坐标系统
LeapMotion控制器在每一个数据帧中提供的坐标以毫米为单位。也就是说,如果一个手指指尖坐标是(x,y,z)=[100,100,-100],这些值都是以毫米记得,或者说,x=+10cm,y=10cm,z=-10cm。
在数据帧的坐标参考系中LeapMotion硬件是坐标中心。坐标原点在LeapMotion硬件的上表面的中心点。也就是说,如果你触碰LeapMotion控制器的中间点(同时能返回数据)那么你手指指尖的坐标将是[0,0,0]。
LeapMotion控制器使用右手坐标系。
通常情况下,LeapMotion放在桌子上,使用者在一边,电脑显示器在另一边,也就是说,使用者在控制器的前方(+Z)显示器在控制器的后方(-Z)。如果使用者开启了自动定位(automatic orientation),那么LeapMotion会在控制器反向放置(绿色LED灯背向使用者)自动适应坐标系统。但是,如果使用者将控制器放置在不同的位置上(颠倒或者侧着放),LeapMotion软件是无法检测或者适应(这样错误的放置)的。
设计你的程序时要鼓励使用者不要离LeapMotion控制器太近。快速的在控制器上方晃动手会增加手和手指在控制器的视野中相互干扰的几率。
将坐标转换到你的应用坐标系中
为了在你的程序中使用从LeapMotion设备中获得的信息,你必须预处理这些数据使其能为你的程序所用。比如,将坐标从leapMotion坐标系转换到你程序的坐标系下,你必须决定哪些坐标轴是有用的。利用多大的LeapMotion的视野,以及是使用绝对映射还是相对映射。
对于3D应用,通常会使用LeapMotion设备的所有三个坐标轴。对于2D应用,你往往可以忽略一个坐标轴,通常是Z轴,将两个坐标轴提供的坐标作为输入,映射到2D应用的坐标系中。
无论你使用两个还是三个坐标轴,你都应当决定在你的应用中使用多大的LeapMotion区域。LeapMotion的视域是一个倒置的金字塔。越接近控制器,X、Z轴的范围就会越小。如果你使用过宽的范围或者允许太低的高度,那么可能使使用者无法访问你程序界面底角。考虑下面这个例子,其将LeapMotion坐标系统映射到下方的矩形中。(这个例子需要插入LeapMotion控制器才能看到):
注意,当你尝试去访问程序界面的底部时,你的手指会超出LeapMotion设备的检测区域所以你没法将光标移动到那里。
你也应当去注意如何缩放LeapMotion坐标来适应你的程序。(以及在2D应用中每毫米使用多少像素)。缩放因子越大,对细微的运动就越灵敏。这会让使用者更容易移动光标。比如从程序界面的一边移动到另一边,但是更不容易精确定位。你必须为你的程序在速度与精确之间找到一个平衡点。
最后·,坐标系的不同可能需要反转一个甚至三个坐标轴。例如,许多2D窗口绘制API都将原点设置在窗口的左上方的角上,y轴正方向向下。LeapMotion的y值正方向是朝上的,所以你需要在转换坐标时翻转坐标轴。另一个例子,Unity 3D游戏开发系统使用左手系坐标系统。然而LeapMotion软件使用右手坐标系。
映射坐标系有点像摄氏度与华氏度的转换,你可以想象每个坐标轴就像相应的起点(零度)与终点(沸点)的缩放。
你可以使用如下公式来转换坐标:
其中:
通过改变起点与终,你能够改变坐标系的映射关系使得在你的程序中在一个更宽的范围或更小的范围检测运动。下面的例子在X轴与Y轴上使用这个公式将LeapMotion的可视区域映射到一个矩形显示区。通过设置不同的缩放,你可以玩耍这个控制器(译者注:........)。
如果你减小范围值,更小范围的物理运动会被映射到程序窗口的显示范围上,使它从窗口的一边移动到另一边更加容易。
X-Center用于抵消在缩放时x轴的中心向左或向右偏移。由于人手会很自然地放在LeapMotion控制器的左边或者右边,如果你基于那只手在使用来偏移坐标映射,会使用户更容易与全屏应用进行交互。
Y-floor滑块用于在LeapMotion Y轴上设置映射到显示区域底部区域的值。值越大代表着使用者需要把手抬得更高,同时也代表着使用者更容易接触到底部。
点选clamp选项是为了防止光标离开显示区域,这样能告诉使用者,哪里是能够进行交互的。
一个更普遍的用来转换坐标的方法就是将LeapMotion中的坐标归一化。然后从这个归一化的缩放变换到最终的程序坐标尺度中。事实上LeapMotion API中的 InteractionBox类会帮你将坐标归一化的。
交互区域
InteractionBox在LeapMotion可视区域定义了一个箱型区域。
只要用户的手或者手指处于这个箱型区域之中,那么也一定在LeapMotion的可视范围中。你可以在你的程序中利用这一点,通过将你程序的交互区域映射到InteractionBox所定义的区域来替代映射到整个LeapMotion的可视区域。InteractionBox类提供normalize_point()方法帮助将LeapMotion坐标映射到你的程序坐标中。
InteractionBox的大小由LeapMotion可视区域和用户的交互高度设置(在LeapMotion 控制面板中)。控制器软件基于保持底角在可视区域中来自适应的调整这个箱型区域的大小。如果你将交互高度的值设置的大,那么箱型区域也会变得更大。使用者可以通过在使用控制器时他最喜欢将手举得高度来设置这个交互高度的值。有些使用者更喜欢将手举得更高。通过使用InteractionBox映射LeapMotion坐标系到你的程序坐标系中,你能够满足所有用户的喜好。用户也能将交互高度设置为自动适应。如果用户在低于箱型区域移动手,控制器软件会自动将交互高度自动调小(直到最小的数值)。同样的,当用户手部的运动高于这个交互箱型区域时,控制器会抬高箱型区域的位置。
由于这个交互箱型区域会改变大小和位置,每一个Frame对象都包含有InteractionBox类的实例。下面的例子展示了InteractionBox的一些行为。在LeapMotion控制面板中改变交互高度设置,你会看到这会如何影响这个箱型区域的。
你可以使用在上个例子中使用的箝位控制(clamp control),你会看到当你使用normalize_point()函数时箝位坐标的影响。如果你不使用箝位,你会得到介于0与1之间的值。
因为交互箱型区域会随时间变化,要注意到当归一化是使用其他数据帧中的InteractionBox时,当前数据帧中的一个点的归一化坐标可能与现实中归一化坐标并不匹配。这样由用户在“空中所画的”直线和正圆可能不会那么直或者平滑,如果他们是使用其他的数据帧归一化。对于大多数应用,这不是个重要的问题,但是你应当在你不确定的情况下使用自动交互高度设置来测试你的程序。
为保证连续帧中一系列被追踪的点都能正确的归一化,你可以保留一个单独的InteractionBox对象----其具有最大的高度、宽度、深度。所有的点都用它来归一化。
使用交互箱型区域映射坐标
使用InteractionBox对象,首先将点坐标归一化然后将归一化后的坐标转换到你应用程序的坐标系。使用交互箱型区域将待转换坐标归一化(即取值范围[0...1])然后将坐标原点到显示区域的左下角。然后,你可以用你程序坐标系中每个坐标轴最大范围乘以归一化后的坐标,如果需要的话,再进行反相和平移。
映射到2D
比如,大多数2D绘制坐标系统将原点放置在窗口的左上角,当然也没有Z轴。将LeapMotion坐标转换到这样的2D坐标系统,你可以使用下面的代码:
app_width = 800
app_height = 600
pointable = frame.pointables.frontmost
if pointable.is_valid:
iBox = frame.interaction_box
leapPoint = pointable.stabilized_tip_position
normalizedPoint = iBox.normalize_point(leapPoint, False)
app_x = normalizedPoint.x * app_width
app_y = (1 - normalizedPoint.y) * app_height
#The z-coordinate is not used
注意,这个例子在转换Y轴坐标时在乘以最大高度前先用1减去了归一化坐标值(用于反相)
如果你在一个2D应用中使用Pointable对象或手掌的位置,可以考虑使用Pointable.stabilizedTipPosition或者Hand.stabilized_palm_postion。这些属性特别稳定而且经过滤波帮助用户与2D界面交互。对于3D应使用不稳定的Pointable.tipPosition和Hand.palm_position比较合适。
映射到3D
在映射到3D坐标系之前,你应当知道缩放因子,原点是否应当平移以及目标坐标系统是用左手系还是右手系。
3D图形的尺寸和大小的单位貌似是任意定义的,但是你需要决定如何将一个现实中的移动映射到虚拟世界中。
坐标系原点由交互箱型区域归一化到左下角。你可以通过将原点移动到更合适的地方来转换这个归一化的坐标系。
LeapMotion坐标系统使用右手系,如果你的坐标系统使用左手系,在归一化之前让Z轴坐标乘以-1(或者反转X轴和Y轴)。
下面的例子展示了将LEapMotion坐标系统转换到原点在底部中建的3D系统:
def leap_to_world(self, leap_point, iBox):
leap_point.z *= -1.0; #right-hand to left-hand rule
normalized = iBox.normalize_point(leap_point, False)
normalized = normalized + Leap.Vector(0.5, 0, 0.5); #recenter origin
return normalized * 100.0; #scale
采用不同的方式映射左手和右手
如果你的程序允许全屏交互。那么在归一化坐标系中为每只手定义不同的原点是个很好的主意。通过将左手的坐标原点移动到右边,右手的原点移动到左边,你实际上将每只手的自然休息点放置在了你程序的交互区域。对于左手来说可以缓解访问程序的右边时的疲劳,反之亦然。
为了移动每只手的原点,使用Hand.is_left检测那只手是左手(或右手)然后将归一化坐标平移到右边或者左边。
def differential_normalizer(self, leap_point, iBox, is_left, clamp):
normalized = iBox.normalize_point(leap_point, False)
offset = 0.25 if is_left else -0.25
normalized.x = normalized.x + offset
#clamp after offsetting
normalized.x = 0 if (clamp and normalized.x < 0) else normalized.x
normalized.x = 1 if (clamp and normalized.x > 1) else normalized.x
normalized.y = 0 if (clamp and normalized.y < 0) else normalized.y
normalized.y = 1 if (clamp and normalized.y > 1) else normalized.y
return normalized
注意:为使用这项技术,你必须将clamp参数设置为false(如果你想使用箝位,你可以在抵消x坐标后手动执行。)
增加灵敏度
将所有交互箱型区域的范围都映射到你程序的交互区并不经常是一个好主意。你可能想要在现实世界中微小的运动但在虚拟世界中(你的程序中)有很大的变化。你可以简单的按照你的需求缩放LeapMotion坐标而不是用归一化坐标。(如前所述)如果你仍需要InteractionBox带来的便利,你可以用比1大的数进行归一化。使用2,则会使灵敏度增加两倍,使用3则会增加3倍,等等。如果你的敏感度调的太高,那么会增加控制的难度并产生跳跃的现象。
你也可以做相反的事情(减小灵敏度),通过对归一化后的坐标乘以一个小于1得数,但是经常不会行得通。顾名思义,用户的动作的范围不会再你程序的整个区域中。所以,容易的方法是在你的程序坐标系中定义一个小一点的交互区域。
下面的例子使用2D坐标,将灵敏度增加1.5倍。
app_width = 800
app_height = 600
pointable = frame.pointables.frontmost
if pointable.is_valid:
iBox = frame.interaction_box
leapPoint = pointable.stabilized_tip_position
normalized_point = iBox.normalize_point(leapPoint, False)
normalized_point *= 1.5; #scale
normalized_point -= Leap.Vector(.25, .25, .25); # re-center
app_x = normalized_point.x * app_width
app_y = (1 - normalized_point.y) * app_height
#The z-coordinate is not used
你可能注意到,将clamp参数设置为true不再使你最后得到的坐标处于你定义的范围内。用户的运动会更加容易移出程序的交互区因为你有效的将一个箱型交互区域的子集(内部小空间)映射到了你的程序中。为用户提供视觉反馈是十分有用且必要的。