[Processing][LeapMotion] 手軽に赤外線カメラアプリを作る

2015.03.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。小室です。 自宅からライブホールであるZepp Sapporoまで徒歩圏内なので、ライブ参戦活動が大変捗ってます。いえい。

LeapMotion

長らく放置していたLeapMotionなのですが、昨年末に参加したGlobal Game Jam 2015 SapporoでUnity、Oculus Riftと共にゲーム開発に利用したのをキッカケに少し触るようになりました。SDKがアップデートされ、諸々の機能強化がされたことも要因の一つですね。

長らく活躍の場がなかったLeapMotion殿。 IMG_20150323_003912

LeapMotionのカメラ画像

LeapMotionの中には2つの赤外線カメラが入っていることはご存知かと思います。150度程度の視野を持つカメラです。左右の微妙にずれる画像を解析して様々なジェスチャーや手の形などを認識しているようです。

実はこの赤外線カメラの情報は、画像としてSDKを通じて自由に取得することができます *1。今回は、この画像を使って超お手軽に赤外線カメラを作ることができることの紹介です。

Processingで赤外線カメラ映像を描画する

今回は、JavaのSDKとProcessingを使って赤外線画像を表示するアプリを作ります *2。自分の環境は以下のとおりです。

  • OS: Mac OSX 10.10.2
  • Processing: Processing 2.2.1
  • LeapMotion: LeapMotion SDK 2.2.3+25971

まずはProcessingの準備

まずProcessingはこちらから最新版をダウンロードしておいてください。

Processingは元々Javaのライブラリとの親和性が非常に高い上に、描画周りをJavaなど目じゃないくらい簡単にサクッと書けてしまうのでおすすめです。Processingの準備を行います。

leapmotion.pde

void setup() {
  size(640, 640);
  smooth();
  background(255);
}
void draw() {
}

これだけで、白の640x640の空の画面が作成されます。 スクリーンショット 2015-03-24 10.35.03

続いて、LeapMotionの設定を確認しましょう。

LeapMotionの設定を確認する

LeapMotionの設定を確認します。設定画面を開きます。

スクリーンショット 2015-03-23 0.54.03

設定の「全般」タブを開くと、「イメージを許可する」という項目があるので、そこにチェックが入っていることを確認します。

スクリーンショット 2015-03-23 0.55.09

これでLeapMotion側の設定は準備万端です。LeapMotionは接続しておくのが前提です。

ProcessingでLeapMotionへアクセスする

メニューのSketch>Import Library>Add Libraryを選択するとLibrary Managerが起動します。 スクリーンショット 2015-03-23 1.05.28

ここに「leap」と入力するといくつか候補が出ます。 スクリーンショット 2015-03-23 1.06.38

今回は、2番めに出てきている「LeapMotion」を利用します。自分の環境では既に導入されてしまっているので、こんな表示ですが、導入されていなければ「Install」というボタンが表示されるので、そちらを押してライブラリをインストールします。

LeapMotionのイベントを取得する

LeapMotionのライブラリを読み込みましょう。新規のSketchを作成します。Sketch>Import Library>LeapMotionを選択します。 スクリーンショット 2015-03-23 16.26.48

これを実行すると、以下がソースに追記されます。

import com.leapmotion.leap.processing.*;

LeapMotionのControllerをインスタンスとして宣言します。

import com.leapmotion.leap.*;

// LeapMotion Controller
Controller controller = new Controller();

LeapMotionのイベントを検出するためには、Listenerを継承したクラスを作成します。

class LeapListener extends Listener {

  public void onConnect(Controller controller) {
    // 接続が完了したら呼ばれる
    System.out.println("Connect!");
  }
  
  public void onFrame(Controller controller){
    // Frame毎に呼ばれる
  }
}

アニメーションと同じく1秒間に何度もFrameが更新されます。その際にこのonFrame()が実行されます。

LeapMotionではとんでもない数でFrameが更新されているので、このメソッドの中で、うかつにSystem.out.println("onFrame!");など記述しないようにしましょう。コンソール表示が大変なことになります。

このクラスをインスタンス化して、先ほど宣言したControllerクラスのインスタンスに設定します。

// LeapMotion Controller
Controller controller = new Controller();
// LeapMotion Controller Event Listener
LeapListener listener = new LeapListener();

