Thanks Driven Life

日々是感謝

肉体系スライドショーツール「Musclide」を作りました

これは JavaFX Advent Calendar 2012 10日目の記事となっております。

追記 いろいろあったので英訳 ver も書きました http://gongo.hatenablog.com/entry/2012/12/13/213720

経緯

去る11/24、JavaKueche で行われた JavaOne2012 報告会 & Java における HTML5 勉強会 に参加したのですが、その懇親会にて @ さんとお話しする機会がありました。

俺「少し前に ScalaFX とか少しだけ触ったことあります。」
寺田さん「そうなんですか!!ありがとうございます!!」
俺「え、一日しか触ったことな…」
寺田さん「ありがとうございます!!ありがとうございます!!」


そんなこんなで JavaFX をよくわかってない僕が「やってみた・作ってみた」記事で埋めようとした結果できたのが「Musclide」です。 パク……ネタ元は @ さんの JavaFX でプレゼンツール - JavaFX in the Box です!

上の記事で、skrb さんがこう仰っていました。

まずはじめに重要なのが、プレゼンツールとコンテンツは切り離して考えるべきということ。 コンテンツ側で表現しなくてはいけないことと、プレゼンツールで実装しなくてはいけないことをはっきりさせないといけません。

その通りだと思います。だから僕もこう言います。

コンテンツ側で表現しなくてはいけないことと、プレゼンツールで実装しなくてはいけないことと、己の肉体の動きをはっきりさせないといけません。

Musclide とは

Muscle + Slide です。その名の通り「肉体でスライド」するツールです。 ソースコードは公開してあります。 gongo/Musclide · GitHub

どういったものかは実際の動きを見ていただきたいと思います。

Musclide demo from gongo on Vimeo.

  1. 「Entering」ボタンを押すとプレイヤー(今回はスライドショーということで「Speaker」とします)の認識が始まります。
  2. 無事認識されると、Speaker の動きが棒人間に反映されます。
  3. 各種ポーズを取ることで、スライドの動きを制御できます。(画像用意するのめんどかったのでご勘弁)

     次(右)に進む
      \○/
       |
      / \

    先頭に戻る || | / \

    前(左)に戻る _ ○ _ | |  | / \

    最後に進む ○ -- | -- / \

  4. 00:53 からは実際のスライドを使ってのデモです。動きの確認用に使っていた領域をクリックして、スライドショーしたい画像が置かれてるディレクトリを指定します。

  5. 残りの時間、肉体派プレゼンとはどういったものかをご確認下さい。

実際に使うまでの流れ

0. 準備

  • Kinect
    • Xtion でもいいと思いますが、もってないので検証できてません。まあ大丈夫でしょう
  • 動ける空間
  • Java/JavaFX
    • 私は Java7u9 をインストールしたのですが、このバージョンになると JavaFX も一緒についてくるので 特に問題ない場合は 7u9 を入れちゃいましょう。

1. OpenNI/Sensor/NITE のインストール

Kinect を使ったシステムを作る場合、OpenNI あるいは Kinect for Windows SDK を使うのが主流だと思います。私が普段触っている開発環境は OS X なので、 OpenNI を使いました。

homebrew を使ってる方は totakke/openni-formula · GitHub を使うほうが手っ取り早いです。 MacPorts とか、あるいは Ubuntu 等の Linux の皆様は OpenNI-インストール - Kinect-wiki をご覧になればどうにかなります。がんばって!

2. 開発環境の準備

IntellijIDEA を使いました。理由は使ったことなかったからです。 あとはソースコードとってきたり、ビルド設定したり、がんばってください!!

せっかくなので私がひっかかった所を挙げます。

  • homebrew でデフォルトの場所 ( /usr/local ) じゃない所にインストールしている ( $HOME/.homebrew ) ため、 dylib や jar が上手く呼ばれなかった。そういう方々は

    • 実行時の VM パラメータで -Djava.library.pathorg.OpenNI.jar の場所を教えてあげたり
    • 環境変数(DYLD_FALLBACK_LIBRARY_PATH 等)で libOpenNI.dyliblibOpenNI.jni.dylib の場所を教えてあげたり

    すると上手くいくかもしれません。OpenNI のインストールは終わったのに、実行する時に "Failed: Can't create any node of the requested type!" って出るんだけど!って人は↑の可能性が高いです。

やってみたかったところと、やってみたこと

実は、Java じゃなく C++ で同じようなツールを作ったことがあります。 といっても、今回のように GUI から触ったわけではなく、 OS X の標準機能であるプレビューで PDF を表示したあと、 キーボードのイベントを体のうごきでシュミレートするってやつです。


