Nativeコード(NDK)でBitmapの画像処理

投稿日:  更新日:

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

monoFilterの実行結果

スポンサーリンク
スポンサーリンク