Java Magazine 徽标
最初发表于 Java Magazine 2014 年 5 月/6 月刊。立即订阅

Leap Motion 和 JavaFX

作者:Johan Vos

2014 年 5 月发布

使用 3-D 手部运动与 JavaFX 应用交互。

迟早有一天,父母需要向孩子们解释什么是键盘。孩子们将很难理解父母为什么要使用这么奇怪的设备数十年。虽然那一天尚未到来,我们已经见到了比键盘、鼠标或触控板输入更直观、更自然的设备。这些设备不一定会替代现有的输入设备,但可以是极好的补充。

很重要的一点是,我们现在编写和使用的软件不要限于一组有限的输入设备。JavaFX 平台可以轻松与新输入设备集成。本文介绍如何在 JavaFX 应用中将 Leap Motion 控制器设备用作用户输入设备。

Leap Motion 控制器是 Leap Motion 公司生产的一款小型设备,配备了红外摄像头和红外 LED。该设备能够跟踪手和手指在设备周围半径约 1 米的平截头棱锥体中的运动。该设备使用一根 USB 电缆连接到计算机。Leap Motion 为 Windows、Linux 和 Mac 系统提供了原生驱动程序。基于这些驱动程序提供了一些 API,适用于不同的编程语言。这些 API 让开发人员能够创建利用 Leap Motion 控制器的应用。

您知道吗?

JavaFX 让您可以创建布局和输入设备之间高度独立的客户端应用。
幸运的是,有一个 Java API。现在,我们将探讨如何在 Java 客户端应用中使用 Java API 与 Leap Motion 控制器交互,但首先将简要讨论一下 Leap Motion 控制器的功能。

要全面了解 Leap Motion 控制器,请参阅 Leap Motion 网站Leap Motion 开发人员网站

还可访问开发人员网站,下载 Leap Motion 软件开发工具包 (SDK) 创建应用。

使用 Leap Motion SDK

Leap Motion 提供了适用于 Windows、Mac OS 和 Linux 的 SDK。该 SDK 包含一些文件,其中有些是 Java 开发必需的,例如:

  • 一个名为 LeapJava.jar 的 Java 库。它提供 Java 应用调用与操作系统相关的原生库所需的 API。这些原生库与 Leap Motion 服务相连,而 Leap Motion 服务通过 USB 从 Leap Motion 控制器设备接收数据

要运行与 Leap Motion 控制器通信的 Java 应用,原生库需要位于原生库路径中。将系统属性 java.library.path 指向特定操作系统原生库的位置(例如,在 64 位系统上是 java -Djava.library.path=/path/to/LeapSDK/lib/x64),启动 Java 应用即可实现。

Java 库 LeapJava.jar 文件应位于类路径中。

Johan Vos 演示如何将 Leap Motion 控制器用于 JavaFX 应用。

Leap Motion 控制器概念

因为 Leap Motion 控制器能够在三个方向跟踪手和手指运动,检索到的数据将以三维右手笛卡儿坐标系形式获取。此坐标系的原点位于设备顶部中心。xyz 轴指向图 1 所示方向。

leapmotion-f1

图 1

从 Leap Motion 控制器获取的所有数据包含在 com.leapmotion.leap.Frame 类(LeapJava.jar 库的一部分)的实例中。根据不同的环境变量,该设备使用 30 Hz 到 200 Hz 之间的频率发送帧数据。

获取帧数据有两种途径,都需要操作一个 com.leapmotion.leap.Controller 类实例:

  • 轮询 Controller
  • Controller 注册一个 Listener,新数据可用时将收到通知

使用 JavaFX 时,第二种方法最直观。可以使用 Listener 上的回调函数更改应用数据模型中的属性,JavaFX 脉冲线程将确保所做的更改反映在用户界面中。因为脉冲事件每秒最多生成 60 次,当 Leap Motion 控制器提供的数据超过图形系统的处理能力时,UI 线程不会过载。

清单 1 中的代码显示如何在 JavaFX 应用中创建 Controller 和注册 Listener。此处介绍的 LeapListener 类扩展了 com.leapmotion.leap.Listener 类,如清单 2 所示。

