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")
}
}
}

