C++
Mac
AppleScript
Xtion
3Dセンサー

【はじめての3Dセンサー】Keynote制御するアプリを作る

More than 1 year has passed since last update.

はじめに

この記事の内容

MacBookAir(モデル:MacBookAir6,2 macOS:10.12.6)にて、Xtionを使った簡易アプリ作成時の自分用メモ。(2017/08現在)

あらすじ

偶然に「XTION PRO LIVE」(箱も説明もないむきだしの本体のみ)を入手。何も知らない状態から、スタートし、手探りでこれを動かしてみる企画。
前回の調査で、OpenNI/NiTEの基本的な使い方を学習したので、今回はいよいよアプリを作ってみる。

対象読者

OpenNI/NiTEの知識ゼロ(に近い)人には多少役に立つかも。

モーションセンサーによるスライド制御アプリの作成

アプリ概要

プレゼンを行う際にマウスを使わずに、Xtionを使って、人の動きによってスライド(Keynote)を操作することができる簡易アプリ(KeynoteController)。
想定している使い方としては、事前にプレゼンに使う資料を開いた状態でKeynoteを起動しておく。XtionをPCに接続した状態で、アプリを起動するとスライドが全画面表示で開始され、特定のポーズを取ることでスライドを進めたり戻したりできる。

アプリを作るのに必要な知識

Macでスライド(Keynote)を制御すること。
調べていくと、どうやらAppleScriptを使ってMacの自動制御をやるのが王道というか近道のようだ。
osascriptというコマンドで、AppleScriptを実行することができ、オプション -e CODE を指定することでCODE部分のコードを実行する。
Keynoteの制御については、以下のサイトを参考に実装する。

今回はC++でコーディングしているので、チートなやり方で実装する。
※system()関数を使って、osascriptを呼ぶことでお茶を濁す。

全体フロー

  • 前提条件
    • 事前に対象となるスライドを開いた状態でKeynoteを起動しておく
    • スライドの出力先モニター及びXtionのセッティングもお忘れなく
  • 前処理
    1. ライブラリ(OpenNI/NiTE)の初期化
    2. デバイス(Xtion)への接続 (※複数のデバイスを同時に利用することが可能)
    3. 対象デバイスの利用するセンサーの初期化
    4. 使用するセンサー(RGBカメラ、Depthセンサーなど)用のストリームオブジェクトを作成
    5. センサーのパラメーターを設定
    6. センサーの開始
    7. スライドの開始
  • メインループ(アプリ本体機能)
    1. センサー情報の利用
      • センサーから取得したフレームを読み出し、ポーズを判定
      • 腕を組んだら、次のスライドに進める
      • PSIのポーズは一つ前のスライドに戻す
    2. その他
      • 割り込み処理(アプリの終了受付、アプリのその他の機能の実現など)
  • 後処理
    1. スライド制御の後始末
    2. センサーの停止
    3. デバイスの切断
    4. ライブラリの後始末

Keynote制御クラスの実装

スライドをコントロールするクラスを作成。参考実装はKeynote制御のみ。
将来的に抽象化して色々な制御ができるクラスに育てるかもしれないし、ファイルを指定したり色々と拡張するかもしれないし...(多分ないなぁ)

/**
 * スライドを管理するクラス。事前に利用するスライドを選択して、Keynoteを起動しておくこと。
 */
class SlideController
{

public:
    /**
     * デストラクター
     */
    ~SlideController()
    {
        stop();
    }

    /**
     * スライドを停止する
     */
    void stop()
    {
        DEBUG_MSG( "STOP KEYNOTE" );
        system( "osascript -e 'tell application \"Keynote\" \nstop the front document\n end tell'" );
    }

