Androidで OpenCV 4を使う方法とカメラライブビューの表示

# この記事について
カメラ画像をネガポジ反転して出力するだけの簡単なAndroidアプリを作ります。
カメラ入力にはOpenCV4を使用します。
カメラ画像を用いて何らかのアプリを作るためのベースとなるプロジェクトとして利用できます。
この記事では、OpenCV Managerは使わずにAPK内にOpenCVライブラリを含む方法で説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/5c63e05d-9740-c827-2c37-b5f7d068c28f.png)

**追記(2020/07/18)** 最近のAndroid Studio (またはAndroid SDK) では、この方法は使えないようです。Camera2 APIかCamera Xを使った方が良いです。 CameraXを使う方法はこちらにまとめました。
https://qiita.com/iwatake2222/items/c0ebe6d84afdef57aab3

## 開発環境
- Windows 10 64-bit
- Android Studio 3.3.2
- 開発言語: Java
- OpenCV 4.1.0
- 対象とするデバイス: Galaxy S7

## 事前準備
### Android Studioのインストール
https://developer.android.com/studio

adbのあるフォルダにパスを通しておくと、色々と便利かも。(本記事では特に使用しません)

### OpenCVのダウンロード
https://github.com/opencv/opencv/releases 
から`opencv-4.1.0-android-sdk.zip` をダウンロードして、適当な場所に展開しておきます。バージョンは好きなのを選んでください。

以後、展開先のパスを`{OpenCV-android-sdk}` として説明を記載します。

# 自分のプロジェクトとapkにOpenCVを取り込む
## 新規プロジェクトの作成
- Android Studioを開く
- `Start a new Android Studio project` をクリック
- `Empty Activity` を選び、Next
- アプリケーション名などを設定する
	- 試すだけなら全部デフォルトで良いです
	- 今回は、開発言語にJava、Minimum API levelはAPI 21としました
	- リリースしたりする予定がある場合は、package nameをちゃんと決めてください
- Android端末をUSBでPCに接続します
- メニューバーの`Run` -> `Run'App'` を選び、Hello Worldが出力されるデフォルト状態のアプリが、Android端末で起動すればOKです

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/f5e57cbf-44cd-862f-a098-8a40c2a7fbca.png)


## OpenCVを取り込む
- Import Module
	- メニューバーの`File` -> ``New` -> `Import Module` を選ぶ
	- `Source directory` に、先ほどダウンロード・展開した、`{OpenCV-android-sdk}/sdk` を指定
		- OpenCV 3.x以前だと`{OpenCV-android-sdk}/sdk/java` を指定？
	- `Module name` がデフォルトだと`sdk` となり分かりづらいので、適当に`opencv` としておきます
	- その他設定はデフォルトのままで、`Next` 、`Finish`

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/fa988a77-d484-80e3-50e4-6246dbe9404e.png)


- 依存関係の設定
	- メニューバーの`File` -> `Project Structure` を選ぶ
	- 左側の`app` を選び、`Dependencies` タブを選ぶ
	- 右上の`+(add)` をクリックし、`3: Module dependenciy` を選ぶ
	- `opencv` を選らんで、OK

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/83990cee-9357-51b1-c5a8-c57ce290f3a9.png)

- (不要) OpenCVライブラリ(soファイル)のコピー
	- 以前だと、OpenCVライブラリ(soファイル)を、自分で`jniLibs`にコピーする必要があったのですが、今では(OpenCV4以降??)不要なようです

- (不要) AndroidManifest, build.gradleの修正
	- 以前だと、OpenCVの`AndroidManifest.xml` 内で、`targetSdkVersion` 等が指定されているのを削除したり、`build.gradle` 内で各種sdkVersion(`compileSdkVersion` 等)をappとそろえる必要がありました。
	- 今では(OpenCV4以降??)不要なようです

## OpenCVのロード
OpenCVを使用するActivityのコード(プロジェクト生成時にEmpty Activityを選び、何も設定を変えていなかったら、`app\src\main\java\com\example\myapplication\MainActivity.java` )を以下のように編集します。

実際には、どちらか片方で十分です。実プロダクトでは、`System.loadLibrary` だけを使うと思います。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("opencv_java4");     // 追加
    }

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

        OpenCVLoader.initDebug();	// 追加
    }
}
```

この状態でRunして、Logcatに以下のようなログが出たらOKです。

```txt:Logcat出力
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper: General configuration for OpenCV 4.1.0 =====================================
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:   Version control:               4.1.0
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:   Platform:
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:     Timestamp:                   2019-04-07T19:03:43Z
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:     Host:                        Linux 4.15.0-47-generic x86_64
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:     Target:                      Android 1 aarch64
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:     CMake:                       3.6.0-rc2
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:     CMake generator:             Ninja
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:     CMake build tool:            /opt/android/android-sdk.gradle/cmake/3.6.4111459/bin/ninja
2019-05-25 18:50:20.050 29439-29439/com.example.myapplication I/OpenCV/StaticHelper:     Configuration:               Release
```

