VR解剖アプリを作る その2 ~Unity上でCTデータを扱う~

3DデータをUnityに取り込んで、カメラで観察するアプリを作ります。 非VRとVRを実装します。

目次

概要

前回はCTスキャンで得られたDICOMファイルをOsiriXで編集し、OBJファイルとして出力しました。

将来的にはもっと細かく部位ごとに個別のOBJファイルとするつもりですが、ひとまずこのまとまりファイルを使ってアプリを作っていきます。



以下の2つのもののプロトタイプを作ります。

  • ドラッグなどで画面操作できる非VRアプリ(MRアプリを想定して)
  • 首振りで操作するVRアプリ


ちなみに、前回までヒト頭部を使っていましたが、東京大学附属動物医療センターからイヌのCTデータを提供していただいたので以降はこちらを使わせていただきます。

*CTデータは東京大学所属の供血犬のものであり、データの公開は許可を得ています。



3Dモデルを取り込む

まずUnityプロジェクトを新規作成します。

「Assets -> Materials」の中にでもOBJファイルをドラッグ&ドロップで放り込みます。
Sceneにドラッグ&ドロップすれば右のようにUnityのSceneビューに表示されます。
左上の操作パネルを使って、いろんな方向から観察することができます。

ただしこれはUnityのSceneビューの機能なのでこのままアプリをデプロイしてもカメラを動かす機能はついていません。

自由なカメラを手に入れるためにはUnity上でカメラなどにスクリプトを添えてあげる必要があります。


非VRアプリ: カメラ回転、移動、ズーム機能

非VRアプリはスマホやタブレットのタッチパネル操作を想定して作っていきます。

将来的にはイベントのトリガーをHoloLensの操作に置き換えてあげればMRアプリになるという算段です。



対象を観察する方法として、カメラを動かす方法と対象自体を動かす方法が考えられます。

今回はカメラを動かすことにしました。



直交座標より極座標の方が理解しやすいので三次元極座標で空間を捉えます。

まず、原点に3Dモデルの中心をセットします。

前述のように3Dモデルは不動です。



基準点(後述)は原点を中心とした半径rの球表面を移動することになります。(r可変)

球の半径はr変化がズームを表現します。

角度θ、φが球表面上での位置を表現します。



肝心のカメラは基準点を原点とするxy二字平面上を移動します。

デフォルトでは(x, y) = (0, 0)の位置、すなわち基準点と同じ位置から原点を観察します。



三次元極座標

Oが原点で、ここに3Dモデルの中心が置かれます。
Pが基準点で、さらにPからxy分だけずれたP'を新しく考え、これがカメラの位置になります。

対応オブジェクトを作る

ここまで出てきた、原点、基準点、カメラに対応するオブジェクトを作成し、関連づけます。

  • 原点: Origin
  • 基準点: ReferencePoint
  • カメラ: Camera(デフォルトがあるので作成不要)


「Origin -> ReferencePoint -> Camera」という風に親子関係を設定します。


対応スクリプトを作る

ReferencePointをクリックして「Inspector -> Add Component -> New Script」で、新しいスクリプトを作ります。
同様にCameraに対してもスクリプトを用意します。
それぞれPolarCoordinates、CameraMoveと名付けました。

Assetsで右クリックして「Create -> C# Script」で作成したのちにオブジェクトにドラッグしても良いです。

基準点に必要な挙動は回転とズームでした。