void setup() {
  size(640, 640);
  smooth();
  background(255);
  
  // Listenerを設定
  controller.addListener(listener);
  controller.setPolicy(Controller.PolicyFlag.POLICY_IMAGES);
}

これでLeapMotionの情報をProcessing内に取り込む準備ができました。

LeapMotionの画像データを描画する

LeapMotionの画像を利用するには、こちらのドキュメントを参照します。若干不完全なソースなのでそのままコピペだとちょっと動きません。以下のように記述しなおします。

class LeapListener extends Listener {

  public void onConnect(Controller controller) {
  }
  
  public void onFrame(Controller controller){
   
    Frame frame = controller.frame();
    if(frame.isValid()){
      ImageList images = frame.images();
      
      Image leftImage = images.get(0);
      Image rightImage = images.get(1);
      PImage left = getGrayScaleImage(leftImage);
      PImage right = getGrayScaleImage(rightImage);
      image(left, 0, 0);
      image(right, 0, leftImage.height());
    }
  }
  
  public PImage getGrayScaleImage(Image image) {
    PImage camera = createImage(image.width(), image.height(), RGB);
    camera.loadPixels();
     
    //Get byte array containing the image data from Image object
    byte[] imageData = image.data();
    
    for(int i = 0; i < image.width() * image.height(); i++){
      int r = (imageData[i] & 0xFF) << 16;
      int g = (imageData[i] & 0xFF) << 8;
      int b = imageData[i] & 0xFF;
      camera.pixels[i] =  r | g | b;
    }
    
    //Show the image
    camera.updatePixels();
    return camera;
  }
}

frame.images();で、左右のカメラの画像を取得しています。 getGrayScaleImage()ではLeapMotionのImageクラスから、Processingで表示できるPImageクラスへ変換します。

画面上の(0, 0)を始点に0番目の画像を描画します。さらに0番目の画像の高さ分 y座標をずらして、1番目の画像を描画しています。これで2つのカメラの画像を描画することが出来ました。

全体のソースは以下のとおりです。

leapmotion.pde

import com.leapmotion.leap.*;

class LeapListener extends Listener {

  public void onConnect(Controller controller) {
  }
  
  public void onFrame(Controller controller){
   
    Frame frame = controller.frame();
    if(frame.isValid()){
      ImageList images = frame.images();
      
      Image leftImage = images.get(0);
      Image rightImage = images.get(1);
      PImage left = getGrayScaleImage(leftImage);
      PImage right = getGrayScaleImage(rightImage);
      image(left, 0, 0);
      image(right, 0, leftImage.height());
    }
  }
  
  public PImage getGrayScaleImage(Image image) {
    PImage camera = createImage(image.width(), image.height(), RGB);
    camera.loadPixels();
     
    //Get byte array containing the image data from Image object
    byte[] imageData = image.data();
    
    for(int i = 0; i < image.width() * image.height(); i++){
      int r = (imageData[i] & 0xFF) << 16;
      int g = (imageData[i] & 0xFF) << 8;
      int b = imageData[i] & 0xFF;
      camera.pixels[i] =  r | g | b;
    }
    
    //Show the image
    camera.updatePixels();
    return camera;
  }
}

// LeapMotion Controller
Controller controller = new Controller();
// LeapMotion Controller Event Listener
LeapListener listener = new LeapListener();

void setup() {
  size(640, 640);
  smooth();
  background(255);
  controller.addListener(listener);
  controller.setPolicy(Controller.PolicyFlag.POLICY_IMAGES);
}

void draw() {
}

実行結果

実行するとこんな感じのグレースケールの画像が描画され、リアルタイムで更新されていきます。 スクリーンショット 2015-03-23 1.11.45

上下で微妙に角度のずれた画像になっているのが確認できると思います。これは2つのカメラの差ですね。 さらに二次元画像にすると樽状に歪んでいるのが分かります。実際にきちんとした画像として見るためには、この歪みを修正してあげる必要があります。歪みの情報も実はLeapMotionのSDKから取得することができるのですが、今回は単にカメラの映像をそのまま利用して表示するのみに留めておきます *3

ちなみにこのカメラ、「赤外線カメラ」なので、暗闇の中でも同じような感じで見えます。暗闇の中懐中電灯などがない場合は効果を発揮しそうですね!

参照

脚注

  1. ただし、Javascriptを除く
  2. と言っても公式ドキュメントにほとんどコードが記述されてるんですけど。
  3. 歪みのところはまだ余り理解できていない・・・