はじめに
先日、CameraXとOpenCVを使った画像処理向けAndroidアプリのひな形という記事を投稿しました。これは、カメラ入力、画像処理、画面出力のすべてをJavaコードで行いました。
今回は、画像処理をNDK(C++)側で処理してみます。
これによって、上手く設計すれば様々なプラットフォームで共通のロジックコードを使用することが出来ます。これについては後半で少し触れます。
例えば、↓の動画はディープラーニング処理(Pose NetとSemantic Segmentation (DeepLab)) を行う処理と結果の描画処理をライブラリ化し、Androidアプリ、Windowsアプリ、Linuxアプリ(x64, armv7, aarch64)から呼べるようにしています。
PoseNet and DeepLab on Android, Windows and Linux(Raspberry Pi)
— iwatake (@iwatake2222) June 30, 2020
圧倒的マルチプラットフォーム感(りんごは知らね) pic.twitter.com/MmILjDuG8u
環境
- Host
- Windows 10 64-bit
- Android Studio 4.0
- Android SDK API Level 30
- 多少低くても大丈夫なはず
- Android NDK Version 21.3.6528147
- opencv-4.3.0-android-sdk.zip
- Target
- Galaxy S7 (Android 8.0.0)
プロジェクトの用意
プロジェクトの用意
まずは、 CameraXとOpenCVを使った画像処理向けAndroidアプリのひな形 と同様にプロジェクトを作成します。
コードや設定も同じように行ってください。
1点だけ違いがあります。テンプレートとしてEmpty Activity
ではなく、Native C++
を選択する必要があります。
Android NDK側でOpenCVを使う
Android NDK(C/C++)側でOpenCVを使うための設定をします。
app/src/main/cpp/CMakeLists.txt
を以下のように編集します。
OpenCV_DIR
には、OpenCVの場所を指定します。Java用にOpenCVをimportした際に、OpenCV/sdkの中身がプロジェクト内にコピーされます。その場所を相対パスで指定するのが楽だと思います。あるいは、オリジナルのOpencVの場所を指定します。この場合、環境に応じてパスは変更してください。
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp )
# ↓↓↓ 追加 ↓↓↓
# set(OpenCV_DIR "D:/devel/opencv-4.3.0-android-sdk/OpenCV-android-sdk/sdk/native/jni")
set(OpenCV_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../sdk/native/jni")
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
target_include_directories(native-lib PUBLIC ${OpenCV_INCLUDE_DIRS})
target_link_libraries(native-lib ${OpenCV_LIBS})
endif()
# ↑↑↑ 追加 ↑↑↑
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
一度、メニューバー -> Build -> Reflesh Linked C++ Projects
をします。
NDK(C++)側の実装
インターフェイスの変更
今回は、Java側からcv::MatをJNIに渡して、JNI側で画像処理をして受け取ったcv::Matを書き換えることにします。
実際にはcv::Matを直接渡すのではなくて、ポインタをlong型で渡して、キャストして使います。
MainActivity.java
に自動的に作られているJNI関数の宣言を以下のように変更します。
public native String stringFromJNI();
⇒
public native int processImage(long objMatSrc, long objMatDst);
NDK(C++)側の実装
app/src/main/cpp/native-lib.cpp
に自動的に作られているサンプルコードを変更します。変更内容は以下の通りです。
- 関数名と戻り値を変更して、
processImage
にする - 引数に
jlong objMatSrc
とjlong objMatDst
を追加 -
objMat
をcv::Mat*
にキャストして、所定の処理を行う
# include <jni.h>
# include <string>
# include <opencv2/opencv.hpp>
extern "C" JNIEXPORT jint JNICALL
Java_com_example_samplecameraxandopencvndk_MainActivity_processImage(
JNIEnv* env,
jobject, /* this */
jlong objMatSrc,
jlong objMatDst) {
cv::Mat* matSrc = (cv::Mat*) objMatSrc;
cv::Mat* matDst = (cv::Mat*) objMatDst;
static cv::Mat *matPrevious = NULL;
if (matPrevious == NULL) {
/* lazy initialization */
matPrevious = new cv::Mat(matSrc->rows, matSrc->cols, matSrc->type());
}
cv::absdiff(*matSrc, *matPrevious, *matDst);
*matPrevious = *matSrc;
return 0;
}
呼び出し側のコードを変更する
元々、Java側のMyImageAnalyzer::analyze
関数内で、画像処理を行っていました。
今回はこの処理をNDK側で行うので、先ほど作成した関数を呼ぶようにします。
@Override
public void analyze(@NonNull ImageProxy image) {
/* Create cv::mat(RGB888) from image(NV21) */
Mat matOrg = getMatFromImage(image);
/* Fix image rotation (it looks image in PreviewView is automatically fixed by CameraX???) */
Mat mat = fixMatRotation(matOrg);
Log.i(TAG, "[analyze] width = " + image.getWidth() + ", height = " + image.getHeight() + "Rotation = " + previewView.getDisplay().getRotation());
Log.i(TAG, "[analyze] mat width = " + matOrg.cols() + ", mat height = " + matOrg.rows());
/* Do some image processing */
/* ★★★ 変更点 ★★★ */
Mat matOutput = new Mat(mat.rows(), mat.cols(), mat.type());
processImage(mat.getNativeObjAddr(), matOutput.getNativeObjAddr());
// if (matPrevious == null) matPrevious = mat;
// Core.absdiff(mat, matPrevious, matOutput);
// matPrevious = mat;
/* Draw something for test */
Imgproc.rectangle(matOutput, new Rect(10, 10, 100, 100), new Scalar(255, 0, 0));
Imgproc.putText(matOutput, "leftTop", new Point(10, 10), 1, 1, new Scalar(255, 0, 0));
/* Convert cv::mat to bitmap for drawing */
Bitmap bitmap = Bitmap.createBitmap(matOutput.cols(), matOutput.rows(),Bitmap.Config.ARGB_8888);
Utils.matToBitmap(matOutput, bitmap);
/* Display the result onto ImageView */
runOnUiThread(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
/* Close the image otherwise, this function is not called next time */
image.close();
}
こんな感じで、NDK側でもOpenCVを使うことが出来ます。
本題はここまででお終いです。
ここからはおまけです。オレオレ設計について語ります。
無視してください。
マルチプラットフォームに対応した設計について
一般的な画像処理システム
一般的な画像処理システムのデータフローを上に示します。
- Input
- カメラ入力など、画像の入力処理を行います
- Image Processor
- 画像処理を行います。例えば、ノイズ除去や、物体検知処理などです
- 所定のフォーマットの画像(Bitmap, YUVなど)を受け取って、処理結果を返します
- 例えば、ノイズ除去処理だったら、ノイズ除去後の画像を出力する
- 例えば、物体検知処理だったら、物体検知結果(座標と識別名)を出力する
- Renderer
- 画像処理結果を基に、出力画像を生成します
- 例えば、バウンディングボックスの描画
- 画像処理結果を基に、出力画像を生成します
- View
- 出力画像を画面に出力します
- (
Display
という名前の方が良かったかも)
環境依存部、仕様依存部を綺麗に分けてみる
環境依存、非依存という観点で、処理を分類してみます。
- Input
- カメラ制御、あるいは動画ファイル読み込みなどは環境に依存して処理が変わる(同じコードを使えない)ため、環境依存
- OpenCVを使えばある程度共通化できるが、Androidとその他では共通化できない。そもそも言語が違う。
- Image Processor
- 純粋な画像処理であれば、環境非依存
- Renderer
- 所定のフォーマットの画像に描画するだけであれば環境非依存
- どういう風に描画したいか? に影響されるので仕様依存
- View
- 出力端末に依って処理が変わる(同じコードを使えない)ため、環境依存
上記のように分類をすると、純粋な画像処理ロジックだけを持つImage Processor部だけをライブラリとするのがよさそうです。
Renderer部は環境依存ではないのですが、どういう風に描画をするかは仕様に依存します。そのため、ライブラリに入れるのは抵抗があります。アプリケーション側で自由に変更できるべきです。
とはいえ、描画処理は面倒なので横着する
あるべき姿としては、↑の図のように、Image Processor部だけをライブラリ化すべきです。
Image Processorが行う処理がノイズ除去のように、入出力どちらも画像だけの場合にはRendererはそもそも不要なので問題はありません。
Image Processorが行う処理が物体検知処理のように、結果をデータ(座標など)として出力する場合、Rendererがバウンディングボックス等を描画します。この時、どのように出力画面を作るかは、仕様(商品仕様だったり、お客様の要望だったり、上司の気分)に依存します。そのため、アプリケーション側で自由に変更できるように、ライブラリの外に追い出しました。
が、検知結果などの描画処理も結構面倒で、各環境ごとに実装するのは手間がかかります。
ということで、普段手軽に試すだけであれば、下の図のように、Image Processor部とRenderer部をまとめてライブラリ化して、画像を入力して画像を出力するだけにしておくのがお手軽だと思います。
これによって、環境依存部はカメラ入力と画面の出力のみを担当して、それ以外の環境非依存部(画像処理と描画処理)を全てライブラリ化できます。そしてこのライブラリ化部分をCで書いておけば、AndroidアプリからはNDK(JNI)として呼び出せるし、他の環境(WindowsやLinux)でもそのまま使うことが出来ます。
(ちなみに、ここまで「ライブラリ」という言葉を使ってしまっていますが、同一バイナリファイルを使いまわせるわけではありません。実際にはcmake上でのライブラリ(add_library)として扱うのが便利です。)
OpenCVは?
ここまで、OpenCVは当然使えるものとして考えてきました。便利だから使っているだけで、無くても大丈夫です。
Android(Java)側とNDK(JNI)側のインターフェイスとして使っている所は別にバイト列にしてもOKです。ポインタと、width、height、channel情報があればやり取り可能です。
Android(Java)側でもNV21とRGBの変換に使っているくらいなので、頑張れば実装可能です。NDK側でもRGB画像をそのまま独自、あるいは、別のライブラリ(Deep Learning処理等)を使って画像処理するのであればOpenCVは不要です。