今回つけた機能は以下の通りです。

  • スワイプでθ、φ変位(それぞれスワイプの縦成分、横成分が対応)
  • キー操作でr変位(Wで拡大、Qで縮小)
  • キーボードEで視点リセット
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PolarCoordinates : MonoBehaviour {
    public GameObject origin; // 原点
    public Camera camera; // 使用するカメラ
    public float swipeMoveSpeedX = 0.01f; // スワイプで移動するスピード
    public float swipeMoveSpeedY = 0.01f; // スワイプで移動するスピード

        public float theta = 90 * Mathf.Deg2Rad; // 極座標角度パラメータθ
        public float phai = 0 * Mathf.Deg2Rad; // 極座標角度パラメータφ
        public float r = 5; // 極座標距離r(ReferencePointの座標から算出)
        public float diffR = 1; // ズーム単位

    private Vector3 baseMousePos; // 基準となるタップの座標
    private Vector3 baseCameraPos; // 基準となるカメラの座標
    private bool isMouseDown = false; // マウスが押されているかのフラグ

        public void UpdatePosition() {
                // θ,φの変位を直交座標の変位に変換する
                float newX = r * Mathf.Sin(theta) * Mathf.Cos(phai);
                float newY = r * Mathf.Cos(theta);
                float newZ = r * Mathf.Sin(theta) * Mathf.Sin(phai);

                if (theta > 0 && theta < 180 * Mathf.Deg2Rad) {
                        this.transform.position = new Vector3 (newX, newY, newZ);
                }

                // 原点にむけてRotationを修正
                transform.LookAt(new Vector3 (0, 0, 0));
        }

        public void ResetPosition() {
                // ハードコーディング。イケてない
                theta = 90 * Mathf.Deg2Rad;
                phai = 0 * Mathf.Deg2Rad;
                r = 5;
                UpdatePosition();
        }

        // Use this for initialization
        void Start () {
        }
        
        // Update is called once per frame
        void Update () {

                // スワイプの判定
                if ((isMouseDown)|| Input.GetMouseButtonDown(0)) {
                        baseMousePos = Input.mousePosition;
                        isMouseDown = true;
            isAutoRotate = false;
                } 

        // 指離した時の処理
                if (Input.GetMouseButtonUp(0)) {
                        isMouseDown = false;
                        basePinchZoomDistanceX = 0;
                        basePinchZoomDistanceY = 0;
                }
                
                // 回転
                if (isMouseDown) {
                        // マウスグリグリ動かすことでここが変化する
                        Vector3 mousePos = Input.mousePosition;
            Vector3 distanceMousePos = (mousePos - baseMousePos);

                        // スワイプの距離が極座標のθ,φに相当する
                        float swipeX = distanceMousePos.x * swipeMoveSpeedX;
                        float swipeY = distanceMousePos.y * swipeMoveSpeedY;
                        theta += swipeY;
                        phai += swipeX;

                        UpdatePosition();
                        baseMousePos = mousePos;
                }

                // ズーム
                if (Input.GetKeyDown(KeyCode.Q)) {
                    r += diffR;
                        UpdatePosition();
                } else if (Input.GetKeyDown(KeyCode.W)) {
                    r -= diffR;
                        UpdatePosition();
                }

                // リセット
                if (Input.GetKeyDown(KeyCode.E)) {
                        ResetPosition();
                } 

                if (Input.GetKeyDown(KeyCode.JoystickButton0)) {
                        ResetPosition();
                } 
        }
}
PolarCoordinates.cs


カメラに必要な挙動は基準点からの移動です。

今回つけた機能は以下の通りです。

  • キーボードの方向キーでxy変位
  • キーボードEで位置リセット
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMove : MonoBehaviour {
        public float diffPosition = 0.05f; // 移動単位

        public void ResetPosition() {
                this.transform.localPosition = new Vector3 (0, 0, 0);
        }

        // Use this for initialization
        void Start () {
                
        }
        
        // Update is called once per frame
        void Update () {
                
                // 移動
                if (Input.GetKey(KeyCode.UpArrow)) {
                    this.transform.localPosition += new Vector3 (0, diffPosition, 0);
                } else if (Input.GetKey(KeyCode.DownArrow)) {
                    this.transform.localPosition -= new Vector3 (0, diffPosition, 0);
                } else if (Input.GetKey(KeyCode.LeftArrow)) {
                    this.transform.localPosition -= new Vector3 (diffPosition, 0, 0);
                } else if (Input.GetKey(KeyCode.RightArrow)) {
                    this.transform.localPosition += new Vector3 (diffPosition, 0, 0);
                }

                // リセット
                if (Input.GetKeyDown(KeyCode.E)) {
                        ResetPosition();
                } 
        }
}
CameraMove.cs


