THETAの中でOpenCVを動かす

本記事は[OpenCV Advent Calendar 2018](https://qiita.com/advent-calendar/2018/opencv)の17日目の記事です。

RICOH THETAというカメラの中で、OpenCVを動かしてみました。
Android Studio + NDK (ndk-build)でOpenCVを使ったアプリをビルドしたい、という方にも参考になれば幸いです。

## はじめに
こんにちは、リコーの@roohii_3です。
弊社で[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出してるのですが、このカメラの最新機種（2018年12月現在）の**RICOH THETA V**は、**Android**で動いています。
Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます。

THETAプラグインでOpenCV＋クラウドなどを組み合わせば、何かを認識して撮影・クラウドに送信、のようなIoT的な使い方もできそうです。
ということで、そのことはじめとしてTHETAプラグインでOpenCVを動かしてみました。


## 開発環境
* OpenCV Android pack ver. 3.4.4
* Android Studio ver. 3.2.1
* JDK ver. 1.8.0_191
* NDK ver. 18.1.5063045
* gradle ver. 4.6
* RICOH THETA V Firmware ver. 2.50.1<br>(Android ver. 7.1.2 / API level 25)

## 事前準備
### OpenCV Android pack
1. [OpenCVのReleasesページ](https://opencv.org/releases.html)から"**Android pack**"の最新版をダウンロード
2. ダウンロードしたファイルを解凍後、任意の場所に配置<br>
私は ”OpenCV-android-sdk” → ”OpenCV-3.4.4-android-sdk” にリネームし、以下の場所に置きました。

```
C:/opencv/OpenCV-3.4.4-android-sdk
```

### Android開発環境
#### Android Studio
[Android Developers](https://developer.android.com/studio/)より、Android Studioをダウンロード・インストール

#### Android SDK・Tools
1. Android Studioのメニューより、”**Tools > SDK Manager**”を開く
2. ”**SDK Platforms**”タブより、Androidのバージョンに応じて必要なものにチェックを入れる<br>
![SDK_download.png](https://qiita-image-store.s3.amazonaws.com/0/304709/282e6fa3-a3e5-9fb2-21f3-64394c22dd14.png)
3. ”**SDK Tools**”タブより、以下のものにチェックが入っていなかったら入れておく
    * Android SDK Build-Tools
    * Android SDK Platform-Tools
    * Android SDK Tools
    * Google USB Driver
    * NDK
4. "**OK**"をクリック

#### JDK
以下より、”**Java SE Developer Kit 8**”をダウンロード・インストール
https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

#### RICOH THETA V
THETAを使う場合は、以下のリンクを参考にして開発者モードにしておく
[【THETAプラグイン開発】プラグインパートナープログラムに登録してみよう](https://qiita.com/mShiiina/items/55d98f366e650ca42251)
[【THETAプラグイン開発】THETAを開発者モードにする手順](https://qiita.com/ueue/items/7c4027db3af463956e26)

## OpenCV for Android
OpenCVの公式ドキュメントにある"[Android Development with OpenCV](https://docs.opencv.org/3.4.4/d5/df8/tutorial_dev_with_OCV_on_Android.html)"によると、AndroidでOpenCVを使うにはいくつか方法があるようです。

1. Javaで使う方法
    1.1. OpenCV Managerを使う方法
    1.2. ライブラリを静的リンクさせる方法

2. NDKによってC/C++で使う方法
    ※ビルドに、"**CMake**"を使う方法、"**ndk-build**"を使う方法の2種類がある模様。

OpenCV公式では「1.1. OpenCV Managerを使う方法」を推奨しているみたいです[^1] [^2]。
しかし、THETAは常時ネットワークに繋がっていないため、OpenCV Managerの特徴を生かせられません。
そのため、1.2.の方法か2.の方法を使うことになります。
今回は、「**2. NDKによってC/C++で使う方法**」で実装することにします。
ビルドには”**ndk-build**”を使います。

## プロジェクトファイル・NDKビルド環境の準備
**1. Android Studioプロジェクトの新規作成**
Android Studioを開き、メニューの"File > New Project"を選択します。画面に従って"Empty Activity"を作成します。
※ **"Include C++ support"のチェックは不要です。**これにチェックを入れるとCMakeList.txtが作成されるようですが、今回はndk-buildでビルドするのでチェックの必要はありません。

**2. ビルドファイルの作成**
"app"ディレクトリの下に"jni"ディレクトリを作成し、"Android.mk"と"Application.mk"を作成します。
![mk_file.png](https://qiita-image-store.s3.amazonaws.com/0/304709/b90b2798-c820-70ca-274a-b163876ca6f1.png)
それぞれのファイルには、以下のように記入します。

**【Android.mk】**

* 7行目の`include`に、OpenCV Android packの中にある"**OpenCV.mk**"のパスを記入
* `LOCAL_MODULE`に、任意の名称（NDKで生成されるライブラリ名）を設定
* `LOCAL_SRC_FILES`に、C/C++（Nativeコード）のソース名を設定<br>
※Nativeコードが複数ある場合は、空白で区切るようです[^3]。

```Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

OPENCV_INSTALL_MODULES:=on
OPENCV_LIB_TYPE:=SHARED
include C:\opencv\OpenCV-3.4.4-android-sdk\sdk\native\jni\OpenCV.mk

LOCAL_MODULE := opencvsample
LOCAL_SRC_FILES := sample.cpp
include $(BUILD_SHARED_LIBRARY)
```

**【Application.mk】**

* `APP_ABI`には、プラットフォームに応じて適当なものを設定

```Application.mk
APP_STL := c++_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := armeabi-v7a
```

**3. OpenCVライブラリ（.soファイル）のコピー**
"app"ディレクトリ下に"jniLibs"ディレクトリを作成し、OpenCVライブラリ（.soファイル）をコピー＆ペーストします。
プラットフォームに応じて、必要なライブラリファイルをディレクトリごとコピペします。

```
コピー元: C:/(OpenCV Android packがある場所)/sdk/native/libs/armeabi-v7a
コピー先: C:/(プロジェクトファイルがある場所)/app/jniLibs/armeabi-v7a
```

![sofile.png](https://qiita-image-store.s3.amazonaws.com/0/304709/6426963b-befc-3a60-ba66-2e3dc3456275.png)

**4. build.gradleの編集**
build.gradle(Module:app)を開き、下記のようにNDK設定を追記します。

* `moduleName`には、"Android.mk"で設定した`LOCAL_MODULE`の名前の先頭に"lib"をつけたものを設定する
* `abiFilters`の設定値は、プラットフォームに応じて適当なものを設定する

```build.gradle
android {
    ...
    defaultConfig {
        ...
        ndk {
            moduleName "libopencvsample"
            abiFilters 'armeabi-v7a'
        }
    }
}
```

エディタウィンドウ上部に"**Sync Now**"が出たら、それをクリック

**5. ビルドシステムの設定**
プロジェクトツリーで"app"を右クリックし、"**Link C++ Project with Gradle**"を選択します。
開いたウィンドウの**Build System**に "**ndk-build**" 、**Project Path**に "**Android.mk**" のパスを指定し、OKをクリックします。
![setbuildsys.png](https://qiita-image-store.s3.amazonaws.com/0/304709/c9287cba-a8cc-13c0-58a4-385a92f22722.png)


## 実装
"drawable"に置いた"lena.jpg"を開き、Nativeコード内でOpenCVを動かして色空間をRGB→BGRに変換、それをImageView上に表示させる、というものを作ってみます。


### layout
バージョンを表示させる`TextView`と、処理後の画像を表示させる`ImageView`を配置しています。

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</android.support.constraint.ConstraintLayout>
```

### Javaコード
"Android.mk"で`LOCAL_MODULE`に設定したライブラリ名の先頭に"lib"を付け、`System.loadLibrary()`で読み込みます。
Nativeコード関数は、`public native String version();`のように宣言しておきます。

```MainActivity.java
package com.theta360.opencvsample;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;

import java.nio.ByteBuffer;

public class MainActivity extends AppCompatActivity {

    // load native library
    static {
        System.loadLibrary("opencvsample");
    }

    private TextView mTextView;
    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // OpenCV version
        mTextView = (TextView) findViewById(R.id.textView);
        mTextView.setText("OpenCV version: " + version());

        // load the picture from the drawable resource
        Bitmap img = BitmapFactory.decodeResource(getResources(), R.drawable.lena);

        // get the byte array from the Bitmap instance
        ByteBuffer byteBuffer = ByteBuffer.allocate(img.getByteCount());
        img.copyPixelsToBuffer(byteBuffer);

        // call the process from the native library
        byte[] dst = rgba2bgra(img.getWidth(), img.getHeight(), byteBuffer.array());

        // set the output image on an ImageView
        Bitmap bmp = Bitmap.createBitmap(img.getWidth(), img.getHeight(), Bitmap.Config.ARGB_8888);
        bmp.copyPixelsFromBuffer(ByteBuffer.wrap(dst));
        mImageView = (ImageView) findViewById(R.id.imageView);
        mImageView.setImageBitmap(bmp);
    }

    // native functions
    public native String version();
    public native byte[] rgba2bgra(int width, int height, byte[] src);
}
```

### Nativeコード
OpenCVの処理はC/C++で行います。
"**app > jni**"下に任意の名前でC/C++ファイルを作成します。
"Android.mk"の`LOCAL_SRC_FILES`には、作成したファイルの名称を設定しておきます。
![cppsource.png](https://qiita-image-store.s3.amazonaws.com/0/304709/d3bcb06d-962e-ecc7-c0cd-e9fb565cd33c.png)

Java ↔ Native(C/C++)のやり取りで用いる関数には、特殊なルールがあるようです。
以下のように、定義します。

```.cpp
JNIEXPORT [戻り値の型] JNICALL Java_[最上位ソースディレクトリからのJavaソースの相対パス]_[呼び出し元のJavaのクラス名]_[関数名](JNIEnv *, jobject, [引数], ...))
```

戻り値や引数の型も特殊のものを使うようです。
詳しくは、[JNIの型とデータ構造](https://docs.oracle.com/javase/jp/1.5.0/guide/jni/spec/types.html)等を参考にしてください。

```sample.cpp
#include <jni.h>
#include <string>
#include <opencv2/core.hpp>
#include <cv.hpp>

extern "C"
{
    JNIEXPORT jstring JNICALL
    Java_com_theta360_opencvsample_MainActivity_version(
            JNIEnv *env,
            jobject) {
        std::string version = cv::getVersionString();
        return env->NewStringUTF(version.c_str());
    }

    JNIEXPORT jbyteArray
    JNICALL Java_com_theta360_opencvsample_MainActivity_rgba2bgra
            (
                    JNIEnv *env,
                    jobject obj,
                    jint w,
                    jint h,
                    jbyteArray src
            ) {
        // 要素列の取得
        // 最後に開放する必要がある
        jbyte *p_src = env->GetByteArrayElements(src, NULL);
        if (p_src == NULL) {
            return NULL;
        }

        // 配列をcv::Matに変換する
        cv::Mat m_src(h, w, CV_8UC4, (u_char *) p_src);
        cv::Mat m_dst(h, w, CV_8UC4);

        // OpenCV process
        cv::cvtColor(m_src, m_dst, CV_RGBA2BGRA);

        // 配列をcv::Matから取り出す
        u_char *p_dst = m_dst.data;

        // 戻り値用に要素を割り当てる
        jbyteArray dst = env->NewByteArray(w * h * 4);
        if (dst == NULL) {
            env->ReleaseByteArrayElements(src, p_src, 0);
            return NULL;
        }
        env->SetByteArrayRegion(dst, 0, w * h * 4, (jbyte *) p_dst);

        // release
        env->ReleaseByteArrayElements(src, p_src, 0);

        return dst;
    }
}
```

## 実行
実行すると、下記のように表示されます。
※THETAには画面がないので、[Vysor](https://www.vysor.io/)を使っています。
![screenshot.jpg](https://qiita-image-store.s3.amazonaws.com/0/304709/2f13068f-d803-2df4-1899-8d2d2d7d71cd.jpeg)

## おわりに
THETA内でOpenCVの動作確認をしてみただけなので、もうちょっと360度映像の特性を活かしたものを作ってみたいです。
何かできたらまた書こうと思います。

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

明日は@n_uehさんの**「OpenCV×VUIについて」**の記事です。

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

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

[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://docs.opencv.org/3.4.4/d5/df8/tutorial_dev_with_OCV_on_Android.html)に、”Using async initialization is a **recommended** way for application development. It uses the OpenCV Manager to access OpenCV libraries externally installed in the target system.”とあります。
[^2]: OpenCV Managerについては → [[Android OpenCV Manager]](https://docs.opencv.org/2.4.13.7/platforms/android/service/doc/index.html)に詳細が書いてありました。ver.2.4のドキュメントなので情報が古いかもですが。
[^3]: https://developer.android.com/ndk/guides/android_mk?hl=ja