public class LeapConcepts extends Application {
    Controller c;
    Listener l;

    public void start (Stage stage) {
         …. // set up the scene and stage
        c = new Controller();
        l = new LeapListener(this);
        c.addListener (l);
    }
}

清单 1

public class LeapListener extends Listener {

    @Override
    public void onConnect (Controller c) {
        … // device connected!
    }

    @Override
    public void onFrame (Controller c) {
        Frame frame = controller.frame();
    }
}

清单 2

发生生命周期事件并且有包含跟踪数据的帧时,将调用 Listener 类。只要 controller 实例连接到 Leap Motion 设备,就会调用 onConnect 方法。如果由于某种原因,连接中断,则调用 onDisconnect 方法。显然,只要有包含数据的新帧,就会调用 onFrame 方法。

可用数据

如上所述,从 Leap Motion 控制器接收的所有信息在 Frame 类的实例上都可用。本文将简要概述可用的数据。Leap Motion 网站上的 Java 文档包含此类及其他类的所有信息。

Leap Motion 控制器能够跟踪手、手指和工具(如铅笔)的位置、方向和运动。根据内部计算,这还让 Leap Motion 服务可以跟踪一些手势,如轻扫、轻点和画圈运动。

如果需要帧中被跟踪的手的信息,我们可以调用 frame.hands()。该方法返回一个 HandList,这是一个 Iterable,可用于获取一些跟踪的 Hand 实例。Hand 实例具有一些属性:例如,手上检测到的手掌或手指的位置、法线矢量和速度。

也可以调用 frame.fingers() 单独跟踪手指。此方法返回一个 FingerList,可用于获取有关检测到的手指的信息(如方向、位置和速度)。

因此,可调用 frame.gestures() 获取手势,将返回 GestureList。注意,仅当接到 Controller 指示时,才会将手势添加到帧数据。因此,最好在扩展 Listener 类的类的 onConnect 方法中启用手势跟踪,如清单 3 所示。

 @Override
    public void onConnect (Controller c) {
        c.enableGesture(Gesture.TYPE.TYPE_SWIPE);
        c.enableGesture(Gesture.TYPE.TYPE_KEY_TAP);
    }

清单 3

清单 3 中代码将确保在用户执行轻扫或点击手势时,将对应的信息添加到所提供的 Frame 实例中。

线程

JavaFX 平台和 Leap Motion 控制器软件对线程都有一些具体的要求。所幸,这些要求相当匹配。

编写 JavaFX 应用时,最好将视图与数据分开。即,定义用户界面组件后将其粘合在一起,但描述其行为(如圆的位置或文本域宽度)的数据保存在 JavaFX 属性中。这些属性可以根据需要随时修改,但这并不意味着每次更改属性时都要重绘用户界面。

但导致用户界面变化的属性更改需要在 JavaFX 应用线程上执行。

只要有新的 Frame 实例可用,Leap Motion 原生库就将创建一个新的 Java 线程,并将调用 Listener.onFrame()(即如果向 Controller 注册了 Listener 的话)。

创建新线程的速度在每秒 30 到 200 次之间。但事实是,仅当先前线程完成工作,即 Listener.onFrame() 方法返回后,才会创建新线程。这可以防止因线程创建的速度高于处理能力而导致线程泛滥的情况。

这两个系统组合可产生一种新方法,使用 Listener 上的 onFrame() 方法(直接或间接)更改随后呈现的 UI 组件的属性。其流程如图 2 中模式所示。

leapmotion-f2

图 2

在此流程中,使用 Platform.runLater() 方法更改 JavaFX 控件的属性来实现 Leap Motion Listener。这确保了只有 JavaFX 应用线程才能修改这些属性,满足了 JavaFX 平台的要求。

在实践中,尤其是在更复杂的应用中,在 Listener 实现与 JavaFX 控件之间添加一个步骤通常很有用。这样,可以更清晰地隔离 Leap Motion 界面与 JavaFX 应用。

运用直觉