Tython Lightning Talk

詳しくはこちらをお読みください。

この時作ったやつをだいたい流用したのですが、 せっかく JavaFX を使ったわけなので、JavaFX らしい(というか私が初めて触るもの)機能を使おうと思いました。

Property & Binding

当初はもうなんのこっちゃわからなかったんですが、使ううちになんとなくわかってきました。 と同時に、「これを使えば Speaker の動きをひたすら監視してくれるから、Speaker の描画簡単になるのでは?」と考え、 実際にできたのがこんなコードです。(長いのでリンクだけ)

「Speaker.java」で各部位のポイントを DoubleProperty として保持し、そいつを 「MuscleRader.java」が持つ Line の各プロパティに bind してあります。 こうすると、Speaker の動きがそのまま Line に反映されて棒人間の完成です。

Custom Event

さきほどの動画で紹介した、パンチを打って動かすver では

  1. 無限ループ
  2. ↑ の中で、人があるモーションを行ったかをチェック
  3. モーションが検出されたら「右」という処理を実行

みたいなことをしています。Musclide でも基本的には同じなんですが、 ただ同じにするだけでは面白くなく。

JavaFX みたいなイベント駆動のあれこれを構築した経験があまり無い私としては、 せっかくなので「『こういうポーズした』というイベントを発行する・捕まえる」という一連の流れを やってみようと思いました。そんなわけでそれ用のイベントを作ってみました。

package musclide.event;

import javafx.event.Event;
import javafx.event.EventType;


public class MuscleEvent extends Event {
    public static final EventType<MuscleEvent> ANY
            = new EventType<MuscleEvent>(Event.ANY, "Detect Muscle Posing");

    public enum Pose {
        NEXT,
        PREV,
        FIRST,
        LAST,
        STANDBY
    };

    public Pose pose = Pose.STANDBY;

    public MuscleEvent() {
        super(ANY);
    }
}

Musclide/src/musclide/event/MuscleEvent.java at master · gongo/Musclide · GitHub

特に複雑なことをしているわけではないんですが。むしろこれであってますか?

イベントを発行する部分は以下です

// あるメソッドの中
TimelineBuilder.create()
        .cycleCount(Timeline.INDEFINITE)
        .keyFrames(
                new KeyFrame(
                        new Duration(1000), // 1秒毎にポーズ確認
                        new EventHandler<ActionEvent>() {
                            @Override
                            public void handle(ActionEvent event) {
                                if (speaker == null || !speaker.isTracking()) {
                                    return;
                                }
 
                                if (speaker.owataPoseDetected()) {
                                    firePosingDetected(MuscleEvent.Pose.NEXT);
                                } else if (speaker.hangerPoseDetected()) {
                                    firePosingDetected(MuscleEvent.Pose.PREV);
                                } else if (speaker.showtimePoseDetected()) {
                                    firePosingDetected(MuscleEvent.Pose.FIRST);
                                } else if (speaker.kakashiPoseDetected()) {
                                    firePosingDetected(MuscleEvent.Pose.LAST);
                                } else {
                                    firePosingDetected(MuscleEvent.Pose.STANDBY);
                                }
                            }
                        }
                )
        ).build().play();

private void firePosingDetected(MuscleEvent.Pose pose) {
        MuscleEvent event = new MuscleEvent();
        event.pose = pose;
        Event.fireEvent(this, event);
}

public final void setOnPosingDetected(EventHandler<MuscleEvent> eventHandler) {
        this.addEventHandler(MuscleEvent.ANY, eventHandler);
}

Musclide/src/musclide/Ring.java at master · gongo/Musclide · GitHub

else-if 連射は心を痛めながら書きました。心を痛めながら書きました。

最後に、イベント検知はこちら

ring.setOnPosingDetected(new EventHandler<MuscleEvent>() {
    @Override
    public void handle(MuscleEvent muscleEvent) {
        switch (muscleEvent.pose) {
            case NEXT:
                next();
                break;
            case PREV:
                prev();
                break;
            case FIRST:
                first();
                break;
            case LAST:
                last();
                break;
            case STANDBY:
            default:
                standby();
        }
    }
});

Musclide/src/musclide/slide/Slide.java at master · gongo/Musclide · GitHub

これでしっかりと「オワタポーズをした」というイベントが発生→ next() が実行、ということができました。簡単!!

忘れてたけど OpenNI と会話してる部分

まとめ

がががっと説明したので不足分はたくさんあるんですが、いかがでしたでしょうか。あなたも肉体派になりませんか?