LoginSignup
8
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-14

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++コードの追加を参照してください。

OpenCVの展開

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

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で以下のように指示する必要があります。

include_directories(${OPENCV_INCLUDE_DIR})

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

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

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

set( OPENCV_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/../sdk/native/jni/include")

みたいになります。
おそらくこれで、コンパイルエラーはなくなると思いますが、このままではOpenCVの実体がないので、試してないですが、実行時にリンクエラーがでるか、ビルドエラーが起こると思います。なのでOpenCVのライブラリをリンクするする必要があります。まずはリンクするライブラリを宣言する必要があります。

add_library(
        lib_opencv ##リンクするライブラリの名称
        SHARED ## 動的リンクライブラリ 静的リンクライブラリの場合はSTATIC
        IMPORTED)

また、今回の例ではOpenCVの動的リンクライブラリをリンクしたいので、SHAREDを指定しています。そして、実際のsoファイルがある場所は以下です。

sdk
 ┃
 ┗━ native
    ┃  
    ┗━ libs
        ┃
        ┣━ arm64-v8a
        ┃    ┃
        ┃    ┗━ libopencv_java3.so
        :
        ┃
        ┗━ x86_64
             ┃
             ┗━ libopencv_java3.so

リンクしたいライブラリの場所を、以下のように指示します。

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管理を参照してください。

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

target_link_libraries(
        native-lib
        lib_opencv)

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

ネイティブの関数を呼び出す

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

    private external fun nativeCreateObject(cascadeName: String, minFaceSize: Int): Long
Java_jp_zzt_facedetection_DetectionBasedTracker_nativeCreateObject(JNIEnv *jenv, jclass type, jstring cascadeName_, jint minFaceSize) {

で、少し脱線しますが、個人的に面倒と思っているのがJavaとJNI間でのオブジェクトのやり取りです。JavaからCの世界にいくから仕方がないのか、、ここら辺でうまい具合にできる方法があれば知りたいです。例えばこんなクラスがあったとすると

class Data {
    var num: Int = 0
    var buffer: ByteArray? = null
}

このクラスを引数にして、ネイティブで扱う場合、こんな具合な変換処理が必要です。なんか長くて眠くなるレベルです。もう少しわかりやすくかけるのかもしれませんが、あまり調べてないです。

    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のオブジェクトをネイティブ側から渡したい場合は、オブジェクト生成用のメソッドをネイティブから呼び出します。これも、もっと良い方法がある気がします。。

    fun createData(num: Int, buffer: ByteArray?): Data {
        return Data(num, buffer)
    }

リフレクションみたいで、きもいです。オブジェクトのクラス名、メソッドのID、第1,2引数を指定します。メソッドIDには、そのメソッドのシグネチャーが必要になります。

    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);

ちなみにシグネチャの確認方法は、こんな感じです。なおkotlinの場合は、kotlincというコンパイラが必要になります。

OpenCVで顔が検出できるか試す

実際に、OpenCVのサンプルであるFaceDetectionを元にOpenCVをネイティブで動かしてみて検出できるか試してみました。

device-2018-12-13-180211.png
device-2018-12-13-180256.png
device-2018-12-13-180457.png
ちゃんと顔(ねこ)の検出が成功しました。

まとめ

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

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5