    /**
     * 前準備(起動しているKeynoteを選択し、一番上のスライドを開始)をする。
     */
    void initialize()
    {
        DEBUG_MSG( "ACTIVATE KEYNOTE" );
        system( "osascript -e 'tell application \"Keynote\" to activate'" );
        DEBUG_MSG( "START KEYNOTE" );
        system( "osascript -e 'tell application \"Keynote\" \nstart the front document from the first slide of the front document\n end tell'" );
    }

    /**
     * 次のスライドに進む。
     */
    void next()
    {
        DEBUG_MSG( "NEXT KEYNOTE" );
        system( "osascript -e 'tell application \"Keynote\" \nshow next\n end tell'" );
    }

    /**
     * 一つ前のスライドに戻る。
     */
    void previous()
    {
        DEBUG_MSG( "PREVIOUS KEYNOTE" );
        system( "osascript -e 'tell application \"Keynote\" \nshow previous\n end tell'" );
    }
};

これでOK。

NiTEを使ってメイン処理を実装

NiTEには、あらかじめ2つの学習済みのモーショントラッカーが付属しているのでそちらを利用して、スライドを進めたり、戻したりすることにする。

  • PSI(ギリシャ文字Ψのポーズ) ... 次のスライドに進める
  • CROSS_HANDS(腕を組む) ... 前のスライドに戻す

みたいに割り当てることで、簡単に実装できそう。
実際にコードを描いてみたところ、学習済みのモデルを使うことであっけないほど簡単にKeynoteのページ送りができた。

/**
 * キーノート制御アプリクラス
 */
class KeynoteControllerApp
{

public:
    ~KeynoteControllerApp()
    {
        slideController.stop();
    }

    void initialize()
    {
        userTracker.create();
        slideController.initialize();
    }

    void update()
    {
        nite::UserTrackerFrameRef userFrame;
        userTracker.readFrame( &userFrame );
        userFrame.getDepthFrame();
        const nite::UserId *pLabels = userFrame.getUserMap().getPixels();

        // ユーザーの取得及びトラッキング開始
        const nite::Array< nite::UserData > &users = userFrame.getUsers();
        DEBUG_MSG("UPDATE: user size:" << users.getSize());
        for ( int i = 0; i < users.getSize(); i++ ) {
            const nite::UserData &user = users[i];

            DEBUG_MSG("->idx:" << i << " isNew:" << user.isNew() << " isLost:" << user.isLost());

            if ( user.isNew() ) {
                // ポーズ検出開始
                userTracker.startPoseDetection( user.getId(), nite::POSE_PSI );
                userTracker.startPoseDetection( user.getId(), nite::POSE_CROSSED_HANDS );

            } else if ( !user.isLost() ) {
                checkPose( user );
            }
        }
    }


private:
    nite::UserTracker userTracker; /**< ユーザー検出トラッカー */
    SlideController slideController;

     /**
      * ポーズを確認する
      */
      void checkPose( const nite::UserData &user )
      {
          // PSI ---> next slide
          {
              const nite::PoseData &pose = user.getPose( nite::POSE_PSI );
              if ( pose.isEntered() ) {
                  DEBUG_MSG("---->PSI is entered");
                  slideController.next();
              } else if ( pose.isHeld() ) {
                  DEBUG_MSG("---->PSI is held");
              } else if ( pose.isExited() ) {
                  DEBUG_MSG("---->PSH is exited");
              }
          }

          // CrossHands ---> previous slide
          {
              const nite::PoseData &pose = user.getPose( nite::POSE_CROSSED_HANDS );
              if ( pose.isEntered() ) {
                  DEBUG_MSG("---->CrossHands is entered");
                  slideController.previous();
              } else if ( pose.isHeld() ) {
                  DEBUG_MSG("---->CrossHands is held");
              } else if ( pose.isExited() ) {
                  DEBUG_MSG("---->CrossHands is exited");
              }
          }
      }
};

ポーズが組み込みのものを使っているためイマイチ感はあるが、最低限やりたいことは叶えられたので、これをベースにネタを仕込むことにする。

付録

参照リンク