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

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

Hololensでワールド座標系とOpenCVの画像座標系の変換を確認

f:id:hiroyky:20170321014208p:plain

やったこと(概要)

Hololensでスクリーンショットを撮って,OpenCVで画像処理,その結果を空間に反映ということは,今後よくあるのではないかと思います. ここで重要なのが座標系の変換です.Hololensの空間であるUnityのワールド座標系とOpenCVの画像座標系を適切に変換する必要があります.

Unityはこうした座標系変換を行うAPIを提供しています.今回はそれらのAPIが適切に機能するかを検証・確認してみました.

OpenCVの画像座標としていますが,一般的な画像座標と等価です.

(何事もおそらくできるだろう という発想が危険です!)

さらに進化した画像処理ライブラリの定番 OpenCV 3基本プログラミング

さらに進化した画像処理ライブラリの定番 OpenCV 3基本プログラミング

詳解 OpenCV ―コンピュータビジョンライブラリを使った画像処理・認識

詳解 OpenCV ―コンピュータビジョンライブラリを使った画像処理・認識

OpenCVによる画像処理入門 (KS情報科学専門書)

OpenCVによる画像処理入門 (KS情報科学専門書)

方法

AirTapしたときに,次の動作を行うプログラムを作成します.

  1. スクリーンショット撮影.
  2. HololensのCursorのワールド座標を取得し,OpenCV画像座標に変換.
  3. OpenCVスクリーンショットに点を描画.
  4. その描画座標を再度,ワールド座標に変換し,空間にCubeを設置.

つまり,ワールド座標→画像座標→ワールド座標の変換を行い,スクリーンショットの点とワールド座標のCubeが同じ場所に設置されればOKです.

コード

今回作成したコードはこちらにあります.

github.com

なお,HololensToolkit for UnityOpenCVForUnity(有償)が必要です.

ポイント

変換API

ワールド座標系から画像座標系へ

画像座標系すなわちビューポート座標系の変換にはCamera.WorldToViewportPointを使います.この関数は正規化された値が返ってくること,Y軸の向きが逆(上方向)であることに注意しましょう. 従って正規化された値を画像サイズで乗算し,Y軸の向きを反転させる必要があります.

OpenCVForUnity.Point worldPointToMatPoint(Vector3 worldPosition, Mat image) {
    Vector3 viewportPoint = Camera.main.WorldToViewportPoint(worldPosition);
    OpenCVForUnity.Point drawPoint = new OpenCVForUnity.Point(viewportPoint.x * image.width(), (1.0 - viewportPoint.y) * image.height());
    return drawPoint;
}

画像座標系からワールド座標系へ

画像座標系からワールド座標系への変換には,まずビューポート座標系は正規化された値であるため,画像座標を正規化し,Camera.ViewportPointToRayでRayを取得し,ワールド座標系を取得します. また,ビューポート座標を求めるときはY軸の向きを反転させる必要があります.

bool matPointToWorldPoint(OpenCVForUnity.Point point, Mat image, out Vector3 worldPoint) {
// 画像座標を正規化,Y軸の向きを反転し,ビューポート座標を求める.
    float viewportPointX = (float)(point.x / (double)image.width());
    float viewportPointY = (float)(1.0 - point.y / (double)image.height());
    Vector3 viewportPoint = new Vector3(viewportPointX, viewportPointY);

    // Rayを使って,Z軸も含んだワールド座標を求める.
    Ray ray = Camera.main.ViewportPointToRay(viewportPoint);
    RaycastHit hitInfo;
    if (Physics.Raycast(ray, out hitInfo)) { 
        worldPoint = hitInfo.point;
        return true;
    }
    worldPoint = new Vector3();
    return false;
}

結果

う~ん,微妙にずれてるけど仕方ないのかぁ~.

f:id:hiroyky:20170321014346j:plain f:id:hiroyky:20170321014208p:plain

その他

※ 生活感ありありの画像で申し訳ありませんm_m

