OpenNIとKINECT for Windows SDKの大きな違いの一つに、「骨格追跡をするのにポーズが必要かどうか」があります。KINECT for Windows SDKではポーズが不要で、ユーザーを認識するとすぐさま骨格の検出も行えるという優位性がありました。
今日、OpenNIのMLを見ていたら2ヶ月ほど前にこんなやり取りがあったようです。
「キャリブレーションデータをファイルに保存する」
名前からして素敵ですね。
SaveCalibrationDataToFileと、対になるであろうLoadCalibrationDataFromFileを使えば、もしかしてポーズなしの骨格追跡ができるのではないか?と思って試したところ、無事に出来たのでまとめます。
これらを使ったサンプルがNiUserTrackerにあり、動作を確認できたので、KINECTセンサープログラミングのコードをベースに簡略化したものを載せます。
ちなみにデバイスはXtion LIVEでやってます。KINETは未確認です。
こんな感じ
実際にやってみると、こんな感じでユーザーの検出と同時に、骨格の追跡も始まるようにできました
一応、キャリブレーションデータの読み込みを外した状態での確認もしましたので、本処理を入れることで、「ポーズなしの骨格追跡ができる」と言えると思います。
コード
全体のコードはこちらです。KINECTセンサープログラミングのCalibrationをベースに、データの書き込みと読み込みを追加しました。
以降、ポイントとなるコードの解説です。
キャリブレーションデータの書き込み
void SaveCalibration( xn::UserGenerator& userGenerator ) { printf("SaveCalibration\n"); XnUserID aUserIDs[20] = {0}; XnUInt16 nUsers = 20; userGenerator.GetUsers( aUserIDs, nUsers ); for ( int i = 0; i < nUsers; ++i ) { // ユーザーがキャリブレーションされていたら、その情報を保存する if ( userGenerator.GetSkeletonCap().IsCalibrated(aUserIDs[i]) ) { printf("Save user's calibration to file: user %d\n", aUserIDs[i]); userGenerator.GetSkeletonCap().SaveCalibrationDataToFile(aUserIDs[i], XN_CALIBRATION_FILE_NAME); break; } } }
現在認識しているユーザーを取得し、その中でキャリブレーションが済んでいる(骨格追跡可能な状態)のユーザーのデータを、ファイルに出力しています。
キャリブレーションデータの書き込み場所
void XN_CALLBACK_TYPE CalibrationEnd(xn::SkeletonCapability& capability, XnUserID nId, XnBool bSuccess, void* pCookie) { xn::UserGenerator* user = (xn::UserGenerator*)pCookie; // キャリブレーション成功 if (bSuccess) { std::cout << "キャリブレーション成功。ユーザー:" << nId << std::endl; user->GetSkeletonCap().StartTracking(nId); // キャリブレーションデータを保存する SaveCalibration( *user ); } // キャリブレーション失敗 else { std::cout << "キャリブレーション失敗。ユーザー:" << nId << std::endl; } }
キャリブレーションデータの書き込みを呼び出すところです。
キャリブレーションデータの書き込みは、キャリブレーションが完了していることが条件なので、CalibrationEndコールバック内、キャリブレーションが成功したときに行うことにします。
キャリブレーションデータの読み込み
bool LoadCalibration( xn::UserGenerator& userGenerator ) { printf("LoadCalibration\n"); XnUserID aUserIDs[20] = {0}; XnUInt16 nUsers = 20; userGenerator.GetUsers(aUserIDs, nUsers); for (int i = 0; i < nUsers; ++i) { // ユーザーがキャリブレーションされているときは何もしない if (userGenerator.GetSkeletonCap().IsCalibrated(aUserIDs[i])) continue; if (userGenerator.GetSkeletonCap().IsCalibrating(aUserIDs[i])) continue; // ファイルから、キャリブレーション情報を取得し、骨格の追跡を開始する printf("Load user's calibration from file: user %d\n", aUserIDs[i]); XnStatus rc = userGenerator.GetSkeletonCap().LoadCalibrationDataFromFile(aUserIDs[i], XN_CALIBRATION_FILE_NAME); if (rc == XN_STATUS_OK) { printf("Make sure state is coherent: user %d\n", aUserIDs[i]); userGenerator.GetPoseDetectionCap().StopPoseDetection(aUserIDs[i]); userGenerator.GetSkeletonCap().StartTracking(aUserIDs[i]); return true; } break; } return false; }
次に、キャリブレーションデータの読み込みです。
保存と同じように、現在認識しているユーザーを取得します。
認識したユーザーから、キャリブレーション中および、キャリブレーション完了している状態のユーザーを除きます。残ったのは「ユーザー検出されたが、キャリブレーションされていない」ユーザーになり、そのユーザーに対して、保存したキャリブレーションデータを適用しています。
ファイルのロードが正常に終了したら、ポーズの検出を止め、そのまま骨格の追跡を開始します。
後述する、キャリブレーションデータの読み込み結果による分岐のために、トラッキングを開始できればtrue、開始できなければfalseを返すようにしました。
キャリブレーションデータの読み込み場所
void XN_CALLBACK_TYPE UserDetected(xn::UserGenerator& generator, XnUserID nId, void* pCookie) { std::cout << "ユーザー検出:" << nId << " " << generator.GetNumberOfUsers() << "人目" << std::endl; // キャリブレーションデータをロードする if ( !LoadCalibration( generator ) ) { // ファイルからトラッキングできなければ、ポーズからキャリブレーションを行う XnChar* pose = (XnChar*)pCookie; if (pose[0] != '\0') { generator.GetPoseDetectionCap().StartPoseDetection(pose, nId); } else { generator.GetSkeletonCap().RequestCalibration(nId, TRUE); } } }
トラッキングデータの読み込みは、「ユーザー検出されたが、キャリブレーションされていない」状態で行うので、ユーザーの検出直後に行います。これによって、ユーザーを検出して、キャリブレーションデータがあれば、そのまま骨格追跡状態になります。
もし、キャリブレーションデータからの骨格追跡ができなければ、通常のポーズからのキャリブレーションになります。
懸念事項など
調べてないですが、現時点で懸念事項がいくつかあります
- 人が変わっても大丈夫なのか?
- 同じようなサイズの人なら大丈夫そうだが、背の高さが違ったりすると対応できなさそう
- ファイルデータから骨格追跡を始めると、骨格情報が暴れる
- 上半身しか写ってないからかもしれませんが、骨格追跡開始直後に、骨格情報が暴れてます
とはいえ、OpenNI でポーズなしに骨格追跡可能な方法がある、というのはとても魅力的ではないでしょうか。