RealSense深度センサーをAndroidなUnityから使う方法

# はじめに

この記事では、Android端末上のUnityアプリケーションからIntel社製の深度センサーデバイスRealSenseを利用するための方法をまとめました。
従来のRealSense SDK 2では、RealSenseを利用するためにRoot権限が必要だったので、利用環境が限られてしまっていたのですが、RealSense T265サポートと同時に公開された新SDKから非Root化端末でも利用できるようになりました。

![D435_Unity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112880/a1a6833f-3deb-4a4a-2d0c-505a8b0317cd.png)


# RealSenseとAndroid OS

## RealSenseアプリの開発方法
Intel社製の深度センサーデバイス[RealSense](https://realsense.intel.com/)を利用するアプリケーションを開発するためには、Intelがソースコードを含めて公開しているRealSense SDKを利用します。
RealSense SDKには、旧来の深度デバイス(F200, R200, LR200 and ZR300)をサポートするVer1.0と新しいデバイス(D400シリーズ)をサポートするVer2.0の二つの系列があります。
今回の開発では、(私が持っている)RealSense D435を用いることにします。D435の主な諸元は次の通りです。

 * Use Environment: Indoor/OutdoorDepth
 * Technology: Active IR Stereo
 * Image Sensor Technology: Global Shutter; 3um x 3um pixel size
 * Depth Field of View (FOV)—(Horizontal x Vertical) for HD 16:9: 85.2° x 58° (+/- 3°)
 * Depth Output Resolution & Frame Rate: Up to 1280 x 720 active stereo depth resolution. Up to 90fps
 * Minimum Depth Distance (Min-Z): 0.105m
 * Maximum Range: 10m+. Varies depending on performance accuracy, scene and light conditions
 * RGB Resolution: Up to 1920 x 1080 resolution
 * RGB FOV (H x V x D): 69.4 x 42.5 x 77 (+/- 3°)

## Android OSから開発を行う場合の課題

今回開発に用いるRealSense SDK 2はマルチプラットフォーム対応がうたい文句の一つで、Linux / Windows / macOS / Androidに対応しているとSDKの公開ページに明記されています。ただ、Android OSサポートに関して(Ver2.18.1迄は)Linux OSと同じ機構を利用しており、libuvc+libusbをベースにUVC Backendが実装されています。
つまり、libusbがAndroid OSで利用可能なことが必須条件なのですが、libusbの[Android OS READMEページ](https://github.com/libusb/libusb/blob/master/android/README)にはつれなく次のような文言が制限事項として書かれています。

```
Runtime Permissions:
--------------------

The default system configuration on most Android device will not allow
access to USB devices. There are several options for changing this.

If you have control of the system image then you can modify the
ueventd.rc used in the image to change the permissions on
/dev/bus/usb/*/*. If using this approach then it is advisable to
create a new Android permission to protect access to these files.
It is not advisable to give all applications read and write permissions
to these files.

For rooted devices the code using libusb could be executed as root
using the "su" command. An alternative would be to use the "su" command
to change the permissions on the appropriate /dev/bus/usb/ files.

Users have reported success in using android.hardware.usb.UsbManager
to request permission to use the UsbDevice and then opening the
device. The difficulties in this method is that there is no guarantee
that it will continue to work in the future Android versions, it
requires invoking Java APIs and running code to match each
android.hardware.usb.UsbDevice to a libusb_device.
```

つまり、Root権限を用いてSELinuxをオフにしてデイバスファイル `/dev/bus/usb/*` へのアクセスを許可すると共に動的に作成されるこのデバイスファイルへ(Linux的)アクセス権限を設定する必要があります。
このような事情があるために、Androidスマートフォン端末からRealSenseを利用することは事実上困難な状況が続いていました。

## New RealSense SDK登場

このlibusbライブラリの壁を突破する切っ掛けとして、今年(2019)の1月に画期的なPull Requestが提案され無事にマージされました。

 * [Enable librealsense on unrooted android devices #3083](https://github.com/IntelRealSense/librealsense/pull/3083)

このPull Requestを含む最新のRealSense SDK Ver2.19.0以降では通常の(非Rootな)Android OSでもRealSesenを利用することが可能になっています。

# RealSense SDKの詳細

## ソースコードの準備とビルド方法

1. RealSense SDKのGithubページにアクセスして普通にcloneをします。
```
git clone https://github.com/IntelRealSense/librealsense
```
2. Android Studioを起動して `librealsense/wrappers/android/` をOpen Projectで開きます。
3. [Build]-[Make Project]でプロジェクト全体をビルドするとライブラリとサンプルの両方をビルドすることが出来ます。

## サンプルアプリケーション

RealSense SDKにはいくつかの種類のサンプルアプリケーションが付属していますが、カメラ画像と深度情報を並べて表示するcaptureアプリを参考に全体を俯瞰してみたいと思います。
　
<ol><li>初期化処理
captureアプリでは、RealSenseを(外部USB接続された)カメラデバイスとして認識・利用する為にAndroidManifest.xmlでPermissionを宣言すると同時にonCreate()メソッド内で必要な処理を行っています。

```xml
    <uses-permission android:name="android.permission.CAMERA"/>
```
</li><li>RealSense初期化処理
captureアプリが起動した後には、onCreate()メソッド内で必要な権限が得られていること確認した後に次のinit()メソッドが呼ばれます。
ここでRsContextオブジェクト、Pipelineオブジェクト、Configオブジェクト及びColorizerオブジェクトの生成が行われます。
それぞれのクラスは、基本的にRealSense SDKのC++ Native Codeのラッピングオブジェクトとなっており、JNI(Java Native Interface)を経由して
SDK内の実際の処理が呼び出される仕組みになっています。

```java
    private void init(){
        RsContext.init(mAppContext);

        mRsContext = new RsContext();
        mRsContext.setDevicesChangedCallback(mListener);

        mPipeline = new Pipeline();
        mConfig  = new Config();
        mColorizer = new Colorizer();

        mConfig.enableStream(StreamType.DEPTH, 640, 480);
        mConfig.enableStream(StreamType.COLOR, 640, 480);

        if(mRsContext.getDeviceCount() > 0) {
            showConnectLabel(false);
            start();
        }
    }
```

</li><li>Streaming開始処理
init()メソッド内で有効なRealSenseデバイスが認識されている場合には、次のstart()メソッドが呼び出されRealSenseからのデータを実際に読み出すStreaming処理を起動します。
Streaming処理を行うメソッド(mStreaming)は、UIスレッドから定期的に呼び出されるようにHandlerオブジェクト(mHandler)にpostされます。

```java
    private synchronized void start() {
        if(mIsStreaming)
            return;
        try{
            Log.d(TAG, "try start streaming");
            mGLSurfaceView.clear();
            mPipeline.start(mConfig);
            mIsStreaming = true;
            mHandler.post(mStreaming);
            Log.d(TAG, "streaming started successfully");
        } catch (Exception e) {
            Log.d(TAG, "failed to start streaming");
        }
    }
```

</li><li>Streaming処理
UIスレッドから定期的に呼び出され、RealSenseからのデータを読み込みAndroid端末の画面に表示する処理を行います。
Pipelineオブジェクトからフレーム情報(Framesetオブジェクト)を読み出し、Colorizerオブジェクトでカラー変換処理を行い画面に表示します。

```java
    Runnable mStreaming = new Runnable() {
        @Override
        public void run() {
            try {
                try(FrameSet frames = mPipeline.waitForFrames(1000)) {
                    try(FrameSet processed = frames.applyFilter(mColorizer)) {
                        mGLSurfaceView.upload(processed);
                    }
                }
                mHandler.post(mStreaming);
            }
            catch (Exception e) {
                Log.e(TAG, "streaming, error: " + e.getMessage());
            }
        }
    };
```
</li></ol>

## RealSenseとUSB Attach処理の話
Android OSではUSBデバイスを接続したときに、ユーザーに対して承認を求める処理が行われます。
一般的にはAndroidアプリ側にその処理が記述されますが、RealSense SDKでは内部に専用のActivity(DeviceWatcherActivity)が実装されておりアプリ側からは隠蔽されるようになっています。

その部分の処理についてはLibrealsenseのAndroidManifest.xmlに記述されています。
USB_DEVICE_ATTACHEDのIntentに対してDeviceWatcherActivityが起動され、このActivityは直ぐに終了しAndroidアプリのActivityに戻る仕組みになっています。
この処理の副作用として**Androidアプリ側のActivityはRealSenseが接続されるときに一度onPause()とonResume()が実行されることに注意**する必要があります。

```xml:librealsense/wrappers/android/librealsense/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.intel.realsense.librealsense">
  <application android:allowBackup="true" android:supportsRtl="true">
    <activity android:name=".DeviceWatcherActivity"
              android:theme="@android:style/Theme.NoDisplay"
              android:directBootAware="true">
      <intent-filter>
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
      </intent-filter>

      <meta-data
        android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
        android:resource="@xml/usb_filter" />
      </activity>
  </application>
</manifest>
```

また、対応するUSBデバイスの種類についてはusb_filter.xmlにVID(Vendor ID)が記述されています。

```xml:librealsense/wrappers/android/librealsense/src/main/res/xml/usb_filter.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="0x08086" />
</resources>
```

# UnityアプリからLibrealsenseへのアクセス方法

RealSense SDK自身にもUnity (C#) Wrapperが実装されていますが、残念ながらWindows OS限定の機能になっており、Android OSでは独自にC#からAndroid Native Pluginを経由してJavaコードを呼び出す必要があります。つまり、Windows OSではC#から直接Native PluginとしてRealSense SDKを呼び出す二層構造ですが、Android OSではC#、Java及びC++/C言語で記述されたNative Codeの三層構造になります。
本節では、例としてUnityアプリケーションからRealSense D435のカメラ画像を取得してテクスチャとして表示する方法のエッセンスを紹介したいと思います。
紹介するコードについては、前節で紹介したcaptureアプリケーションを改造する事を前提としています。

## Unityアプリケーションのスケルトン
まず、Unityを起動して新しいUnityアプリケーションを作成します。
Unityアプリケーションのビルドの流れとしては、Unityで生成したAndroidプロジェクトをAndroid Studioで実際にビルドする方法をとります。

次の手順を実行してLibrealsenseとインテグレーションするアプリケーションの準備を行います。

1. [File]-[Build Settings]を開いてAndroid Platformに変更を行います。

2. "Build System"がGradleになっていることを確認します。

3. "Export Projects"チェックボックスをONにし、Build Settingsダイアログの下の"Export"ボタンを押下します。

4. Androidプロジェクトを保存するディレクトリを指定するダイアログが表示されるので、適当なディレクトリを指定します。

## Librealsenseのインテグレーション

Android Studioを起動して、先ほど作成したAndroidプロジェクトを開きます。
インテグレーション全体の流れとしては、標準で作成されるUnityPlayerActivityを継承する新しいActivityを作成しRealSense SDKを利用するコードを記述します。
　
<ol></li>librealsense-debug.aar(共有ライブラリ)を[File]-[New]-[New Module]-[Import .JAR/.AAR Packages]から取り込みます。

</li><li> build.gradleが上書きされないように一番先頭のコメント分を削除し、dependenciesに次の行を追加します。

```
    implementation project(':librealsense-debug')
```

</li><li>新しいActivityを次のように作成します。

```java:新しいMainActivityクラスの例
public class MainActivity extends UnityPlayerActivity {
    private static final String TAG = "librs capture example";
    private static final int PERMISSIONS_REQUEST_CAMERA = 0;

    private boolean mPermissionsGrunted = false;

    private Context mAppContext;
    //private TextView mBackGroundText;
    //private GLRsSurfaceView mGLSurfaceView;
    private boolean mIsStreaming = false;
    private final Handler mHandler = new Handler();

    private Pipeline mPipeline;
    private Config mConfig;
    private Colorizer mColorizer;
    private RsContext mRsContext;
```

</li><li>AndroidManifest.xmlの下記<android/>タグのnameプロパティをUnityPlayerActivityからMainActivityに変更し、UnityプロジェクトのAssets/Plugin/Android/配下にコピーしておきます。

```xml
<activity ... android:name="package.name.UnityPlayerActivity" />
```
</li></ol>

## C#の世界とJavaの世界

C#のプログラムからJavaのオブジェクトへアクセスする方法は、通常のJava Native Pluginと同じ手法が利用できます。
今回の例では、RealSense D435で取得した画像データをMainActivity内のStatic Buffer領域に保存したあと、C#から読み出す方法について紹介します。
　
<ol><li>Java側の処理

データの共有に必要なバッファ領域(m_Buffer)を作成して、mStreaming()の処理内で画像をバッファ領域にコピーしています。

```java:Javaの世界実装例
public class MainActivity extends UnityPlayerActivity {
    //...
    static public byte[] m_Buffer;
    //...

@Override
    protected void onResume() {
        super.onResume();
        if(mPermissionsGrunted)
            init();
        else
            Log.e(TAG, "missing permissions");
    }

    private void init(){
        RsContext.init(mAppContext);

        mRsContext = new RsContext();
        mRsContext.setDevicesChangedCallback(mListener);
        //...
        m_Buffer =  new byte[640 * 480 * 3];

        if(mRsContext.getDeviceCount() > 0) {
            showConnectLabel(false);
            start();
        }
    }

    Runnable mStreaming = new Runnable() {
        @Override
        public void run() {
            try {
                try(FrameSet frames = mPipeline.waitForFrames(2000)) {
                    try (VideoFrame vf = frames.first(StreamType.COLOR,
                         StreamFormat.RGB8).as(VideoFrame.class)) {
                        vf.getData(m_Buffer);
                    }
                }
                mHandler.post(mStreaming);
            }
            catch (Exception e) {
                Log.e(TAG, "streaming, error: " + e.getMessage());
            }
        }
    };

    private synchronized void start() {
        if(mIsStreaming)
            return;
        try{
            Log.d(TAG, "try start streaming");
            mPipeline.start(mConfig);
            mIsStreaming = true;
            mHandler.post(mStreaming);
            Log.d(TAG, "streaming started successfully");
        } catch (Exception e) {
            Log.d(TAG, "failed to start streaming");
        }
    }
    //...
}
```

</li><li>C#側の処理

C#側では次のスクリプトファイルを実装し、テクスチャ付きのMaterialを貼り付けた3D Objectにアタッチします。

```cs:C#の世界実装例
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RealSenseScript : MonoBehaviour
{
    private Texture2D m_mainTexture;
    private AndroidJavaObject m_context;

    void Start()
    {
        m_mainTexture = (Texture2D)GetComponent<Renderer>().material.mainTexture;
	
#if UNITY_ANDROID
        // Context(Activity)オブジェクトを取得する
        AndroidJavaClass unityPlayer =
	    new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        m_context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
#endif
    }

    void Update()
    {
#if UNITY_ANDROID
        AndroidJavaClass mainActivity =
	    new AndroidJavaClass("package.name.MainActivity");
        byte[] buf = mainActivity.GetStatic<byte[]>("m_Buffer");
        for (int y = 0; y < 480; y++)
        {
            for (int x = 0; x < 640; x++)
            {
                int i = (x + y * 640) * 3;
                Color c = new Color(buf[i] / 256.0f, buf[i + 1] / 256.0f,
		          buf[i + 2] / 256.0f);
                m_mainTexture.SetPixel(639 - x, y, c);
            }
        }
        m_mainTexture.Apply();
#endif
    }
}
```

# まとめ

この記事では、Android OS上のUnityアプリケーションからRealSense SDKを用いて深度センサーデバイスを利用する方法をまとめました。

紹介した新しいRealSense SDK 2では、UVC Backend全体をAndroid OSの作法に則って全く新しく実装するという手法で、Androidの壁を越えて利用できるようになっています。
RealSense深度センサーはコンパクトで利用しやすいデバイスなので、Android端末と組み合わせて色々と面白い応用が考えられるのではないかと思っています。

また、今回の新しいRealSense SDK 2では、新しく発売されたV-SLAMデバイスのRealSense T265もサポートされているのですが、残念ながらWindows / Linux OSのみのサポートとなっています。SDK内部実装を調査したところ、T265の部分に関してはほぼ完全に新規コードで実装されており、かつLibusbライブラリの上に実装されていることが分かっています。
つまりVer2.18迄の旧来のRealSense SDKと同じ課題を抱え込んでいる状況で、一筋縄ではAndroidアプリケーションから利用できません。
Intelの将来的なサポートを期待したいところです。