Hololensで初音ミクに踊ってもらった. MMDモデルとモーションの取り込み

f:id:hiroyky:20170307004236j:plain

作ったもの

Hololensでベッドの上で初音ミクに「Perfect Human」を踊ってもらいました. HololensアプリケーションにMMDのモデルとモーションを取り込んで動いています.


【Hololens】初音ミクにPerfectHumanをベッドの上で踊ってもらった

使用したもの

Hololens Tookkit for Unityに加え, 以下のものを活用させていただきました.

モデル

bowlroll.net

モーション

bowlroll.net

MMDファイルのUnityへの取り込み

MMD for Unity

github.com

開発キット

  • Unity 5.5 0p2

ポイント

MMD for Unityのインポートと修正

インポート

UnityでMMDモデルを利用するにはMMD4Mecanimを利用する方法もあるようですが,現行バージョンではUWPアプリケーションが非対応になっているようです. そこで,代わりにMikuMikuDance for Unityを利用します.公式サイトからzipファイルをダウンロードし,展開したディレクトリをそのままUnityのProjectにインポートします.

修正

インポートしたMMD for Unityに修正を加えます.2017年3月現在やや古い状態なので動くようにします・(ここではとりあえず動作することを目指して,機械的に修正を加えていきますが適切かどうかはまた別な話です.)

1 APIのアップデート

取り込んだMMD for Unityのディレクトリを右クリックしてRun API Updateをクリックします.

f:id:hiroyky:20170307002914p:plain

2 コードの修正

いくつかの行がエラーになるはずなので修正方法をピックアップします.

まず,以下のコードをコメントアウト

// AnimationUtility.SetAnimationType(clip, animation_type);

GameObject.collider, GameObject.rigidbodyのようにcolliderやrigidbodyをプロパティとして参照している箇所でエラーが出ています.そこをGetComponent<>()メソッドを使って取得するようにします.

// 変更前
parent.rigidbody
parent.collider
// 変更後
parent.GetComponent<Rigidbody>();
parent.GetComponent<Collider>();
// ・・・みたいに

MMDモデルとモーションの取り込み

MMD for Unityの公式サイトのドキュメントに従ってモデルとモーションを取り込みます.

MikuMikuDance for Unity - Howto

モデルの取り込み時にAnimationTypeをGeneric Mecanimを選択します.

f:id:hiroyky:20170307005144p:plain

アニメーションの設定

MMD for Unityでモーションのアニメーションまで作ったらあとは登録します. AnimationControllerを作ってprefabにanimationに設定していきます.

f:id:hiroyky:20170307010256p:plain

f:id:hiroyky:20170307010528p:plain

RigidbodyやSpatialMappingの設定

  • モデルにRigidbodyやColliderを設定.
  • 空間認識のためにSpatialMappingを設定.

ただ,起動直後に重力を有効にしてしまうと空間認識が完了しておらず,モデルが奈落の底に落ちてしまうため,空間認識が完了してから重力を有効にします.

まとめ

MMDのモデルとモーションを取り込んでダンスするHololensアプリケーションを作りました.

参考文献

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によるキャプチャ画像の座標系に変換しました.(間違ってるかも)
  • 変換した座標をもとにキャプチャ画像をトリミングしました.

HololensでQRコードリーダを作ってみた.

作ったもの

AirTapするとQRコードを読み込んで,その内容を空間に表示するHololensアプリケーションです.


Hololens Qr code reader

仕組み

QRコードの検出とデコードにZXing.Netを利用しました.

AirTapするとHololensのカメラの画像をZXingに入力して,デコードを行います. デコードで得られたQRコードの内容のカーソルの座標に表示します. カーソルの座標を3次元で得るためにSpatialMappingはあらかじめ行っています.

(まぁ,できあいの物をつなげただけと言われればそれまでです.)

ソースコード

今回開発したものはこちらで公開しています.

github.com

ポイント

