よこやま日記・技術/ガジェット編

@hiroyky IT系エンジニアのまったりブログ

HololensでPhotoCapture画像をワールド座標の照準オブジェをもとにトリミング

f:id:hiroyky:20170226213437j:plain

作ったもの

前回,突貫ながらHololensのQRコードリーダを開発しました.

hiroyky.hatenablog.com

今回は,QRコードリーダに正方形の照準を加えました.

f:id:hiroyky:20170226210028j:plain

前回作ったQRコードリーダはAirTapするとHololensの視野全体をキャプチャし, QRコードの検出と解析を行うものでした.

視野角全体をキャプチャするため,画像のほとんどの部分は不要です. 特に,視野角にQRコードが2つ入っていると対応できなくなっていました.

そこで,Hololensに正方形の照準を設けて,キャプチャ下画像のうち照準内のみを切り出して処理するようにしました.

仕組み

MainCamera直下に照準としてQuad(以下QrSight)を設置します. f:id:hiroyky:20170226213714p:plain

PhotoCaputureを行なったときに,QrSightの4頂点のワールド座標をキャプチャ画像の座標系に変換し,囲まれた範囲を切り出します.

ソースコード

ソースコードは前回同様このリポジトリにあります.

github.com

特に今回は Assets/Scripts/PhotoInput.csを実装しました.

ポイント

QrSightの設置

MainCameraの直下にQuadオブジェクトを設置します.枠線のみの透過pngの画像をテクスチャとして割り当てます. これを照準として利用します. f:id:hiroyky:20170226215437p:plain

  • popsition: (x, y, z = 0,0,0)
  • scale: (x, y, z = 0.2,0.2,0.2)

QrSightの4頂点の座標

AirTapで画像キャプチャされたときにQrSightの4頂点の座標を求め,座標変換を行います.

まずは,QrSightの座標(中央座標)からscaleを手掛かりに頂点をそれぞれ求めます. 今回はこのようにQuadの各頂点の位置を求めましたが,もっと良いやり方があればご教授願いです.

そして,Camera.main.WorldToScreenPointメソッドで ワールド座標系から投影座標系に変換を行います.なおこの時にRayCastした座標を使うべきか否かは悩みどころで悩んでます.

        var position = QrSight.transform.position;
        var direction = QrSight.transform.forward;
        var scale = QrSight.transform.localScale;

        // ワールド座標系でのQR照準の座標を求めます.
        var leftTop = new Vector3(
                position.x - scale.x / 2,
                position.y + scale.y / 2,
                position.z);
        var rightTop = new Vector3(
                position.x + scale.x / 2,
                position.y + scale.y / 2,
                position.z);
        var rightBottom = new Vector3(
                position.x + scale.x / 2,
                position.y - scale.y / 2,
                position.z);
        var leftBottom = new Vector3(
                position.x - scale.x / 2,
                position.y - scale.y / 2,
                position.z);

#if false
        /* Rayを使うかどうかが悩みどころ;;*/
        RaycastHit leftTopHit, rightTopHit, leftBottomHit, rightBottomHit;
        Physics.Raycast(leftTop, direction, out leftTopHit);
        Physics.Raycast(rightTop, direction, out rightTopHit);
        Physics.Raycast(leftBottom, direction, out leftBottomHit);
        Physics.Raycast(rightBottom, direction, out rightBottomHit);

        // ワールド座標系を投影座標系に変換
        var leftTopScreen = Camera.main.WorldToScreenPoint(leftTopHit.point);
        var rightTopScreen = Camera.main.WorldToScreenPoint(rightTopHit.point);
        var leftBottomScreen = Camera.main.WorldToScreenPoint(leftBottomHit.point);
        var rightBottomScreen = Camera.main.WorldToScreenPoint(rightBottomHit.point);
#else
        // ワールド座標系を投影座標系に変換
        var leftTopScreen = Camera.main.WorldToScreenPoint(leftTop);
        var rightTopScreen = Camera.main.WorldToScreenPoint(rightTop);
        var leftBottomScreen = Camera.main.WorldToScreenPoint(leftBottom);
        var rightBottomScreen = Camera.main.WorldToScreenPoint(rightBottom);
#endif

これで投影座標系に変換できたため画像上のピクセルが求まったかと思いきやそうではなくて,キャプチャ画像の座標に変換する必要がありそうでした. 次のように,各頂点の座標をいったん正規化してCameraParametersの解像度を使って変換します.

        // 投影座標系を,PhotoCaptureが撮影する画像上での座標に変換
        int leftSide = (int)(leftTopScreen.x / (float)Camera.main.pixelWidth * cameraParameters.cameraResolutionWidth);
        int rightSide = (int)(rightTopScreen.x / (float)Camera.main.pixelWidth * cameraParameters.cameraResolutionWidth);
        int bottomSide = (int)(leftBottomScreen.y / (float)Camera.main.pixelHeight * cameraParameters.cameraResolutionHeight);
        int topSide = (int)(leftTopScreen.y / (float)Camera.main.pixelHeight * cameraParameters.cameraResolutionHeight);

これで,キャプチャ画像座標系でのQrSightの頂点座標が求まりました.ただ,ズレも目立つので調整する必要ありそうです(;‘∀’)

画像の変換・トリミング

キャプチャ画像座標系でのQrSightの頂点座標をもとに,画像を抜き出します. ここはOpenCVを使えばよいと思いますが今回はゴリゴリ書いてみました.

        byte[] dst = new byte[src.Count];
        for (int y = 0; y < cameraParameters.cameraResolutionHeight; ++y) {
            for (int x = 0; x < cameraParameters.cameraResolutionWidth; ++x) {
                int px = (y * cameraParameters.cameraResolutionWidth + x) * stride;
                if (x >= leftSide && x <= rightSide && y >= bottomSide && y <= topSide) {
                    for (int i = 0; i < stride; ++i) {
                        dst[px + i] = src[px + i];
                    }
                } else {
                }
            }
        }
// *一部,ブログ記事用に本体コードに変更を加えています.

まとめ

  • 前回開発したHololens Qrコードリーダに照準を付けてみました.
  • ワールド座標系をHololensのPhotoCaptureによるキャプチャ画像の座標系に変換しました.(間違ってるかも)
  • 変換した座標をもとにキャプチャ画像をトリミングしました.