本文基于 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++ 应用程序啦!
来源: http://lib.csdn.net/article/vr/45562