読者です 読者をやめる 読者になる 読者になる

catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

HololensでARをやってみる

HololensでARをやってみる

HoloLensでARをやってみました。かたりぃなです。 まだ完成には遠いので間違いなど含まれているかもしれません。

参考にさせていただいたサイト http://littlewing.hatenablog.com/entry/2016/09/25/172541 座標系のことが丁寧に邦訳されていました。原文を読む前にざっと読んでおくと作業が捗ります。

やったこと・実験環境リスト

やったことは単純です。

  • 適当な前処理と検出処理をして、MTGのカードを認識する
  • 3次元姿勢推定
  • 検出対象物上に文字列を書いた板ポリゴンをレンダリング

実験環境やライブラリは次のとおりです

とりあえずARっぽいことができるようになったので手順を整理します。

カメラキャリブレーション

まずキャリブレーション自体は不要です。 不要というと語弊がありますが、正確にいうとHoloLensのAPIでカメラパラメータをとれるので、そのパラメータがそのまま使えます。

カメラ内部パラメータを取り出す

UWPのMediaCaptureで映像フレームを受け取ると、Windows::Media::Capture::Frames::VideoMediaFrameクラスの形になるので、このCameraIntrinsicsプロパティを拾います。

公式ドキュメントはこちら

https://docs.microsoft.com/en-us/uwp/api/Windows.Media.Capture.Frames.VideoMediaFrame

ここで得られたパラメータをOpenCVの3次元姿勢推定関数に渡すために、cv::Matを作ります。 DirectXと行列の定義が少々異なるので、要注意です。

ちなみにOpenCVのカメラ内部パラメータの行列の定義はこうなっています。

http://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html

カメラ内部パラメータをOpenCVで扱える行列にする

姿勢推定にはおなじみのOpenCVを使います。 というわけでVideoMediaFrameからカメラパラメータを抽出して行列を作るコードです。 flとかppとかをデバッガでみるとそれらしい値が入っているのが分かります。

     auto camin = videoMediaFrame->CameraIntrinsics;
        auto fl = camin->FocalLength;       // 焦点距離
        auto pp = camin->PrincipalPoint;    // 主点
        double inst_raw[] =
        { fl.x, 0, pp.x,
            0, fl.y, pp.y,
            0, 0, 1 };
        cv::Mat inst(3, 3, CV_64F, inst_raw); // カメラ内部パラメータ

三次元姿勢推定を行う

カメラ内部パラメータは取得できました。あとは3次元の姿勢推定のために次の2つがあればOKです。

  • 平面に投影された画像上で検出されたオブジェクトのピクセル単位の座標
  • ピクセル座標と結びついている実世界空間上での検出対象物の座標

歪み補正パラメータはいったん無視しておきます。まず動かしたいので。 とりあえずカードの四隅が検出できるものとして、それが実世界でどうなるかの座標を定義します。 MTGのカードのサイズは6.3x8.8cmなので、メートル単位に直して設定しておきます。 つまりwidth=0.063, height=0.088です。 どうしてメートル単位でいくかというと、HoloLensの座標系がメートル単位なので、合わせておいたほうが色々と都合が良いです。

 // 6.3cm x 8.8cm
    std::vector<cv::Point2f> base_points;
    const float left = -m_width * 0.5f;    // 0.5はカードの中央を原点としたいので。
    const float right = m_width * 0.5f;
    const float top = m_height * 0.5f;
    const float bottom = -m_height * 0.5f;
    base_points.push_back(cv::Point3f(right, top, 0) );        // 右上
    base_points.push_back(cv::Point3f(right, bottom, 0) ); // 右下
    base_points.push_back(cv::Point3f(left, bottom, 0));   // 左下
    base_points.push_back(cv::Point3f(left, top, 0));  // 左上

これで検出したカードの実世界での座標が定義できました。 次に画像中からカードの四隅の座標を検出します。 今回試した簡易的な方法ではまだまだ誤検出が多いので、ソースコード掲載しません。

ここまでに得られた情報をもとにsolvePnPに問題を解いてもらいます。 cam_distは空っぽです。

std::vector<cv::Point2f> detect_points = {};//画像処理によって得られたオブジェクトの位置(ピクセル座標)
cv::Mat tvec, rvec, cam_dist;
cv::solvePnP(base_points, detect_points, cam_inst, cam_dist, rvec, tvec);

これでrvec,tvecが得られました。 しかし、このrvec,tvecともにp(0,0,0)からp(0,0,1)を向いているカメラ座標系でのお話です。 HoloLensではワールド空間に対し、装着者の頭の位置を加味する必要があるので、少々加工します。その前に……。

姿勢推定結果を行列ではなくベクトルで扱いたい

cv::Mat形式で姿勢推定結果が出ました(tvec,rvec)。しかしOpenCV2.4のcv::Matをそのまま扱うのはちょっと面倒です。

