Android C言語を触る(JNI, NDK など基礎知識) | 世界的日曜WEBプログラマー日記

世界的日曜WEBプログラマー日記

一年後に世界的なWEBサービスを運営するにはどうすればいいのか書いていく(予定)


画像処理ではRGB形式の他にYUV形式がある。
YUV形式のフォーマットは一般にPlanar, Semi-Planar, Interleavedの3つある。
YUV planar semi-planar inter


尚ここでCr == V、Cv == U と同じ意味であることに注意。

planarはそれぞれのYUVのチャンネル毎にまとめてデータが置かれている。
semi-planarはYだけデータが置かれており、UVは交互に並んでいる。
interleavedはYUVが交互に並んで置かれている。

444であれば、それぞれのチャンネルはフルでサイズ分もっており、
422であればYだけフル、U, Vは1/2ずつ、
420(別名411)はYだけフル、U, Vは1/4ずつ持つ。

422, 420にする理由はデータ量圧縮のためである。

Androidにおいてカメラデバイスから来るフォーマットはAndroidは420 semiplanar( 420SP )で有ることがほとんどのようだ。
自身の端末のフォーマットについては確かめたほうが良く、以下のメソッドで確かめることができる。
 camera.getParameters().getPreviewFormat();


cameraはカメラオブジェクトである。(詳細は前回の記事参照のこと)
getPreviewFormatの返り値はintegerである。
0x11,(NV21)はYVU420SPを意味する。
0x16,(NV16)はYUV422SPを意味する。
ちなみ、NV21はYVUで、NV16はYUVであり、UとVの並び順が違うので注意すること!!

カメラからのプレビューの画像データを取得するには
onPreviewFrame(byte[] data, Camera camera) 

のメソッドを定義すればよい。プレビュー画像の配列はbyte型でくる。

カメラデバイスからY, U, Vに関して,最小値と最大値は0x00-0xffの範囲で返却される。
注意として、JAVAでは強制的にカメラデバイス側の符号を無しのデータを、signed型として扱ってしまう。
つまり、byte型配列を扱おうとすると、128から上の数は符号がマイナスになってしまう。
(ex. 255->-1, 254->-2 .. 2の補数。127までは問題が無い)
0-255を正しく扱うためには0xffでANDをとってやる必要がある。具体的には以下のようにしてアクセスする。
 int val = (data[ i ] & 0xff);// <== 強制的に最上位ビットの符号を消してやる。

これで-128~127の値が0~255として変換される。

尚、Native(JNI、C言語)で呼び出す場合はこうした処理をしなくともunsignedでキャストしてやれば事が足りる。

JNIについて
JNIを触る際、一番参考にするのはNDKをインストールした際についてくるサンプルコードである。コレが一番わかり易い。
これからNDKを勉強した人は必ず一回サンプルコードを実行すること!!!!変に文献やWEBは逆に遠回りである。

作成したC言語のファイルをコンパイルするには以下を呼ぶだけで良い。(makeコマンド相当)
$ ndk-build

Android.mkがMakefileとなっている。これがないとコンパイルされない。

JNIを使うときはプロンプトでndk-buildを叩いて、イクリプス上で実行して実機に流すという、ややたるいステップとなっている。
またC言語をで変更してndk-buildしても、JAVA上のソースコードに変更(更新)が無いとCライブラリの転送が行われないので注意。

C言語側のソースで作成する関数名にはルールがある。
例えばJava側のパッケージ名を
 package com.ekispresso.camera2;

としていた場合、
JNIを呼び出すクラス名がCameraView、
関数名がaddであれば、C言語側の関数名は
Java_com_ekispresso_camera2_CameraView_add
となる。(_区切り)

以下はその具体例である.

例)
--Java側
int a = 10;
int b = 20;
int c = add( a, b ); //<== JNIに記載する関数名
public native int add( int val1, int val2 );
// クラス内に記述する。Cの関数を使うという宣言

--C言語側
jint
Java_com_ekispresso_camera2_CameraView_add( JNIEnv* env, jobject thiz, jint a, jint b ) {
int c = a + b;
return c;
}

注意:JNIEnvとjobjectの二つは必ず引数として入れなければならない。Java側で引数が二つある場合は4つとなる。
jintの他にも jfloatなどがあり、これはint, float と等価であるため、c=a+bのようなことは問題ない。

配列を引数として扱いたい場合はちょっと注意が必要である。
byteやIntである場合、

Java_com_ekispresso_camera2_CameraView_render( JNIEnv* env, jobject thiz,
jintArray _rgb,
jint width, jint height,
jbyteArray _data,
jint camera_width, jint camera_height)
{
jint* rgb_bitmap=(*env)->GetIntArrayElements( env, _rgb, &b);
jbyte* data = (*env)->GetByteArrayElements( env, _data, &b);
// .... // <== なんかやって
(*env)->ReleaseIntArrayElements(env, _rgb, rgb_bitmap, 0);
(*env)->ReleaseByteArrayElements(env, _data, data, 0);
}

のようにして使用する。
上記はJava側に明示的に上記の配列を使用してますよ!GCしないでね!と伝えている。
使い終わったあとはReleaseなどを忘れない様に。

C言語ソース側ではmallocなども問題なく使える。

Android.mkの書き方

インクルードパスをLOCAL_C_INCLUDESにガンガン足していく
 
LOCAL_C_INCLUDES += ./src/interface ./src/spec ./src/
とする。

コンパイルしたいC言語のファイルをガンガン足していく。具体的には
LOCAL_SRC_FILES :=rendering.c ./src/core/ekispresso_core_malloc.c ./src/core/ekispresso_heap.c ./src/core/ekispresso_crc32.c ./src/core/ekispresso_environment.c ./src/core/ekispresso_core_sincos.c ./src/core/ekispresso_prng_xorshift.c
とひたすら書く。

モジュール名を定義する。コレはなんでもいい。
LOCAL_MODULE := rendering

これで終わり。
正しくインクルードパスなど設定で来ていればndk-buildすれば通るはず。
Androidのデフォルトのサンプルを参考にすると良い。

株式会社OctOpt
コンピューターサイエンス会社OctOptの技術公式ブログ
等々力 康弘
@rocky_house