只需少量代码即可将 Leap Motion 控制器与现有应用集成。确定响应手部动作的最佳方式既是巨大挑战也是巨大机会。直觉在此非常重要。
为简单起见,本文采用的方法是 Listener 实现使用 Platform.runLater() 方法直接修改 JavaFX 控件的属性。

我们将使用一个非常简单的示例演示此流程:我们将编写一个显示一个圆圈的 JavaFX 应用。圆的位置和半径由手部动作确定。可从此 Bitbucket 站点获取该示例的完整代码。

如本文开头所述,JavaFX 让您可以创建布局和输入设备之间高度独立的客户端应用。我们首先将创建一个独立于 Leap Motion 控制器的 JavaFX 应用。清单 4 的代码生成一个布局,中间有个圆。

public class LeapConcepts extends Application {
...
@Override
public void start (Stage primaryStage) {
        Circle circle = new Circle(20);
        circle.setFill(Color.GREEN);
        circle.translateXProperty().bind(centerX);
        circle.translateYProperty().bind(centerY);
        circle.radiusProperty().bind(radius);       
        StackPane root = new StackPane();
        root.getChildren().add(circle);
        Scene scene = new Scene(root, 300, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
}
…
}

清单 4

如此代码所示,位置(通过 translateXtranslateY 属性)和半径(通过 radius 属性)与 JavaFX 属性绑定。这些属性在 JavaFX 应用中声明,通过公有方法访问,如清单 5 所示。

private final DoubleProperty centerY = 
new SimpleDoubleProperty(0);
    private final DoubleProperty centerX = 
new SimpleDoubleProperty(0);
    private final DoubleProperty radius = 
new SimpleDoubleProperty(10);
    
    public DoubleProperty centerX() {
        return centerX;
    }
    
    public DoubleProperty centerY() {
        return centerY;
    }
    
    public DoubleProperty radius () {
        return radius;
    }

清单 5

到目前为止,此应用是非常静态的。它显示一个绿色的圆圏,有固定半径和固定位置。我们现在要编写一个监听 Leap Motion 控制器数据的类。为此,我们扩展了 Leap Motion Listener 类,如清单 6 所示。

public class MyLeapListener extends Listener {
   @Override
    public void onFrame(Controller controller) {
        Frame frame = controller.frame();
        HandList hands = frame.hands();
        if (!hands.isEmpty()) {
            Hand hand = hands.get(0);
            final float x = hand.palmPosition().getX();
            float y = hand.palmPosition().getY();
            float z = hand.palmPosition().getZ();
            Platform.runLater(() -> {
                app.centerX().set(x);
                app.centerY().set(z);
                app.radius().set(50.-y/5);
            });
        }
    }
}

清单 6

在该类中,我们重写了 onFrame() 方法。出现新的帧数据时,将调用此方法的实现。通过调用 Frame frame = controller.frame(); 获取帧数据,通过方法调用传递控制器实例。

清单 6 所示,可通过调用 HandList hands = frame .hands(); 轻松获取检测到的手。如果未检测到手,hands.isEmpty() 调用将返回 true,对此我们不做处理。如果至少检测到一只手,可通过调用 Hand hand = hands .get(0); 获取。通过调用 hand.palmPosition() 以三维矢量形式获取检测到的手的手掌位置。

我们将 Leap Motion 坐标系的 xz 坐标映射到 JavaFX 应用中创建的圆的 translateXtranslateY 属性。y 坐标映射到 radius 属性。

注:使用 Leap Motion API 会涉及到一些数学计算。您必须将 Leap Motion 控制器获取的坐标转换成屏幕上的像素。Leap Motion 控制器坐标用距离 Leap Motion 顶部中心的距离表示(单位是毫米)。

因为 onFrame 方法是在原生 Leap Motion 库创建的线程上调用的,因此无法直接更改 JavaFX 属性。我们只能使用 Platform.runLater() 模式将 JavaFX 属性的更改推入事件队列。

剩下只需创建 Listener 的实例并将其添加到一个 Controller 中。这在我们的应用类中完成,如清单 7 所示。

private com.leapmotion.leap.Controller c;
private com.leapmotion.leap.Listener listener;

@Override
public void start (Stage primaryStage) {
    …
    c = new Controller();
    listener = new MyLeapListener(this);
    c.addListener(listener);
}

清单 7

Listener 一样,Controller 也是在 JavaFX 应用线程上创建的。但这些方法会立即返回,不会造成用户界面冻结。为防止 controller 对象被垃圾回收机制回收,维持对该对象的引用很重要。

简单的地图应用

目前为止我们探讨的示例是非常基本的,但概括了将 Leap Motion 控制器数据与 JavaFX 应用集成时需采用的核心原则。

在此基础上有无限可能。我们将用一个简单的地图应用结束本文,它使用 Leap Motion 控制器而非鼠标导航。该地图应用的代码可从这里获取。图 3 显示该应用的屏幕截图。

leapmotion-f3

图 3

我们应用与上一示例中相同的核心原则,这意味着首先创建一个不依赖输入设备的地图应用。

我们创建一个 MapArea 实例充当存放 MapTile 实例的容器。MapTile 表示指定缩放级别的 256 x 256 像素的世界地图。这些图块取自 OpenStreetMap,这是制图员社区打造的一个开放数据源。将坐标及缩放级别映射到像素所需的计算不在本文讨论范围内。MapArea 类包含一个 loadTiles() 方法,此方法在需要时加载新的 MapTile 实例及其对应的地图图像。这有两个可能的原因:

