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