ネイティブでOpenCVをリンクし、ちょろっと動かしてみる

ZOZOテクノロジーズ15日目の担当です。CMakeListを使って、OpenCVをネイティブでリンクさせ、動かす簡易的なサンプルを紹介します。なお、OpenCVは1mmも詳しくありません。

##はじめに
NDKを利用しますので、`Start a new Android Project`から`Include C++ support`にチェックをつけ新プロジェクトを立ち上げます。最後にC++ライブラリサポートの選択をしますが、特に問題がない限り`default`で問題ありません。Finishするとプロジェクトが作られ、Android Studioが自動で`CMakeLists.txt`というC++側のビルドスクリプトのテンプレートを自動生成してくれて楽チンです。なお、前提条件としてAndroid Studioでのネイティブの実装環境設定に関しては、[プロジェクトへのC/C++コードの追加](https://developer.android.com/studio/projects/add-native-code)を参照してください。

##OpenCVの展開
[OpenCVのリリースブランチ](https://github.com/opencv/opencv/releases/)から適当なSDK(opencv-x.x.x-android-sdkとなっているもの)をダウンロードします。今回は[3.4.4](https://github.com/opencv/opencv/releases/tag/3.4.4)を使います。場所は任意ですが、zipを展開し、`sdk`というディレクトリをプロジェクトのルート直下などに移動します。OpenCVは以下のようなファイル構成になっています。

```cmake
sdk 
 ┃
 ┣━ etc: パターン検出に必要なカスケード分類機などがまとめられてる
 ┃
 ┣━ java: Java用のファイルがまとめられている 
 ┃
 ┗━ native: リンク用のso, aファイルがまとめられている
            *so:動的リンクライブラリ a:静的リンクライブラリ
```

このなかで、javaアプリケーション内だけでOpenCVを使いたい場合は、javaディレクトリをプロジェクトライブラリとしてimportします。Linuxカーネルに近い位置でOpenCVを動かしたい場合は`native`ファイルを使います。OpenCVを使う場合は、高度な計算処理を必要とするので、だいたい後者になるのかなと思われます。

##OpenCVをリンクする
Android Studioでプロジェクトを作った時に自動で`cpp/native-lib.cpp`というファイルが生成されたと思います。ファイル名はなんでもいいのですが、この場合、ビルド時に`native-lib.so`もしくは`native-lib.a`といったライブラリとなり、これをjava側からロードすることで、定義した関数を呼び出すことが可能になります。ここで、OpenCVの関数を呼びたい場合、native-lib.cpp内でOpenCVのヘッダーファイルをincludeする必要がありますが、普通にincludeしてもコンパイルエラーになります。includeを有効にするにはCMakelists.txtで以下のように指示する必要があります。

```cmake
include_directories(${OPENCV_INCLUDE_DIR})
```
ここでいう、OPENCV_INCLUDE_DIRはヘッダーファイルがある場所を意味してます。このサンプルでは、以下の場所にあるので、

```cmake
sdk
 ┃
 ┗━ native
    ┃  
    ┗━ jni
        ┃
        ┗━ include
            ┃
            ┣━ opencv: .hファイルがまとまってます
            ┃
            ┗━ opencv2: .hファイルがまとまってます
```

Cmakelists.txtがある場所からなので、この場合は

```cmake
set( OPENCV_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/../sdk/native/jni/include")
```
みたいになります。
おそらくこれで、コンパイルエラーはなくなると思いますが、このままではOpenCVの実体がないので、試してないですが、実行時にリンクエラーがでるか、ビルドエラーが起こると思います。なのでOpenCVのライブラリをリンクするする必要があります。まずはリンクするライブラリを宣言する必要があります。

```cmake
add_library(
        lib_opencv ##リンクするライブラリの名称
        SHARED ## 動的リンクライブラリ 静的リンクライブラリの場合はSTATIC
        IMPORTED)
```
また、今回の例ではOpenCVの動的リンクライブラリをリンクしたいので、`SHARED`を指定しています。そして、実際のsoファイルがある場所は以下です。

```cmake
sdk
 ┃
 ┗━ native
    ┃  
    ┗━ libs
        ┃
        ┣━ arm64-v8a
        ┃    ┃
        ┃    ┗━ libopencv_java3.so
        :
        ┃
        ┗━ x86_64
             ┃
             ┗━ libopencv_java3.so
```
リンクしたいライブラリの場所を、以下のように指示します。

```cmake
set( OPENCV_LIB_DIR "${PROJECT_SOURCE_DIR}/../opencv-sdk/native/libs" )

set_target_properties(
        lib_opencv
        PROPERTIES IMPORTED_LOCATION
        ${OPENCV_LIB_DIR}/${ANDROID_ABI}/libopencv_java3.so)
```
`${ANDROID_ABI}`は、AndroidのCPUアーキテクチャ(`armeabi`, `armeabi-v7a`, `arm64-v8a`など)のことで、OpenCVはアーキテクチャ毎にディレクトリがきられており、各アーキテクチャ名以下の.so(もしくは.a)ファイルを指定します。基本的にネイティブ側で実装する場合は、アーキテクチャを意識する必要がありますが、最近では、ほぼ`arm64-v8a`かと思われます。なお、CPUアーキテクチャについては[ABI管理](https://developer.android.com/ndk/guides/abis)を参照してください。

最後にOpenCVのsoライブラリを`natib-lib`ライブラリとリンクを指示します。

```cmake
target_link_libraries(
        native-lib
        lib_opencv)
```

これで、OpenCVの関数を呼び出せるようになったと思います。

##ネイティブの関数を呼び出す
C/C++のコードをJava側から呼び出すのは、ネイティブ側で`Java_パッケージ名_利用するクラス名_関数名`、いわゆるJNIというインタフェーズを定義し、Java側から呼び出します。Java側からはJNIを以下のように宣言します。てか、Javaだと`alt+enter`の候補でJNI生成があるけどkotlinだとなんかできなくなってて少し不便。。

```java
    private external fun nativeCreateObject(cascadeName: String, minFaceSize: Int): Long
```

```c++
Java_jp_zzt_facedetection_DetectionBasedTracker_nativeCreateObject(JNIEnv *jenv, jclass type, jstring cascadeName_, jint minFaceSize) {
```
で、少し脱線しますが、個人的に面倒と思っているのがJavaとJNI間でのオブジェクトのやり取りです。JavaからCの世界にいくから仕方がないのか、、ここら辺でうまい具合にできる方法があれば知りたいです。例えばこんなクラスがあったとすると

```java
class Data {
    var num: Int = 0
    var buffer: ByteArray? = null
}
```
このクラスを引数にして、ネイティブで扱う場合、こんな具合な変換処理が必要です。なんか長くて眠くなるレベルです。もう少しわかりやすくかけるのかもしれませんが、あまり調べてないです。

```c++
    int num = env->GetIntField(data, jp_zzt_facedetection_Data__fieldID_num);
    jbyteArray dataArray = static_cast<jbyteArray>(env->GetObjectField(data, jp_zzt_facedetection_Data__fieldID_buffer));
    jbyte *bytes = env->GetByteArrayElements(dataArray, false);
```
逆にJavaのオブジェクトをネイティブ側から渡したい場合は、オブジェクト生成用のメソッドをネイティブから呼び出します。これも、もっと良い方法がある気がします。。

```java
    fun createData(num: Int, buffer: ByteArray?): Data {
        return Data(num, buffer)
    }
```
リフレクションみたいで、きもいです。オブジェクトのクラス名、メソッドのID、第1,2引数を指定します。メソッドIDには、そのメソッドのシグネチャーが必要になります。

```c++
    jclass clz = env->FindClass("jp/zzt/facedetection/Data");
    jmethodID methodId = env->GetStaticMethodID(clz, "createData", "(I[B)Ljp/zzt/facedetection/Data;");
    return env->CallStaticObjectMethod(clz, methodId, ret->num, ret->buffer);
```
ちなみにシグネチャの確認方法は、[こんな感じ](https://qiita.com/ryohey/items/39509ca69ba9523e3952)です。なおkotlinの場合は、`kotlinc`というコンパイラが必要になります。

## OpenCVで顔が検出できるか試す
実際に、OpenCVのサンプルである[FaceDetection](https://github.com/opencv/opencv/tree/master/samples/android/face-detection)を元にOpenCVをネイティブで動かしてみて検出できるか試してみました。


![device-2018-12-13-180211.png](https://qiita-image-store.s3.amazonaws.com/0/218011/76c8ed2f-8c4c-546d-b9e0-863260ea7ae7.png)
![device-2018-12-13-180256.png](https://qiita-image-store.s3.amazonaws.com/0/218011/f4ab27eb-c788-ddad-de19-698b6408aac2.png)
![device-2018-12-13-180457.png](https://qiita-image-store.s3.amazonaws.com/0/218011/cc68fd22-8764-ca70-92af-8e97fe4d604c.png)
ちゃんと顔(ねこ)の検出が成功しました。

##まとめ
以上、OpenCVをCMakeListでリンクさせるサンプルを紹介しました。もし、間違っている箇所があればご容赦ください。今回のサンプルではヘッダーファイルのある場所、ライブラリがある場所を指定するぐらいですが、色々とやれることは[ありそう](https://cmake.org/cmake/help/v3.4/manual/cmake-commands.7.html)です。OpenCVライブラリをリンクするだけなら、さほど難しくはないですね。ちなみにjavaでOpenCVを使いたい場合は、単純にsdkのjavaディレクトリをAndroid Studiosでライブラリプロジェクトとしてimportするだけで使えるはずです。今回初めてOpenCVをサンプルで実装してみましたが少し興味が湧いて、どうにか検出した顔の上にマスクする処理を実装しようとしましたが、OpenCVレベル2ぐらいなので、完封負けしました。そのうち挑戦したいです。