WebCamとSpatialMappingを許可

HolotoolKit-Unityをインポート後,WebCamとSpatialMappingを許可します. f:id:hiroyky:20170223003953p:plain

HololensのWebCamは2017年2月23日現在,UnityEditor上でのデバッグには非対応のようです. 実際,UnityEditor上でWebCamを使用すると開発PCに接続しているWebCamの映像が入力されました. (これはこれでデバッグとして使えるかもしれませんが.)

インポートするZXingについて

HololensはUWPアプリケーションであるため,ZXingに含まれるzxing.winmdをプラグインとしてインポートします. インポート後,WSAPlayerでのみ使用するように設定されていることを確認します. f:id:hiroyky:20170223003403p:plainf:id:hiroyky:20170223003433p:plain

UnityEditorは非対応のため,プリプロセッサで分岐します.

    public string Decode(byte[] src, int width, int height) {
#if !UNITY_EDITOR
        Debug.Log("qr decoding...");
        ZXing.IBarcodeReader reader = new ZXing.BarcodeReader();
        var res = reader.Decode(src, width, height, ZXing.BitmapFormat.BGRA32);
        if (res == null) {
            return null;
        }
        return res.Text;
#else
        return "editor debugging...";
#endif
    }

.Net Framework,.Net Core, UWP, Xamarinなど最近いろいろありますが,この辺りを知っている人は難なくできそうでうですが,自分にとってはつまづきポイントだったため,記述しておきます;;

WebCamで撮影

Hololensで見ている景色を撮影する方法は Locatable camera in Unityで解説されています.

また,CameraParameterのhologramOpacityプロパティの値を0にすることでホログラムが映らなくなるはずなので,今回は0にします.

c.hologramOpacity = 0;

該当コード: HololensQrCodeReader/PhotoInput.cs at master · hiroyky/HololensQrCodeReader · GitHub

QRコードのデコード

QRコードの検出とデコードはもっぱらZXingのライブラリ任せです. byte配列にして渡しています.

該当コード: HololensQrCodeReader/QrDecoder.cs at master · hiroyky/HololensQrCodeReader · GitHub

QRコードの内容表示

QRコードを無事デコードできたら,その内容をTextMeshで表示します。 TextMeshの表示位置はカーソルの位置です。

まとめ

突貫で簡易ながらHololensでQRコードリーダを作ってみました.

MQTTでServer(Broker)も設置してRaspberry Pi 3とLinux機を通信させてみた。

概要

先日、RaspberryPi3にGroveの温湿度センサを取り付けて温湿度をREST APIとして提供する装置を作ってみました。

hiroyky.hatenablog.com

REST APIでも良いのですが、今回はMQTTというプロトコルを使って温度・湿度を送受信してみました。 MQTTについてはMQTT as a Service sango MQTTについて詳しく知る(外部)が参考になると思います。

目標

  1. Raspberry PIをPublisherとし、温湿度を送信すする。
  2. Linux上に構築したMQTT Server(Broker)を構築する。
  3. 別のLinuxマシンが温湿度を受信する。

RaspberryPiが送信した温湿度を、MQTT Serverを経由して別のLinuxマシンが受信します。

Raspberry Piではじめるおうちハック ~ラズパイとIoTでつくる未来の住まい~

Raspberry Piではじめるおうちハック ~ラズパイとIoTでつくる未来の住まい~

やってみた

MQTT Serverのセットアップ

まっ更なUbuntuにmosquittoをインストール。各OSごとの導入方法はDownloads | Mosquittoで紹介されています。 Ubuntu16.04では標準のパッケージリストにすでに含まれているようです。最新のパッケージが欲しい場合はリポジトリを追加する必要がありそうですが、このまま行きます。

$ sudo apt-get install mosquitto
$ sudo service mosquitto status
sudo: unable to resolve host mqtt-broker
● mosquitto.service - LSB: mosquitto MQTT v3.1 message broker
   Loaded: loaded (/etc/init.d/mosquitto; bad; vendor preset: enabled)
   Active: active (running) since Mon 2017-01-02 17:39:05 UTC; 19h ago
