THETAの中でOpenCVを動かす【プレビューフレーム取得編】

## はじめに
こんにちは、リコーの@roohii_3です。

以前、「[THETAの中でOpenCVを動かす][1]」という記事を書きました。
今回は、**THETAからプレビュー映像を取得し、OpenCVでリアルタイムに処理**してみたのでノウハウを紹介します。
下のような感じで、プレビュー画面を取得してリアルタイムで二値化してみるものを作ってみました。
![rgb2binarization.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304709/8f1a56c7-a2d4-d8de-a741-cd7e74197c0a.jpeg)
コードは以下で公開しています。
[https://github.com/theta-skunkworks/theta-plugin-opencv-preview-sample](https://github.com/theta-skunkworks/theta-plugin-opencv-preview-sample)

※ 今回のコードでは、画像処理後のデータの保存は行っていません。
プレビュー画面を確認するには、[Vysor][2]を使ってください。

## 開発環境
* OpenCV Android pack ver. 3.4.5
* Android Studio ver. 3.3+
* gradle ver. 5.1.1
* RICOH THETA V Firmware ver. 2.50.1

## 事前準備
### RICOH THETA V
以下のリンクを参考にして、THETAを開発者モードにしておきます。

* [【THETAプラグイン開発】プラグインパートナープログラムに登録してみよう](https://qiita.com/mShiiina/items/55d98f366e650ca42251)
* [【THETAプラグイン開発】THETAを開発者モードにする手順](https://qiita.com/ueue/items/7c4027db3af463956e26)

### THETA Plugin SDK (Android Studioプロジェクトファイル)
THETA Plugin SDKをベースに作っていきます。以下からダウンロードします。
[https://github.com/ricohapi/theta-plugin-sdk](https://github.com/ricohapi/theta-plugin-sdk)

### OpenCV Android pack
 [OpenCVのReleasesページ](https://opencv.org/releases.html)から、**ver3.x**の最新版の"**Android pack**"をダウンロードします。
※ OpenCV4も出てますが、APIレベルがTHETAに合っていないため、THETA上で動かない可能性があります。そのため**OpenCV3**を入れました。

### Vysor
プレビュー画面を確認するために、Vysorを利用します。
以下からインストールしておきます。
[https://www.vysor.io/][2]



## プロジェクトファイルの準備
### プロジェクトファイルを開く
Android Studioを起動し、"File > Open"から、ダウンロードしてきた**THETA Plugin SDK**を開きます。
アプリケーションIDや、お好みでパッケージ名・プラグイン名を変更します(→[参考](https://qiita.com/roohii_3/items/5fbb3cb5645c9300ce69#2-%E5%90%8D%E7%A7%B0%E5%A4%89%E6%9B%B4))。

### プロジェクトにOpenCVをインポートする
[前回の記事][1]では、NDK(C/C++)でOpenCVを使う方法を紹介しましたが、今回はjava上でOpenCVを使ってみます。
前回の記事に書いた、「[1.2. ライブラリを静的リンクさせる方法](https://qiita.com/roohii_3/items/63a546169981f41967df#opencv-for-android)」に当たります。

#### 手順
1. "File > New > Import module"を選択し、"Source directory"に"`C:/(OpenCVを置いた場所)/sdk/java`"と入力
![import-module.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304709/e8ca4e75-f6df-bd5e-c94c-cf6c9addf175.png)

2. "app"ディレクトリ下に"jniLibs"ディレクトリを作成し、OpenCVライブラリ（.soファイル）をコピペ

    ```
    コピー元: C:/(OpenCVを置いた場所)/sdk/native/libs/arm64-v8a
    コピー先: C:/(プロジェクトファイルがある場所)/app/src/main/jniLibs/arm64-v8a
    ```

    ![lib.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304709/15e76bf4-c072-a4b0-bc0d-90551221a84f.png)

3. "File > Project Structure"を開き、"app"を選択→"Dependencies"タブ→”+”→”Module dependency”で"openCVLibrary345"を選択→"OK"
![module-dependency.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304709/5f8dee20-c919-e607-b43b-4dd6c4ac37ae.png)


本記事の方法でOpenCVを呼び出すには、`OpenCVLoader.initDebug()`を使います。

```MainActivity.java
protected void onResume() {
    super.onResume();
    if (!OpenCVLoader.initDebug()) {
        Log.d(TAG, "Internal OpenCV library not found.");
    } else {
        Log.d(TAG, "OpenCV library found inside package.");
        mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
    }
}
```

OpenCV Managerを使う場合は、`OpenCVLoader.initAsync()`を使います（→[参考](https://docs.opencv.org/3.4.5/d5/df8/tutorial_dev_with_OCV_on_Android.html)）。


## 実装

### OpenCVをTHETAで使うときのポイント
OpenCVのAndroid packには、いくつかサンプルアプリが用意されています。
しかし、THETAでそのまま使うと以下のようなエラーが出てしまいます。
![error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304709/12fd526a-2fdb-c7b7-22c4-0ea038b6f71f.png)
OpenCVのサンプルアプリ内では`Camera API`が使われており、それによってカメラの制御を行います。
しかしTHETAプラグインのデフォルトでは`Camera API`がロックされているために、このような表示が出てしまいます。
THETAでOpenCVを使うには、以下のポイントがあります。

#### プラグイン内でCamera APIを使えるようにする
THETAのプレビュー画面を取得するには、`Camera API`を使うと便利です。
[ドキュメント](https://api.ricoh/docs/theta-plugin-reference/broadcast-intent/)によると、Broadcast Intentで"`com.theta360.plugin.ACTION_MAIN_CAMERA_CLOSE`"を使うと、プラグイン内で`Camera API`を使えるようになります。
終了時には、"`com.theta360.plugin.ACTION_MAIN_CAMERA_OPEN`"を使ってカメラリソースを開放する必要があります。
実際には、`PluginActivity`を継承したクラス内で、開始時には`notificationCameraClose()`を、終了時には`notificationCameraOpen()`あるいは`close()`を使えばこれらのIntentを行ってくれます。

#### THETA独自のパラメータを使う
OpenCVには`JavaCameraView`というクラスが用意されており、内部では`Camera API`で諸々の設定や操作がされているようです。
しかしTHETAで使うには、**THETA独自のパラメータ**を設定する必要があります。
THETA独自のCamera APIパラメータは、以下にまとまっています。
[https://api.ricoh/docs/theta-plugin-reference/camera-api/](https://api.ricoh/docs/theta-plugin-reference/camera-api/)

たとえばOpenCVのサンプルアプリでは、カメラ起動時に"`RIC_SHOOTING_MODE`"というパラメータに"`RicMoviePreview1920`"といった値を設定すると動くようになりました。


### ソースコード
重要そうな部分だけ抜粋します。
大枠はOpenCVのサンプルコードの"`tutorial-x-xxx`"を参考に作っています。
ソースコードの詳細は[GitHub](https://github.com/theta-skunkworks/theta-plugin-opencv-preview-sample)をご覧ください。

#### Camera APIの有効化・無効化
ポイントに書いたように、プラグイン起動時に`notificationCameraClose()`、終了時に`close()`を呼び、それぞれCamera APIの有効化と無効化を行っています。
※ `onPause()`内で`close()`を呼ぶと失敗してしまうことがあり、「Modeボタン長押し(プラグイン終了の合図)」されたときに`close()`を呼ぶようにしました。

```MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    
    // ...略...

    // Set a callback when a button operation event is acquired.
    setKeyCallback(new KeyCallback() {
        @Override
        public void onKeyDown(int keyCode, KeyEvent keyEvent) {}

        @Override
        public void onKeyUp(int keyCode, KeyEvent keyEvent) {}

        // 終了時(Modeボタン長押し)に呼ばれる
        @Override
        public void onKeyLongPress(int keyCode, KeyEvent keyEvent) {
            if (keyCode == KeyReceiver.KEYCODE_MEDIA_RECORD) {
                if (!isEnded) {
                    // ...略...
                    
                    // Camera APIの無効化
                    close();
                    isEnded = true;
                }
            }
        }
    });

    // Camera APIの有効化
    notificationCameraClose();

    // ...略...
}

```

#### THETA独自のカメラ制御クラス
`JavaCameraView`をそのままTHETA用にアレンジした`ThetaView`というクラスを追加しました。
`initializeCamera()`メソッド以外はほぼ`JavaCameraView`のままです。
しかし`initializeCamera()`内も、ポイントに書いたように「"`RIC_SHOOTING_MODE`"というパラメータに"`RicMoviePreview1920`"を設定する」ことと、不必要そうな記述を省いただけの変更です。

```ThetaView.java
public class ThetaView extends CameraBridgeViewBase implements PreviewCallback {

    // "RicMoviePreviewXXXX"パラメータの値に応じて、フレームサイズを定義する
    private static final int PREVIEW_SIZE_WIDTH = 1920;
    private static final int PREVIEW_SIZE_HEIGHT = 960;
    private static final String PREVIEW_SIZE_PARAM = "RicMoviePreview1920";


    protected boolean initializeCamera(int width, int height) {
        boolean result = true;
        synchronized (this) {
            mCamera = null;

            mCamera = Camera.open();
            if (mCamera == null)
                return false;

            /* Now set camera parameters */
            try {
                Camera.Parameters params = mCamera.getParameters();

                // フレームサイズを設定
                Size frameSize = new Size(PREVIEW_SIZE_WIDTH, PREVIEW_SIZE_HEIGHT);
                Log.d(TAG, "Set preview size to " + Integer.valueOf((int) frameSize.width) + "x" + Integer.valueOf((int) frameSize.height));
                params.setPreviewSize((int) frameSize.width, (int) frameSize.height);

                // 撮影モードを設定
                params.set("RIC_SHOOTING_MODE", PREVIEW_SIZE_PARAM);

                mCamera.setParameters(params);

                // ...略...

                /* Finally we are ready to start the preview */
                Log.d(TAG, "startPreview");
                mCamera.startPreview();

            } catch (Exception e) {
                result = false;
                e.printStackTrace();
            }
        }

        return result;
    }
```

#### 画像処理部分
`CvCameraViewListener2`を`implements`するクラスの、`onCameraFrame()`メソッドでプレビュー画面の1フレームを取得できます。
`rgba()`でRGBAが、`gray()`ではグレースケールで取得できました。
ここではグレースケースでフレームを取得し、OpenCVを使って2値化する処理を行っています。

```MainActivity.java
public class MainActivity extends PluginActivity implements CvCameraViewListener2 {

    private ThetaView mOpenCvCameraView;
    private Mat mOutputFrame;

    // ...略...

    public void onCameraViewStarted(int width, int height) {
        mOutputFrame = new Mat(height, width, CvType.CV_8UC1);
    }

    public void onCameraViewStopped() {
        mOutputFrame.release();
    }

    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        // rgba()でRGBAが、gray()でグレースケールでプレビューの1フレームが取得できる
        Mat gray = inputFrame.gray();

        // do some image processing
        // ここでは2値化処理だけ
        Imgproc.threshold(gray, mOutputFrame, 127.0, 255.0, Imgproc.THRESH_BINARY);

        return mOutputFrame;
    }

```

## 実行
USBでTHETAを接続し、Vysorを起動して実行してください。

### パーミッションの設定
一番始めにプラグインを起動したときは、「パーミッション」に許可が入っていないためすぐプラグインが落ちてしまいます。
Settingsアプリを開き、"OpenCV Preview Sample > Permissions"より、"Camera"の許可をONにしてください。
![permissions.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304709/c14c7f6b-167e-afce-7b47-13bb97fab35b.jpeg)


パーミッション許可を入れ、再度プラグインを実行すると、以下のような画面が出てきます。
![binarization.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304709/440710ef-52f5-780a-27cb-74eb8cd342bc.jpeg)


## おわりに
今回のように簡単な処理だったら、遅延なくサクサク動きました。
もうちょっと複雑な処理だとどうなるか見てみたいです。
また、今回はプレビュー画面を取得するだけだったので、撮影処理もチャレンジしてみたいです。
また何かできたら記事書きます。

**続編書きました↓**
[THETAの中でOpenCVを動かす【プレビューフレーム応用編】](https://qiita.com/roohii_3/items/75c3e73cee0ed6d53b4b)(2019/06/05 追記)


### RICOH THETAプラグインパートナープログラムについて
THETAプラグインに興味を持たれた方がいれば、以下の記事もぜひご覧ください。

* [THETAプラグインパートナープログラムはじめました](https://qiita.com/mrtakada/items/8f3bbf8d283c26899473)
* [【THETAプラグイン開発】プラグインパートナープログラムに登録してみよう](https://qiita.com/mShiiina/items/55d98f366e650ca42251)

[RICOH THETAプラグイン開発者コミュニティ](https://qiita.com/organizations/theta-plugin)では、他にも記事を書いています。
RICOH THETAプラグインについては[こちら](https://api.ricoh/products/theta-plugin/)。興味を持たれた方は[twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発コミュニティ(slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もぜひどうぞ｡


[1]:https://qiita.com/roohii_3/items/63a546169981f41967df
[2]:https://www.vysor.io/