Android NDKはNativeコードからBitmapを処理するためのグラフィックライブラリを実装しています。これを使うとBitmapが容易に扱えるので便利です。
画像はデータ量が多いため、画像処理や解析を行うと重い処理になってしまいます。Nativeコードで行えば高速化が出来ます。
また、JNI(Java Native Interface)があるので、Kotlin~Nativeコード間のデータ受け渡しの親和性が高い点もよいです。処理の指示を引数で渡したり、処理の結果をNativeコードの戻り値で返したりが簡単です。
今回、このライブラリを使ってBitmapの画像処理を行ってみたので紹介します。
目次
ライブラリ(jnigraphics)をリンク
Android NDKのグラフィックライブラリ(jnigraphics)が必要になります。
jnigraphicsをfind_libraryで検索し、画像処理のNativeコード(bitmap-lib.cpp)含むライブラリ(bitmap-lib)へ、シナリオでリンクします。
# Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.10.2) # Declares and names the project. project("プロジェクト名") find_library( log-lib log ) find_library( graphics-lib jnigraphics ) add_library( bitmap-lib SHARED bitmap-lib.cpp ) target_link_libraries( bitmap-lib ${log-lib} ${graphics-lib} )
find_libraryはデフォルトでplatforms以下を検索してライブラリを探します。
ディレクトリを覗くとjnigraphicsの存在が確認できます。
sdk/ndk <--- ndkのインストール先 ├── 21.1.6352462 <--- ndkのバージョン │ ├── build │ ├── platforms │ │ ├── android-16 <--- API毎のライブラリ : : : │ │ ├── android-24 │ │ │ ├── arch-arm <--- ABI毎のライブラリ │ │ │ ├── arch-arm64 │ │ │ ├── arch-x86 │ │ │ │ └── usr │ │ │ │ └── lib : : : : : │ │ │ │ ├── libGLESv1_CM.so │ │ │ │ ├── libGLESv2.so │ │ │ │ ├── libGLESv3.so │ │ │ │ ├── libjnigraphics.so <--- ここ │ │ │ │ ├── liblog.so : : : : : │ │ │ │ └── libz.so │ │ │ └── arch-x86_64 : : : │ │ └── android-29 : :
NativeコードでBitmapを処理
ヘッダの指定
Bitmap関連の関数を使うためにヘッダの指定が必要です。
この中に関数のプロトタイプ宣言、定数、構造体が収められています。
#include <jni.h> #include <string> #include <android/bitmap.h> extern "C" JNIEXPORT jobject JNICALL Java_com_example_nativecode_graphics_MainActivity_monoFilter( JNIEnv* env, jobject obj, jobject jbitmap) { : }
AndroidBitmap_getInfo
AndroidBitmap_getInfoはBitmapの情報を取得する関数です。
AndroidBitmapInfoのポインタを引数で渡すと、その中に情報がセットされます。
: int result; AndroidBitmapInfo info; result = AndroidBitmap_getInfo(env, jbitmap, &info); : }
/** Bitmap info, see AndroidBitmap_getInfo(). */ typedef struct { /** The bitmap width in pixels. */ uint32_t width; /** The bitmap height in pixels. */ uint32_t height; /** The number of byte per row. */ uint32_t stride; /** The bitmap pixel format. See {@link AndroidBitmapFormat} */ int32_t format; /** Unused. */ uint32_t flags; // 0 for now } AndroidBitmapInfo;
関数は戻り値として整数を返します。この整数が関数の成功・失敗を表しています。
「戻り値 < 0」は失敗です。
/** AndroidBitmap functions result code. */ enum { /** Operation was successful. */ ANDROID_BITMAP_RESULT_SUCCESS = 0, /** Bad parameter. */ ANDROID_BITMAP_RESULT_BAD_PARAMETER = -1, /** JNI exception occured. */ ANDROID_BITMAP_RESULT_JNI_EXCEPTION = -2, /** Allocation failed. */ ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3, };
AndroidBitmap_lockPixels/unlockPixels
Bitmapデータ(画像)はAndroidシステムのヒープメモリー上に配置されています。
ヒープメモリーなので定期的なGC(Garbage Collection)が行われます。この時、Bitmapデータのアドレスが変わってしまう可能性が出てきます。
Bitmapデータのアクセス中にアドレスが変わってしまうと、正しい処理が出来ません。
これを回避するために、AndroidBitmap_lockPixelsでアドレスをロックし、その間にBitmapへアクセスするという方法を取ります。
Bitmapへアクセスが終わったら、AndroidBitmap_unlockPixelsでロックを解除します。
よって、AndroidBitmap_lockPixelsとunlockPixelsは必ずペアで使うことになります。
: void *pixels; result = AndroidBitmap_lockPixels(env, jbitmap, &pixels); if(result < 0) return NULL; /* ↑ アドレスのロック Bitmapデータ(画像)へアクセスする ↓ アドレスのアンロック */ AndroidBitmap_unlockPixels(env, jbitmap); :
AndroidBitmap_lockPixelsはもう一つ重要な機能があります。
引数へvoid型のポインタ(pixels)を渡すと、画像データの先頭アドレスが代入されて返ってきます。この先頭アドレスを起点にして各ピクセルへアクセスが可能です。
画像処理・解析のサンプル
monoFilter(モノクロ化)
Nativeコードを使ってBitmap画像をモノクロ化(グレイスケール化)するサンプルです。
画像データの先頭アドレスpixelsを起点に行の先頭アドレス(line)を計算しています。その後、行内をピクセル(rgba)単位で走査しつつ、グレイスケールに変換しています。
#include <jni.h> #include <string> #include <android/bitmap.h> typedef struct { uint8_t red; uint8_t green; uint8_t blue; uint8_t alpha; } rgba; extern "C" JNIEXPORT jobject JNICALL Java_com_example_nativecode_graphics_MainActivity_monoFilter( JNIEnv* env, jobject obj, jobject jbitmap) { int result; AndroidBitmapInfo info; result = AndroidBitmap_getInfo(env, jbitmap, &info); if(result < 0) return NULL; if(info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) return NULL; void *pixels; result = AndroidBitmap_lockPixels(env, jbitmap, &pixels); if(result < 0) return NULL; for(int y = 0; y < info.height; y++) { rgba *line = (rgba *)((uint8_t *)pixels + info.stride * y); for(int x = 0; x < info.width; x++) { float mono =0.299f * (float)line[x].red + 0.587f * (float)line[x].green + 0.114f * (float)line[x].blue; line[x].red = (uint8_t)mono; line[x].green = (uint8_t)mono; line[x].blue = (uint8_t)mono; } } AndroidBitmap_unlockPixels(env, jbitmap); return jbitmap; }
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val _bitmapOrig = BitmapFactory.decodeResource( resources, R.drawable.ajisai_120x90) val _bitmapMono = monoFilter( _bitmapOrig.copy(Bitmap.Config.ARGB_8888, true)) ?: _bitmapOrig findViewById<ImageView>(R.id.imgOrig).setImageBitmap(_bitmapOrig) findViewById<ImageView>(R.id.imgMono).setImageBitmap(_bitmapMono) } external fun monoFilter(bitmap: Bitmap): Bitmap? companion object { init { System.loadLibrary("bitmap-lib") } } }