  • 用户在铺展地图,显示一个新区域
  • 用户在更改缩放级别,应呈现更详细版本的地图图块

所有这些均可通过传统鼠标或触控板实现,但我们可以轻松地添加一个 Leap Motion Listener 实现同样的目的。我们扩展 Leap Motion Listener 类并实现 onFrame 方法,如清单 8 所示。

 @Override
    public void onFrame(Controller controller) {
        Frame frame = controller.frame();
        HandList handList = frame.hands();
        Iterator<Hand> handIterator = handList.iterator();
        while (handIterator.hasNext()) {
            Hand hand = handIterator.next();
            if (hand.isValid() && (hand.fingers().count() > 2)) {
                Vector palmPosition = hand.palmPosition();
                final float x = palmPosition.getX();
                final float z = palmPosition.getZ();
                final float y = palmPosition.getY();
                Platform.runLater(() -> {
                    if (Math.abs(x) > 10) {
                        area.moveX(x/10);
                    }
                    if (Math.abs(z) > 10) {
                        area.moveY(z/10);
                    }
                    if (Math.abs(y-150) > 20) {
                        area.moveZoom(y-150);
                    }
                });
            }
        }
    }

清单 8

在此代码中,我们再次检测手的位置。但这次添加一个额外的检查:仅当用户张开手时才移动或缩放。为此,我们询问检测到几根手指。手攥拳时,Leap Motion 控制器数不出手指数。只要数出至少两个手指,我们就认定手已经打开。

接下来,我们将按手的位置成比例地移动地图区域,缩放级别将按手的高度成比例地更改。

更多信息


 Leap Motion 网站

 Leap Motion 开发人员网站

 NightHacking 视频:就 JavaFX 和 Leap Motion 专访 Johan Vos

总结

如本文所示,只需少量代码即可将 Leap Motion 控制器与现有应用集成。确定响应手部动作的最佳方式既是巨大挑战也是巨大机会。直觉在此非常重要。有创意的开发人员可能会乐于试验这种很酷的设备。

JavaFX 平台提供了一个很棒的环境,可以将新输入设备与现有或新 Java 代码结合。

使用线程时需要特别小心,不过 JavaFX 线程模型允许将用户界面呈现与后台事件和计算处理完美分离。

使用 JavaFX,开发人员可以利用 JavaFX 控件提供的巨大图形潜力。整个 Java 平台可用于丰富应用,例如,为后端系统提供通信。


vos-headshot


Johan Vos
自 1995 年开始使用 Java。他是 LodgON 的创始人之一,为社交网络软件提供基于 Java 的解决方案。Vos 爱好嵌入式和企业开发,专注于使用 JavaFX 和 Java EE 的端到端 Java。