这篇文章我没翻译,因为我觉得他的例子一看便知,太清晰简单了(也是自己翻译的太慢,翻不动了~)。转载来自wyf2742的文章。
本文基于Leap SDK,对C++版本的应用程序sample进行解释。希望在通读全文之后,大家能够借助Leap设备获取跟踪数据。
纲要:
一、概述
二、创建一个Controller对象
三、创建类Listener的子集
四、获取一个帧数据
五、获取手势信息
六、运行程序
在Leap SDK文件夹下,请找到本文所需的文件,具体如下:
需要注意的是,我们可以在 LeapSDK/docs/API_Reference/annotated.html 下扎到Leap API的参考文献。
一、概述
在本概述中,Leap Motion Controller检测并跟踪人手和手指,Leap设备一次捕获一“帧”,我们的应用程序将通过Leap API来获取这一“帧”的数据。
应用程序sample说明了如何使用Leap API来监听frame事件以及如何获取每一帧中的人手、手指,其中frame事件由Leap设备来触发。应用程序是个简单的命令行程序,程序可以输出被检测人手和手指的信息。应用程序的代码包含在了文件Sample.cpp中。
应用程序sample使用了Leap API中很大部分的关键类,具体包括如下:
关于上述类的更多信息,请参阅Leap API的参考文献。
二、创建一个Controller对象
类Leap::Controller在Leap设备和应用程序之间提供了主要的接口,当我们创建了一个Controller对象后,Controller对象将连接PC上的Leap软件,然后通过Leap::Frame对象获取人手的跟踪数据。其中,我们可以通过实例化一个Controller对象、调用Controller::Frame函数,来获取Frame对象。
如果我们以后自己的应用程序中包含一个不断更新的循环或帧率,那我们可以调用Controller::frame作为更新程序的一部分;不然的话,我们就需要自己手动给controller对象绑定一个监听器。一旦读入有效的跟踪帧数据(其他Leap事件也可),controller对象将调用定义在Leap::Listener子类中的回调函数。
在本应用程序sample中,主函数main创建了一个Controller对象,并通过调用Controller::addListener函数,将Leap::Listener子类的一个实例(即对象)绑定到Controller对象上,代码如下:
int main() { // Create a sample listener and controller SampleListener listener; Controller controller; // Have the sample listener receive events from the controller controller.addListener(listener); // Keep this process running until Enter is pressed std::cout << "Press Enter to quit..." << std::endl; std::cin.get(); // Remove the sample listener when done controller.removeListener(listener); return 0; }
但仅有这一段是不能运行的,还需要要创建一个Leap::Listener的子类SampleListener。这个监听器的子类定义了一些回调函数,当Leap事件发生时或者跟踪的帧数据就绪时,controller对象可以来回调。
三、创建类Listener的子集
应用程序sample定义了一个Leap::Listener的子类SampleListener,其中集成了处理Leap事件的回调函数,这些事件包括:
1、onInit—— 一旦监听器监听的controller对象开始初始化时,立即触发;
2、onConnect—— 当controller对象连接Leap设备,并即将发送运动跟踪的帧数据时,立即触发;
3、onDisconnect—— 当controller对象与Leap设备断开(比如,从USB拔出Leap设备或关闭Leap软件),立即触发;
4、onExit—— 当监听器与controller对象分离时,将触发监听器;
5、onFrame—— 当获取到运功跟踪的帧数据时,立即触发;
对于3种生命周期事件的回调函数onInit、onDisconnect和onExit,为方便起见,在本应用程序sample中,把一句话作为标准输出。而对于onConnect和onFrame事件,监听器的回调函数做了稍多些的处理。当controller对象调用了回调函数onConnect,函数将识别所有的手势类型。当controller对象调用onFrame函数,函数将获取最新的运动跟踪帧数据,并把检测目标的信息作为标准输出。
四、获取一个帧数据
当Leap产生新的运功跟踪帧数据,controller会调用回调函数onFrame,我们可以通过调用函数Controller::frame()来获得对应的数据,其中函数的返回值就是最新的Frame对象(Controller对象的引用被作为参数传给了回调函数)。一个Frame对象包含一个ID、一个时间戳、包括手的对象的list列表(手的对象即Leap检测范围内实际存在的手)。
下面的代码是应用程序sample中函数onFrame的一部分,onFrame函数实现的功能有:从controller对象获取最新的Frame对象,并根据Frame对象检索人手的list列表,然后输出Frame的ID、时间戳、以及帧数据中人手的数量、手指数、工具的数量:
// Get the most recent frame and report some basic information const Frame frame = controller.frame(); std::cout << "Frame id: " << frame.id() << ", timestamp: " << frame.timestamp() << ", hands: " << frame.hands().count() << ", fingers: " << frame.fingers().count() << ", tools: " << frame.tools().count() << std::endl;
接下来,函数将检测list列表中的第一只人手:
if (!frame.hands().isEmpty()) { // Get the first hand const Hand hand = frame.hands()[0];
Hand类的一个对象包含ID、反映人手物理特征的属性、手指对象的list列表,而每个Figer对象又包含ID、反映手指物理特征的属性。
一旦检测到一只人手,程序将对手指进行判断,然后对表征手指位置的数据取平均值,最后输出手指的个数、手指的平均位置。
// Check if the hand has any fingers const FingerList fingers = hand.fingers(); if (!fingers.isEmpty()) { // Calculate the hand's average finger tip position Vector avgPos; for (int i = 0; i < fingers.count(); ++i) { avgPos += fingers[i].tipPosition(); } avgPos /= (float)fingers.count(); std::cout << "Hand has " << fingers.count() << " fingers, average finger tip position" << avgPos << std::endl; }
接下来,函数继续输出跟手掌弧度一致的球体半径、手掌心的空间位置:
// Get the hand's sphere radius and palm position std::cout << "Hand sphere radius: " << hand.sphereRadius() << " mm, palm position: " << hand.palmPosition() << std::endl;
最后,函数onFrame将调用函数Vector,并根据人手的法线向量和方向向量,来计算人手的倾斜度、旋转度、偏转角。其中,角的单位进行了由弧度转为角度的变换。
// Get the hand's normal vector and direction const Vector normal = hand.palmNormal(); const Vector direction = hand.direction(); // Calculate the hand's pitch, roll, and yaw angles std::cout << "Hand pitch: " << direction.pitch() * RAD_TO_DEG << " degrees, " << "roll: " << normal.roll() * RAD_TO_DEG << " degrees, " << "yaw: " << direction.yaw() * RAD_TO_DEG << " degrees" << std::endl << std::endl;
五、获取手势信息
为了从Leap设备获取手势,我们首先要启用感兴趣的手势识别类型。在controller对象连接Leap设备后(即isConnected为真值),我们可以随时启用手势识别。在本应用程序sample中,回调函数onConnect()通过调用enableGesture()函数,启用了所有的手势识别类型。其中,enableGesture()函数是由类Controller定义的。
void SampleListener::onConnect(const Controller& controller) { std::cout << "Connected" << std::endl; controller.enableGesture(Gesture::TYPE_CIRCLE); controller.enableGesture(Gesture::TYPE_KEY_TAP); controller.enableGesture(Gesture::TYPE_SCREEN_TAP); controller.enableGesture(Gesture::TYPE_SWIPE); }
Leap设备把代表识别动作模型的Gesture对象放到Frame对象中gestures的list列表里。在回调函数onFrame()中,应用程序sample循环读取gestures的list列表,并把每个手势的信息输出。整个操作是通过一个标准for循环和switch语句来实现的。
Gesture API使用的是一个Gesture基类,Gesture基类是依靠代表各种手势的类延伸出来的。gesture list列表中的对象是类Gesture的实例,所以我们必须要将Gesture的实例转换为对应子类的实例。在这里,类型转化是不支持的,不过每个子类的构造函数都提供了类型转化的功能。例如,一个代表旋转动作的Gesture实例可以用下面的语句转化为类CircleGesture的实例:
CircleGesture circle = CircleGesture(gesture);
如果要将一个Gesture实例转化为错误的子类类型,那么对应的构造函数将会返回一个无效的Gesture对象。
我们常常会用到,将当前帧的手势信息与前面的帧里对应的手势进行比较,例如,画圈的手势动作里有个进度属性,此属性用来表征手指已经画圈的次数。这是一个完整的过程,如果想在帧与帧之间获取这个进度,我们需要减去前一帧中手势进度值。在实际操作中,我们可以通过手势gesture的ID找到对应的帧。下面的代码就是用了这个方法由前帧导出相应的角(单位:弧度):
float sweptAngle = 0; if (circle.state() != Gesture::STATE_START) { CircleGesture previousUpdate = CircleGesture(controller.frame(1).gesture(circle.id())); sweptAngle = (circle.progress() - previousUpdate.progress()) * 2 * PI; }
整个手势识别循环检测的代码如下:
// Get gestures const GestureList gestures = frame.gestures(); for (int g = 0; g < gestures.count(); ++g) { Gesture gesture = gestures[g]; switch (gesture.type()) { case Gesture::TYPE_CIRCLE: { CircleGesture circle = gesture; std::string clockwiseness; if (circle.pointable().direction().angleTo(circle.normal()) <= PI/4) { clockwiseness = "clockwise"; } else { clockwiseness = "counterclockwise"; } // Calculate angle swept since last frame float sweptAngle = 0; if (circle.state() != Gesture::STATE_START) { CircleGesture previousUpdate = CircleGesture(controller.frame(1).gesture(circle.id())); sweptAngle = (circle.progress() - previousUpdate.progress()) * 2 * PI; } std::cout << "Circle id: " << gesture.id() << ", state: " << gesture.state() << ", progress: " << circle.progress() << ", radius: " << circle.radius() << ", angle " << sweptAngle * RAD_TO_DEG << ", " << clockwiseness << std::endl; break; } case Gesture::TYPE_SWIPE: { SwipeGesture swipe = gesture; std::cout << "Swipe id: " << gesture.id() << ", state: " << gesture.state() << ", direction: " << swipe.direction() << ", speed: " << swipe.speed() << std::endl; break; } case Gesture::TYPE_KEY_TAP: { KeyTapGesture tap = gesture; std::cout << "Key Tap id: " << gesture.id() << ", state: " << gesture.state() << ", position: " << tap.position() << ", direction: " << tap.direction()<< std::endl; break; } case Gesture::TYPE_SCREEN_TAP: { ScreenTapGesture screentap = gesture; std::cout << "Screen Tap id: " << gesture.id() << ", state: " << gesture.state() << ", position: " << screentap.position() << ", direction: " << screentap.direction()<< std::endl; break; } default: std::cout << "Unknown gesture type." << std::endl; break; } }
六、运行程序
为了运行应用程序,我们需要以下步骤:
1、编译、链接应用程序sample;
windows下,依靠Leap.h、LeapMath.h(两者在SDK的include文件目录下)和Leap.lib(32位系统下,文件在lib\x86文件目录下;64位的在lib\x64目录下)
注意:在debug模式下编译应用程序,连接Leapd.lib库文件
2、将Leap设备通过数据线连接电脑后,放在自己的前面;
3、打开Leap软件;
4、运行应用程序sample;
windows下,请确保sampple.exe和Leap.dll在同一个文件目录下,或者Leap.dll在的动态库的搜索路径内;
运行后,我们将会发现,当程序初始化并连接到Leap设备后,会有Initialized和Connected字符的标准输出。我们还可以发现,Leap设备每次触发onFrame事件时都会显示帧数据的信息。当把手放到放到Leap设备上面时,我们同样会发现有手指和手掌的位置信息的输出。
现在,我们已经知道了如何通过Leap设备来获取运动跟踪数据,那么举一反三,下面就可以基于Leap设备开发自己的C++应用程序啦!