【苉雅篇】Leap Motion 向左走,Processing 向右走
本文主要回答以下问题:
- Leap Motion 是什么
- Leap Motion 怎么玩
- 开发 Leap Motion 要掌握的常识
- Processing 怎么结合 Leap Motion
- 如何动手开发两者结合的第一个程序
周末,邻居罗斯带给苉雅一件很奇怪的小物件。
罗斯神秘兮兮地说道,这是可以触摸到未来的魔法!
The Future is in Your Hands
苉雅看着罗斯正对着电脑胡乱地挥动着双手,感觉十分滑稽。罗斯说,他只不过是在用 Leap Motion 操控电脑。
苉雅:Leap Motion?
罗斯:Leap Motion 是现在世界上最强的虚拟和增强现实运动控制技术的创造者!该系统可以在高精确度和高跟踪帧率下,检测并跟踪手、手指和类似手指的工具。
苉雅:……
罗斯:The future is in your hands.
Play It
罗斯:要想玩转 Leap Motion,我们首先需要在电脑上安装 Leap Motion 的 SDK。
苉雅:那要怎么做呢?
Leap Motion 的安装步骤:
在 Leap Motion 的官网开发者界面上注册一个账号,然后下载 SDK。解压,安装。
最后运行 Leap Motion App Home,程序将会自动进入新手教程,另外还有一些 APP 等你尝试。
那么接下来,舞动你的双手,让我们一起摇!
注意:Leap Motion的可视范围是一个倒金字塔,塔尖在设备中心。
基础知识
很快,苉雅就迷上了这个小巧而强大的方块。罗斯告诉苉雅,Leap Motion 和 Processing 是能够结合起来的,但在正式开发前,她可能需要了解一些小知识(参看文章,点击这里):
Coordinate system
Leap Motion 的坐标系原点位于 Leap Motion 控制器镜面的中心。以这个中心平行设备向右,为 X 轴的正方向;垂直 X 轴指向玩家,为 Z 轴的正方向;垂直镜面向上,为 Y 轴的正方向,如下图所示:
也就是右手坐标系,下图方便大家记忆:
Leap Motion API 用以下单位测量物理量:
距离: 毫米
时间: 微秒(除非另有说明)
速度: 毫米/秒
角度: 弧度
Motion tracking data
倘若 Leap Motion 设备在其视野中追踪手、手指和工具,它将提供一组数据集(或者是帧,或者是数据)进行更新。每帧数据包含一个基本追踪数据列表,如手、手指和工具,也包括识别出的手势和描述场景中的运动因素。
【注释:帧是 Leap Motion 最根本的数据模型,可以把它理解为一个集合,其他的实体数据都存放在这个集合里,比如手、手指、手势等。】
当检测到手、手指、工具或手势时,Leap Motion 将为它分配一个唯一的 ID 指示符。只要这个实体一直存在于设备的可视范围内,这个 ID 指示符就保持不变【和 Kinect 的骨骼追踪的 ID 是一致的】。如果追踪目标丢失或者失而复得,Leap Motion 会分配一个新的 ID 指示符(软件无法知道手、手指是否和之前看到的一样)【也就是说,不包含手指识别啦,和 Kinect 的骨骼追踪在丢失后情况完全一致】。
图片:XBOX 360 Kinect Sensor
Hands
手模型提供关于手,或者其他被检测出来的手指或工具的坐标、特征和运动。
Leap Motion 使用人手的内部模型来提供预测性追踪,即使手部分不可见时也是如此。手模型总是提供五个手指的位置,但是当手的轮廓和其手指清晰可见时,其跟踪效果是最佳的。
Leap Motion 的接口函数尽可能多地提供关于一个手的信息。也就是说,Leap Motion 可能无法计算出在每帧下的所有手部信息。例如,当一个手握成一个拳头,它的手指则无法被 Leap Motion 看到,所以手指的信息就为空。
如果一个人的手或其他手状物体出现在视野范围内,则手部列表中可能会出现多于两只手的情况。但是,我建议在 Leap Motion 的视野中至多保留两只手,以获得最佳运动跟踪质量。
Arms
一个臂是骨状物体。数据集提供了一个臂的取向、长度、宽度和结束点。当肘部不在视野内时,Leap Motion 控制器根据过去的观察结果以及典型的人体比例来估计其位置。
Fingers
Leap Motion 可以提供手的每个手指的信息。如果全部或部分手指不可见,则基于最近的观察和手的解剖模型来估计手指特征。每个手指由类型名称标识。
手指由 Finger 类表示。
指尖位置和方向矢量提供指尖的位置和手指指向的大致方向
一个 Finger 对象提供了描述每个解剖手指骨的位置和定向的 Bone 对象。所有的手指都包含从基座到尖端排列的四根骨头。
骨骼被识别为:
- 掌骨 - 将手指连接到手腕上的骨头(拇指除外)
- 近节指骨 - 手指根部的骨骼,与手掌相连
- 中节指骨 - 指尖和基部之间的手指中间骨
- 远节指骨 - 手指末端的终端骨骼
这种拇指模型与标准解剖命名系统不太匹配。真正的拇指比其他手指少一根骨头。然而,为了便于编程,Leap Motion 拇指模型包括零长度的掌骨,以便拇指具有与其他手指相同索引的相同数量的骨骼。因此,拇指的解剖掌骨被标记为近节指骨并且解剖近侧指骨被标记为 Leap Motion 手指骨模型中的中节节骨。
Tool
Leap Motion 会把细长笔直的圆柱形物体当作工具,比如铅笔。
相关类:Tool,Tool 是 Pointable 类的子类。
Gesture
在 Leap Motion 的世界,除了记录我们手的坐标、方向的数据,还能根据我们每一个手指和工具的运动,识别出我们的某些手势,比如:画圆、挥手、按键、点击屏幕。
相关类:Gesture以及Gesture的子类:CircleGesture、SwipeGesture、KeyTapGesture、ScreenTapGesture。
Sensor Images
随着计算出的跟踪数据,您可以从 Leap Motion 相机中获取原始传感器图像。
具有叠加校准点的原始传感器图像
Leap Motion For Processing
Leap Motion for Processing 是一个用于结合 Processing 和 Leap Motion 的库,由 Darius Morawiec 开发,倘若对遇到的问题已然束手无策,你可以发邮件给他。
Installation
- 方法一:自动安装——
Sketch > Import library … > Add library … > Filter: “Leap Motion”
- 方法二:手动安装,关于 Processing 的库的安装我之前已经写好了教程,请参看文章《二虎尽食之 Processing 添加库、模式、工具》。
在使用这个库编程时,请保证 Leap Motion 已经可以在你的电脑上运行且处于开启状态。至于版本匹配问题,请到这里查看。
Usage
(1)Basics
以下示例展示了基本数据访问:
import de.voidplus.leapmotion.*; //导入库
// ======================================================
// 目录:
// ├─ 1. 回调
// ├─ 2. 手
// ├─ 3. 手臂
// ├─ 4. 手指
// ├─ 5. 骨
// ├─ 6. 工具
// └─ 7. 设备
// ======================================================
//声明LeapMotion的对象
LeapMotion leap;
void setup() {
size(800, 500);
background(255);
// ...
leap = new LeapMotion(this);
}
// ======================================================
// 1. 回调
void leapOnInit() {
// println("Leap Motion 初始化");
}
void leapOnConnect() {
// println("Leap Motion 连接");
}
void leapOnFrame() {
// println("Leap Motion 帧");
}
void leapOnDisconnect() {
// println("Leap Motion 未连接");
}
void leapOnExit() {
// println("Leap Motion 退出");
}
void draw() {
background(255);
// ...
//获取帧频
int fps = leap.getFrameRate();
for (Hand hand : leap.getHands ()) {
// ==================================================
// 2. 手
int handId = hand.getId();
PVector handPosition = hand.getPosition();
PVector handStabilized = hand.getStabilizedPosition();
PVector handDirection = hand.getDirection();
PVector handDynamics = hand.getDynamics();
float handRoll = hand.getRoll();
float handPitch = hand.getPitch();
float handYaw = hand.getYaw();
boolean handIsLeft = hand.isLeft();
boolean handIsRight = hand.isRight();
float handGrab = hand.getGrabStrength();
float handPinch = hand.getPinchStrength();
float handTime = hand.getTimeVisible();
PVector spherePosition = hand.getSpherePosition();
float sphereRadius = hand.getSphereRadius();
// --------------------------------------------------
// 绘制
hand.draw();
// ==================================================
// 3. 手臂
if (hand.hasArm()) {
Arm arm = hand.getArm();
float armWidth = arm.getWidth();
PVector armWristPos = arm.getWristPosition();
PVector armElbowPos = arm.getElbowPosition();
}
// ==================================================
// 4. 手指
Finger fingerThumb = hand.getThumb();
// or hand.getFinger("thumb");
// or hand.getFinger(0);
Finger fingerIndex = hand.getIndexFinger();
// or hand.getFinger("index");
// or hand.getFinger(1);
Finger fingerMiddle = hand.getMiddleFinger();
// or hand.getFinger("middle");
// or hand.getFinger(2);
Finger fingerRing = hand.getRingFinger();
// or hand.getFinger("ring");
// or hand.getFinger(3);
Finger fingerPink = hand.getPinkyFinger();
// or hand.getFinger("pinky");
// or hand.getFinger(4);
for (Finger finger : hand.getFingers()) {
// or hand.getOutstretchedFingers();
// or hand.getOutstretchedFingersByAngle();
int fingerId = finger.getId();
PVector fingerPosition = finger.getPosition();
PVector fingerStabilized = finger.getStabilizedPosition();
PVector fingerVelocity = finger.getVelocity();
PVector fingerDirection = finger.getDirection();
float fingerTime = finger.getTimeVisible();
// ------------------------------------------------
// 绘制
// Drawing:
// finger.draw(); // Executes drawBones() and drawJoints()
// finger.drawBones();
// finger.drawJoints();
// ------------------------------------------------
// 选择
switch(finger.getType()) {
case 0:
// System.out.println("thumb");
break;
case 1:
// System.out.println("index");
break;
case 2:
// System.out.println("middle");
break;
case 3:
// System.out.println("ring");
break;
case 4:
// System.out.println("pinky");
break;
}
// ================================================
// 5. 骨
// --------
// https://developer.leapmotion.com/documentation/java/devguide/Leap_Overview.html#Layer_1
//骨末节
Bone boneDistal = finger.getDistalBone();
// or finger.get("distal");
// or finger.getBone(0);
Bone boneIntermediate = finger.getIntermediateBone();
// or finger.get("intermediate");
// or finger.getBone(1);
//骨近端
Bone boneProximal = finger.getProximalBone();
// or finger.get("proximal");
// or finger.getBone(2);
//骨掌骨
Bone boneMetacarpal = finger.getMetacarpalBone();
// or finger.get("metacarpal");
// or finger.getBone(3);
// ------------------------------------------------
// 触摸仿真
int touchZone = finger.getTouchZone();
float touchDistance = finger.getTouchDistance();
switch(touchZone) {
case -1: // 无
break;
case 0: //悬停
// println("Hovering (#" + fingerId + "): " + touchDistance);
break;
case 1: // 触摸
// println("Touching (#" + fingerId + ")");
break;
}
}
// ==================================================
// 6. 工具
for (Tool tool : hand.getTools()) {
int toolId = tool.getId();
PVector toolPosition = tool.getPosition();
PVector toolStabilized = tool.getStabilizedPosition();
PVector toolVelocity = tool.getVelocity();
PVector toolDirection = tool.getDirection();
float toolTime = tool.getTimeVisible();
// ------------------------------------------------
// 绘制:
// tool.draw();
// ------------------------------------------------
// 触摸仿真
int touchZone = tool.getTouchZone(); //触摸区域
float touchDistance = tool.getTouchDistance(); //触摸距离
switch(touchZone) {
case -1: // 无
break;
case 0: // 悬停
// println("Hovering (#" + toolId + "): " + touchDistance);
break;
case 1: // 触摸
// println("Touching (#" + toolId + ")");
break;
}
}
}
// ====================================================
// 7. 设备
for (Device device : leap.getDevices()) {
float deviceHorizontalViewAngle = device.getHorizontalViewAngle(); //设备水平视角
float deviceVericalViewAngle = device.getVerticalViewAngle(); //设备垂直视角
float deviceRange = device.getRange(); //设备范围
}
}
(2)Gestures
以下示例显示如何识别预定义的手势:
import de.voidplus.leapmotion.*;
// ======================================================
// 目录:
// ├─ 1. 滑动手势
// ├─ 2. 转圈手势
// ├─ 3. 屏幕点击手势
// └─ 4. 按键手势
// ======================================================
LeapMotion leap;
void setup() {
size(800, 500);
background(255);
// ...
leap = new LeapMotion(this).allowGestures(); // 所有手势
// leap = new LeapMotion(this).allowGestures("circle, swipe, screen_tap, key_tap");
// leap = new LeapMotion(this).allowGestures("swipe"); // Leap只检测滑动
}
void draw() {
background(255);
// ...
}
// ======================================================
// 1. 滑动手势
void leapOnSwipeGesture(SwipeGesture g, int state) {
int id = g.getId();
Finger finger = g.getFinger();
PVector position = g.getPosition();
PVector positionStart = g.getStartPosition();
PVector direction = g.getDirection();
float speed = g.getSpeed();
long duration = g.getDuration();
float durationSeconds = g.getDurationInSeconds();
switch(state) {
case 1: // 开始
break;
case 2: // 更新
break;
case 3: // 停止
println("滑动手势: " + id);
break;
}
}
// ======================================================
// 2.转圈手势
void leapOnCircleGesture(CircleGesture g, int state) {
int id = g.getId();
Finger finger = g.getFinger();
PVector positionCenter = g.getCenter();
float radius = g.getRadius();
float progress = g.getProgress();
long duration = g.getDuration();
float durationSeconds = g.getDurationInSeconds();
int direction = g.getDirection();
switch(state) {
case 1: // 开始
break;
case 2: // 更新
break;
case 3: // 停止
println("转圈手势: " + id);
break;
}
switch(direction) {
case 0: // 逆时针/左手
break;
case 1: // 顺时针/右手
break;
}
}
// ======================================================
// 3. 屏幕点击手势
void leapOnScreenTapGesture(ScreenTapGesture g) {
int id = g.getId();
Finger finger = g.getFinger();
PVector position = g.getPosition();
PVector direction = g.getDirection();
long duration = g.getDuration();
float durationSeconds = g.getDurationInSeconds();
println("屏幕点击手势: " + id);
}
// ======================================================
// 4. 按键手势
void leapOnKeyTapGesture(KeyTapGesture g) {
int id = g.getId();
Finger finger = g.getFinger();
PVector position = g.getPosition();
PVector direction = g.getDirection();
long duration = g.getDuration();
float durationSeconds = g.getDurationInSeconds();
println("按键手势: " + id);
}
(3)Camera-Images
此外,你可以访问原始相机图像:
import de.voidplus.leapmotion.*;
LeapMotion leap;
void setup() {
size(640, 480);
background(255);
leap = new LeapMotion(this);
}
void draw() {
background(255);
// ...
if (leap.hasImages()) {
for (Image camera : leap.getImages()) {
if (camera.isLeft()) {
// Left camera
image(camera, 0, 0);
} else {
// Right camera
image(camera, 0, camera.getHeight());
}
}
}
}
Creating a Simple Sketch
终于可以自己动手了,可怜的苉雅,看到长长的理论知识,忍不住打起了盹。
现在,我们使用 Leap Motion for Processing 库创建一个简单的草图来确保其正常运行吧:
- 创建一个新的草图。
- 在Processing菜单中,选择Sketch> Import Library>Leap Motion for Processing
import de.voidplus.leapmotion.*;
- 添加代码以创建一个 Controller 对象:
Controller controller = new Controller();
- 在草图draw()功能中,从控制器获取一帧跟踪数据:
Frame frame = controller.frame();
- 最后,通过添加以下代码来显示手和手指的数量:
text( frame.hands().count() + " Hands", 50, 50 );
text( frame.fingers().count() + " Fingers", 50, 100 );
合在一起,你的代码应该是这样的:
import de.voidplus.leapmotion.*;
Controller controller = new Controller();
void setup(){
size( 200, 200 );
}
void draw(){
background(0);
Frame frame = controller.frame();
text( frame.hands().count() + " Hands", 50, 50 );
text( frame.fingers().count() + " Fingers", 50, 100 );
}
Last…
倘若你遇到了无可描述的问题,欢迎和我一起探讨。