VR解剖図 HoloLensアプリ

実装した結果このような挙動を実現できます。
回転、移動、ズームを組み合わせることで任意の点を観察することができるようになりました。


VRアプリ: ジャイロ機能

VRアプリでは画面操作はできません。

コントローラを用意しない場合は、主に首振りや視線で操作することになります。

今回は複雑なものは実装せず、3Dモデルを首振りで観察することを目指します。

スマホのジャイロ機能を使用します。



加えてVRアプリ化を施す必要があります。

現在、UnityでVRアプリを作ろうと思ったら例として以下の2つの選択肢が取れます。

  • Google VR SDK for Unityを使う
  • UnityのVR support機能を使う

UnityのVR機能はまだ新しいものですが、今後この組み込み機能を使うのがスタンダードとなるでしょう。

外部ライブラリに依存させたくないので今回はこちらを選択します。



ちなみに今回はAndroid向けに作っていきます。



VR設定

「File-> Build Settings-> Player Settings」でInspectorウィンドウが表示されます。
ドロイド君のマークが選択されていることを確認します。

「Inspector-> Other Settings-> Virtual Reality Supported」にチェックします。
Virtual Reality SDKは何かしらを選択しなくてはいけないのでCardboardを選択しておきます。
多分Noneでもいけると思います。

Minimum API Levelを要求通りに上げ、Bundle Identifierを適当に設定します。



対応オブジェクトを作る

対応オブジェクトを作る

今回は以下の2つのオブジェクトがあればOKです。

  • ジャイロの対象: Gyro
  • カメラ: Camera(デフォルトがあるので作成不要)


親子関係は左の通りです。


対応スクリプトを作る

非VRの時と同じようにスクリプトファイルを作成します。

今回はGyroにだけ設定すれば良いです。

  • 起動時にジャイロ機能を有効にする
  • ジャイロに応じて回転を与える


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Gyro : MonoBehaviour {
        Quaternion gyro;

        void Start () {
                Input.gyro.enabled = true;
        }
        
        void Update () {
                gyro = Input.gyro.attitude;
                transform.rotation = Quaternion.Euler(0, 0, 180) * (new Quaternion(gyro.x, -gyro.y, gyro.z, gyro.w));
        }
}
Gyro.cs

クォータニオンによる回転処理は理解するのに時間がかかりました。

ややこしいのでここでは割愛します。

詳しいことを知りたい方は是非調べてみてください。



実機で挙動を確認する

PCとAndroidを接続して実機で確認します。

Androidの設定からUSBデバッグを有効にしておきましょう。

USBケーブルでPCとつないで、Unity上でBuild & Runを実行します。



VR解剖図 Androidアプリ

分かり辛いですが、Android実機の傾きにカメラが追従しています。


まとめ

初めてのUnity、初めてのC#でしたが、意外となんとかなりました。

VR界隈も機械学習と同じで情報の更新が早いのでソースの鮮度には気をつける必要があります。

とりあえず最低限の閲覧機能をつけることができました。

全体の流れをつかめたのでこれからは洗練化していきます。

次の目標は塊になっている3Dモデルを、骨ごとに切り分けることですね。

それができたら臓器、筋肉、血管、神経も出力して相対位置を固定。

これで最低限の教材として使えるかなという感じです。

画像解析が適用できるのはまだまだ後ですね。



参考

本記事は個人的に開発しているアプリのこれまでの開発日誌をまとめたものです。

下記以外の参考サイトや開発詳細、トライアンドエラーの様子は以下のサイトを参照してください。

ここには載せませんでしたがPS3コントローラでの操作を試みたりもしています。



参考サイト