鉴于博客里有人留言,发现大家对Leap这一块的兴趣还是很高的,而且最近ZOL今年底的时候也给Leap分配了一定的篇幅来介绍,可见Leap体感(准确说是手势)识别的宣传及普及力度还是有的,至于Leap在未来应用领域的开拓上会有怎样的情况,我们就拭目以待吧。好,闲话不多说,今天翻译另一篇文章——Touch Emulation,模拟手指的触击动作。
Leap Motion提供了丰富的API,在我们自己开发的应用中,咱可以使用API的输出数据来识别触击动作。需要注意的是,在API中,有关触击的数据是由类Pointable提供的。
1、概述
为了识别触击动作,Leap设备虚拟出一个自适应的触摸平面,在这个虚拟出来的二维平面上,我们便可以实现数据交互了。虽然虚拟二维平面会根据用户手指及手的位置做调整,但在Leap设备的三维坐标系中,与X-Y面是基本平行的。当用户的手指或者使用工具向前移动时,Leap会判断Pointable对象是在靠近还是在触击虚拟二维平面。借助Leap API,当手指或工具触击到虚拟平面时,API会返回两个值:虚拟面区域的信息、手指与触摸面的距离。
(图1为虚拟触摸区域,0点左侧+1区域为悬停区,0点右侧-1区域为触击区)
Leap Motion借助触摸面,能将Pointable对象识别成这样几种情况:悬停在靠近触摸面的地方、触击触摸面、远离触摸面(触击方向有误时也会识别为远离)。三个区域分别称为:“悬停”、“触击”、“无信号”。注意,三个区域的转换常常是延迟于触击diatance的,这样的延迟是用来防止突然性和重复性的转换,所以当在我们自己开发的应用中,在实现触击交互时,我们可以不必过分考虑“触击”区域。
记住,只有当Pointable对象在悬停区和触击区的时候,触击distance才变为有效。distance是一个在[+1,-1]区间内规格化的数值,当Pointable对象第一次进入悬停区,触击distance为+1.0,随着Pointable对象靠近触击区域,distance的值也逐渐降为0值;当Pointable对象穿透了触摸面(图1中红色网络区域)进入触击区域,distance为0值,随着Pointable对象在触击区域中越来越深,distance值将逐渐变为-1。但注意了,distance值永远不会超出-1。
在开发应用时,我们可以根据区域信息来判断,是否需更新UI中与悬停和触击动作相关的UI元素。我们还可以根据distance信息表征的触摸面距离信息,进一步修改UI元素。例如,当我们把手指悬在Leap Controller设备的上方、且处在悬停区域时,咱可以把Leap Controller设备当前的聚焦状态显示出来,然后呢,根据distance信息在屏幕上显示对应的游标,作为手指离触摸面距离的反馈信息。
作为模拟触击API信息中的一部分,Leap Motion提供的Pointable对象位置信息不仅是规格化的,而且数据很稳定。Leap Motion软件内含自适应的滤波器,改滤波器能够对行为动作进行平滑化和平缓操作,以方便用户在屏幕的小块区域进行操作交互(如点击按钮、链接)。当用户的动作缓慢时,平滑化效果会更好,用户点击特殊点的效果会更好,同样,我们可以用这样的方法来做归零矫正。
2.获取触摸面
触摸面的信息可以通过Pointable类的touchZone属性来获取,触摸面是用Zone枚举产生的,其中包含以下状态信息:
NONE——此时,pointable对象要么离触摸面太远,要么就是触击方向有问题(比如触击方向为指向用户);
HOVERING——此时pointable对象指尖已经进入悬停区,但还没有触击;
TOUCHING——此时pointable对象已经穿过触摸面,进入触击区;
下面一段代码,描述最前端手指所在区域的检测信息:
Leap::Frame frame = leap.frame(); Leap::Pointable pointable = frame.pointables().frontmost(); Leap::Pointable::Zone zone = pointable.touchZone();
3.获取触击Distance
触击distance是通过Pointable类的touchDistance属性来获取的,distance随着手指在虚拟平面的靠近和触击,其范围在+1到-1区间内变化。distance信息没有一个实际的数量意义,它只是反映Leap Motion对pointable距离的检测效果。
下面一段代码,描述了最前端手指的触击distance:
Leap::Frame frame = leap.frame(); Leap::Pointable pointable = frame.pointables().frontmost(); float distance = pointable.touchDistance();
4.获取Pointable的稳定位置信息
Pointable对象的位置信息是通过Pointable类的stabilizedTipPosition属性,此位置信息是基于标准Leap Motion十字坐标来输出的,并且经过了上下文相关的滤波和稳定化处理。
下面一段代码,描述最前端手指的稳定位置信息:
Leap::Frame frame = leap.frame(); Leap::Pointable pointable = frame.pointables().frontmost(); Leap::Vector stabilizedPosition = pointable.stabilizedTipPosition();
5.Leap Motion设备坐标系和App坐标系的转换
当开发触击模拟相关app应用时,我们必须把Leap Motion设备的坐标系映射到app的屏幕空间中。为了方便映射,Leap Motion的API提供了InteractionBox类。InteractionBox描绘的是在Leap Motion视野范围内一个长方体容器,该类提供了一个函数,函数会把长方体空间内的位置映射到[0,1]规格化区间内,我们先将实际的位置信息规格化,然后根据app的尺寸产生最终的坐标,最后在app坐标系中得到一个点。
例如,如果我们在客户区内有一窗口,窗口大小由变量windowWidth、windowHeight确定。当我们手指触击的位置在客户区范围内的时候,我们可参考下面的代码,来得到触击点的二维坐标值:
Leap::Frame frame = leap.frame(); Leap::Finger finger = frame.fingers().frontmost(); Leap::Vector stabilizedPosition = finger.stabilizedTipPosition(); Leap::InteractionBox iBox = leap.frame().interactionBox(); Leap::Vector normalizedPosition = iBox.normalizePoint(stabilizedPosition); float x = normalizedPosition.x * windowWidth; float y = windowHeight - normalizedPosition.y * windowHeight;
6.触击案例
在下面的例子中,对于app应用窗口中检测到的所有Pointable对象,我们通过调用模拟触击的API,将pointable对象的位置显示出来。本例中,在触摸区域内,我们将触击点涂色,并根据触击distance来设置阿尔法值。指尖的稳定位置数据通过类InteractionBox来映射到app窗口:
本例中,触击的代码如下(本例通过调用库Cinder来创建app窗口并绘图):
#include "cinder/app/AppNative.h" #include "cinder/gl/gl.h" #include "Leap.h" #include "LeapMath.h" using namespace ci; using namespace ci::app; using namespace std; class TouchPointsApp : public AppNative { public: void setup(); void draw(); private: int windowWidth = 800; int windowHeight = 800; Leap::Controller leap; }; void TouchPointsApp::setup() { this->setWindowSize(windowWidth, windowHeight); this->setFrameRate(120); gl::enableAlphaBlending(); } void TouchPointsApp::draw() { gl::clear( Color( .97, .93, .79 ) ); Leap::PointableList pointables = leap.frame().pointables(); Leap::InteractionBox iBox = leap.frame().interactionBox(); for( int p = 0; p < pointables.count(); p++ ) { Leap::Pointable pointable = pointables[p]; Leap::Vector normalizedPosition = iBox.normalizePoint(pointable.stabilizedTipPosition()); float x = normalizedPosition.x * windowWidth; float y = windowHeight - normalizedPosition.y * windowHeight; if(pointable.touchDistance() > 0 && pointable.touchZone() != Leap::Pointable::Zone::ZONE_NONE) { gl::color(0, 1, 0, 1 - pointable.touchDistance()); } else if(pointable.touchDistance() <= 0) { gl::color(1, 0, 0, -pointable.touchDistance()); } else { gl::color(0, 0, 1, .05); } gl::drawSolidCircle(Vec2f(x,y), 40); } } CINDER_APP_NATIVE( TouchPointsApp, RendererGl )