・・・

MQTTのおためし

MQTTのSubscriverをセットアップ

こちらもまっ更なUbuntuにインストール。mosquitto_clientsをインストールします。

$ sudo apt-get install mosquitto-clients

mosquitto_subで起動します。とりあえずトピック名はsampleでいきます。

$ mosquitto_sub -d -t sample -h <mosuqittoサーバのアドレス>
Client mosqsub/2193-mqtt-test2 sending CONNECT
Client mosqsub/2193-mqtt-test2 received CONNACK
Client mosqsub/2193-mqtt-test2 sending SUBSCRIBE (Mid: 1, Topic: sample, QoS: 0)
Client mosqsub/2193-mqtt-test2 received SUBACK
Subscribed (mid: 1): 0
Client mosqsub/2193-mqtt-test2 received PUBLISH (d0, q0, r0, m0, 'sample', ... (5 bytes))

MQTTのPublisherをセットアップ

温湿度センサのRaspberryPi3にインストール。mosquitto_clientsをインストールします。

$ sudo apt-get install mosquitto-clients

テキトーなメッセージを送ってみます。

$ mosquitto_pub -d -t sample -m "Hello" -h <mosquittoサーバのアドレス>

Subscriberの方にメッセージのHelloが標準出力されました。

MQTTのPublisherをPythonで実装

pythonインターフェイスの導入

ありがたいことにmosquittoのpythonインターフェイスが用意されているので導入します。

pypi.python.org

$ sudo pip install paho-mqtt

まぁこんな感じかな。grovepiからセンサの値を取得して、そのままMQTTで送信。 コード全体はこちらのリポジトリ このスクリプトをcronあたりで定期実行すればとりあえずOKと。

#!/usr/bin/python2.7
# -*- coding:utf-8 -*-

import paho.mqtt.publish
import grovepi

def parse_args():
・・・

def sensing():
    (temperature, humidity) = grovepi.dht(8, 0)
    return (temperature, humidity)

def publish(hostname, data):
    print data
    paho.mqtt.publish.multiple([data], hostname=hostname)

if __name__ == '__main__':
    args = parse_args()
    (temperature, humidity) = sensing()
    data = {
        'topic': args.topic,
        'payload':str({'temperature':temperature, 'humidity':humidity})
    }
    publish(args.hostname, data)

参考文献

Ubuntuでnvidiaドライバをインストールしたらログインループにorz..

概要

Ubuntu 16.04 LTSにnvidiaドライバをインストールしたらGUIでのログインができなくなってしまいました。 ログイン画面が表示されてパスワードを入力してログインを試みても、一瞬画面が黒くなってまた戻ってしまいます。

似た事例

同じ現象に悩まされた人はいるようです。そして解決方法もマチマチな感じ。

askubuntu.com

askubuntu.com

may46onez.hatenablog.com

www.rokudw.net

ちなみにこちらは動画で症状と解決方法を提案しています。ただ、自分はこの方法を試しても改善しませんでした。しかしながら、コストは大きくなのでとりあえず試してみるのがいいと思います。


How to fix ubuntu login loop [ quick tutorial ]

解決方法

自分の成功した解決策は次のaskubuntuに投稿された1つ目の投稿です。

askubuntu.com

ctrl + alt + f1を押してCUIでログイン。

$ sudo ubuntu-drivers list
$ sudo ubuntu-drivers autoinstall
$ sudo reboot

これでとりえあず良くなりました。

機種

;; (なんかNAVERまとめ みたいなエントリになってしまった。。。)

CUDA C プロフェッショナル プログラミング (impress top gear)

CUDA C プロフェッショナル プログラミング (impress top gear)

CUDA高速GPUプログラミング入門

CUDA高速GPUプログラミング入門