行列のままでもいいのですが、これは必ず3x1の行列になるので、各要素へのアクセスは常にcol=0です。つまりrow=1がx, row=2がy, row=3がzを表すベクトルです。 ただのベクトルとして扱いやすいようにベクトル型にします。

cv::Vec3d t,r;
tvec.copyTo(t);     // 得られたtvec,rvecともに=3x1行列なので
rvec.copyTo(r);     // 単純なコピーでよい

これでベクトルになりました。 Hololens(UWP)のAPIに受け渡しやすいようにfloat3型のほうがよかったかもしれませんね。

HoloLensの空間上に姿勢推定結果を使ってレンダリングする

とりあえず適当にやってみます。 ここまでに得られた平行移動ベクトルと回転ベクトル(tとr)はいずれも装着者の頭の位置からの相対座標です。 レンダリングは基本的にワールド空間で行いたいので、ワールド空間座標を求めるためのHoloLens装着者の頭の位置、姿勢を表す行列を作ります

装着者の頭の位置から座標系を表す行列を求める

やってることはそのまんまです。

  1. 装着者の頭の姿勢(headRight, headUp, headBack)を3次元空間の基底とする
  2. レンダリング対象のオブジェクトの位置を頭の位置からの相対座標で設定する
  3. ワールド空間上でのカメラの位置と姿勢+オブジェクトの位置を表現する行列を作る
     // HoloLensのpointerposeをもとに座標系を求める。SolvePnPで得られている結果は、この座標系からの相対座標。
        float3 const headPosition = pointerPose->Head->Position;
        float3 const headForward = pointerPose->Head->ForwardDirection;
        float3 const headBack = -headForward;
        float3 const headUp = pointerPose->Head->UpDirection;
        float3 const headRight = cross(headForward, headUp);

        // 装着者の頭の位置を原点としてレンダリング対象の位置を求める
        m_targetPosition = headPosition + (headRight * trans[0]) + (headUp * -trans[1]) + (headBack * -trans[2]);
        m_normal = normalize(-m_position);

        // 時間軸方向の線形補間
        float3 const prevPosition = m_position;
        m_position = lerp(m_position, m_targetPosition, lerpDeltaTime);

        // 行列をつくる
        camera = make_float4x4_world(m_position, -m_normal, headUp);

VisualStudioが吐き出した元々のスケルトンからの改造なのですが、m_targetPositionを求めるためにベクトルにベクトルを掛けていて謎でした。 「内積でも外積でもない、なんだこれ?」と悩んでいましたが、よく考えると、平行移動行列とベクトルの積を展開したものです。

なので、m_targetPositionがワールド座標系における検出対象オブジェクトの位置になります。 (ワールド空間上の座標=ワールド空間上のカメラ座標 * カメラからの相対座標で表現されたオブジェクトの座標)

cameraにはこの平行移動成分とカメラの姿勢が含まれた行列が入ります。

オブジェクトのローカル座標系での回転量を設定

回転ベクトルをロドリゲスの回転公式から求めてもいいのですが、OpenCVDirectXでは座標系の変換が面倒なので、DirectXの世界で行列を作りました。 手順としては

  1. 回転ベクトルの大きさを求め、回転量とする
  2. 回転ベクトルを正規化。これで回転の軸として使える
  3. make_float4x4_from_axis_angleで、回転軸と回転量をもとにした行列を作る

です。ちょっと回りくどいのでそのうち直したいところです。

        // OpenCVとDirectXの行列の変換とか面倒なので、DirectX(UWP)のAPIで行列を作る
        auto r = float3{ rot[0], rot[1], -rot[2] };
        float strong = length(r);
        float3 axis = normalize(r);
        float4x4 local_rot = make_float4x4_from_axis_angle(axis, strong);

最後に回転行列を求めて順序を間違えないようにかけてあげればOKです。 この行列はそのままVirtexShaderが使うモデル行列になります。

     m_modelConstantBufferData.model = local_rot * camera;

できた!でも、、、

とりあえず板ポリゴンは出ました。しかしまだ面白味のある3Dモデルがレンダリングできていません。。。 座標書いた板ポリゴンだけじゃ味気ないです。そもそも座標ズレている気がしますし。

あと細かいところですが、座標変換が直観的にわかりにくいコードになってしまいました。 モデルの位置と姿勢は本来はモデル座標系のお話なので、カメラには含めるべきではない気がします。 (とはいえ、SolvePnPではカメラが原点からZ軸方向を向いているという前提での姿勢推定なので、この記述でもいいのかもしれない?)

感想と今後の展望

やっと「実機がないとできないこと」をやり始められたので一安心です。 カードの検出と姿勢推定もとりあえずはできたので、次は安定化のために検出したカードの上にアンカー置いてみたいところです。 またカードの種別を識別したいのでDeepLearningのモデルを使ってのカードの画像分類もやっていきたいですね。 やりたいこと山盛りです。

それでは今回はこれくらいで。