なんか、Logcatの上の方を見ていると、`OpenCV/StaticHelper: OpenCV error: Cannot load info library for OpenCV` こんなエラーが出ているのですが、動作には影響がありませんでした。

# カメラライブビュー表示
ここまでで、OpenCVを取り込んだプロジェクトを準備できたので、カメラのライブビューを表示するアプリケーションを作ってみようと思います。

## パーミッションの設定
カメラを使うための設定をします

### マニフェストの設定
まず、「このアプリケーションはカメラを使うよ」と宣言しておく必要があります。

`app/src/main/AndroidManifest.xml` に以下の3行を追加します。追加するのは、`<manifest>` の直下です。既にある`<application>` と同列に追加します。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">

    <!--↓追加 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-feature android:name="android.hardware.camera" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
    <!--↑追加-->

    <application
        android:allowBackup="true"
省略
    </application>

</manifest>
```

### 許可をもらう
実は、マニフェストの設定だけでも事足ります。
しかし、これだけだと、ユーザがアプリの管理画面から権限設定をする必要があります。通常のユーザはその画面がどこにあるかも知りません。

なので、アプリの方から自発的に「許可をください」と要求画面を出します。

以下のようなコードで、もしも許可設定されていなかったら、許可要求画面を出すようにします。呼び出し元のコードは後程示します。

```java:MainActivity.java
    public static boolean getPermissionCamera(Activity activity) {
        if (ContextCompat.checkSelfPermission(
                activity,
                android.Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            String[] permissions = new String[]{Manifest.permission.CAMERA};
            ActivityCompat.requestPermissions(
                    activity,
                    permissions,
                    0);
            return false;
        } else {
            return true;
        }
    }
```

## CameraViewの配置
レイアウトに、CameraViewを追加します。`layout/activity_main.xml` を開き、Textモードで、`org.opencv.android.JavaCameraView` を追加します。

```xml: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:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!--↓追加-->
    <org.opencv.android.JavaCameraView
        android:id="@+id/camera_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible"
        tools:layout_editor_absoluteX="0dp"
        tools:layout_editor_absoluteY="0dp"
        app:show_fps="true"/>
    <!--↑追加-->
</android.support.constraint.ConstraintLayout>
```

## コード全文
コード全文を以下に示します。

- `CameraBridgeViewBase.CvCameraViewListener` インターフェイスを継承します。
	- `MainActivity` に、`implements CameraBridgeViewBase.CvCameraViewListener` を追加後、Alt+Enterで適当にimportを追加してもらいます。再度、Alt+Enterで、Implement Methods をして必要な関数を追加してもらいます
- `CameraBridgeViewBase` を宣言して、先ほどレイアウトに配置した`JavaCameraView` に割り当てます。`setCvCameraViewListener` でこのActivityを指定して、先ほど追加した`onCameraViewStarted`、`onCameraViewStopped`、`onCameraFrame` を呼んでもらいます。
- `onCameraFrame` は毎フレーム呼ばれる関数です。引数(`inputFrame` )にカメラからの入力画像が入り、戻り値に表示したいMat を設定します。
	- この`onCameraFrame` はUIスレッドとは別スレッドで動いているので、ここで画像処理等多少重たい処理をやっても大丈夫そうです。Best Practiceかどうかは分かりませんが。
	- 今回はネガポジ反転してみました
	- `onCameraFrame` での処理が重くなると、描画のfpsも当然遅くなります。これは実装者にとってはうれしいことです。`onCameraFrame` 内で重い処理をやっている最中に、再度`onCameraFrame` 関数が呼ばれることはないようです。なので、リエントラントにする必要はなさそうです。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener {
    private CameraBridgeViewBase m_cameraView;

    static {
        System.loadLibrary("opencv_java4");
    }

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

        getPermissionCamera(this);
//        OpenCVLoader.initDebug();
        m_cameraView = findViewById(R.id.camera_view);
        m_cameraView.setCvCameraViewListener(this);
        m_cameraView.enableView();
    }

    public static boolean getPermissionCamera(Activity activity) {
        if (ContextCompat.checkSelfPermission(
                activity,
                android.Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            String[] permissions = new String[]{Manifest.permission.CAMERA};
            ActivityCompat.requestPermissions(
                    activity,
                    permissions,
                    0);
            return false;
        } else {
            return true;
        }
    }

    @Override
    public void onCameraViewStarted(int width, int height) {

    }

    @Override
    public void onCameraViewStopped() {

    }

    @Override
    public Mat onCameraFrame(Mat inputFrame) {
        // ここで何らかの画像処理を行う
        // 試しに、ネガポジ反転してみる
        Core.bitwise_not(inputFrame, inputFrame);
        return inputFrame;
    }
}
```

機種や、デバイスの向き、設定によっては画面が回転していたりする可能性があります。それらの調整をやるとコードと説明が複雑になるので省略しています。
同様に、onDestoryやonResumeでの終了、復帰も省略しています。

# おわりに
- AndroidでOpenCV4を使う手順をまとめました
- OpenCV4を使ってカメラプレビューを取得、表示する手順をまとめました