カメラ画像をSmartWatchに送信する(2)
前回のブログでは、カメラ画像をYUV420フォーマットからRGBフォーマットに変換するために、Javaで変換プログラムを作っていましたが、JNIを使ってCプログラム化することが出来ました。(GitHubにソースを公開しました)
そこで、防備録として書いておきます。
CameraViewPitcher
CameraViewPitcherというプログラム名としました。Activityは下記のようです。Bitmapキャッチャの起動と停止コマンドを実装しました。以下にAvtivityを丸々書きます。
public class CameraViewPitcherActivity extends Activity { public static final String CONTROL_START_REQUEST_INTENT = "com.sonyericsson.extras.aef.control.START_REQUEST"; public static final String CONTROL_STOP_REQUEST_INTENT = "com.sonyericsson.extras.aef.control.STOP_REQUEST"; public static final String EXTRA_AEA_PACKAGE_NAME = "aea_package_name"; public static final String HOSTAPP_PERMISSION = "com.sonyericsson.extras.liveware.aef.HOSTAPP_PERMISSION"; public static final String HOST_APP_PACKAGE_NAME = "com.sonyericsson.extras.smartwatch"; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public void onResume(){ super.onResume(); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); //タイトルを非表示 requestWindowFeature(Window.FEATURE_NO_TITLE); //Bitmapキャッチャを起動する //一度停止リクエストを出す Intent intent = new Intent(CONTROL_STOP_REQUEST_INTENT); intent.putExtra(EXTRA_AEA_PACKAGE_NAME, "com.luaridaworks.smartwatch.bitmapcatcher"); intent.setPackage(HOST_APP_PACKAGE_NAME); this.sendBroadcast(intent, HOSTAPP_PERMISSION); //100ms待つ long sTime = System.currentTimeMillis() + 100; while(sTime>System.currentTimeMillis()); //そして起動リクエストを出す intent = new Intent(CONTROL_START_REQUEST_INTENT); intent.putExtra(EXTRA_AEA_PACKAGE_NAME, "com.luaridaworks.smartwatch.bitmapcatcher"); intent.setPackage(HOST_APP_PACKAGE_NAME); this.sendBroadcast(intent, HOSTAPP_PERMISSION); setContentView(new SmartWatchCameraView(this)); } @Override public void onStop(){ super.onStop(); //Bitmapキャッチャを停止する Intent intent = new Intent(CONTROL_STOP_REQUEST_INTENT); intent.putExtra(EXTRA_AEA_PACKAGE_NAME, "com.luaridaworks.smartwatch.bitmapcatcher"); intent.setPackage(HOST_APP_PACKAGE_NAME); this.sendBroadcast(intent, HOSTAPP_PERMISSION); //finish(); } @Override public void onDestroy(){ super.onDestroy(); //Bitmapキャッチャを停止する Intent intent = new Intent(CONTROL_STOP_REQUEST_INTENT); intent.putExtra(EXTRA_AEA_PACKAGE_NAME, "com.luaridaworks.smartwatch.bitmapcatcher"); intent.setPackage(HOST_APP_PACKAGE_NAME); this.sendBroadcast(intent, HOSTAPP_PERMISSION); } }
SmartWatchCameraView
カメラ画像を扱う本体です。Cで書いたネイティブプログラムは、Javaの中で下記のように宣言することになります。yuv2rgb()とhalfsize()という2つの関数を作りました。
public native int yuv2rgb(int[] int_rgb, byte[] yuv420sp, int width, int height, int offsetX, int offsetY, int getWidth, int getHeight); public native int halfsize(int[] int_rgb, int width, int height); static { System.loadLibrary("yuv2rgb_module"); }
yuv2rgb_moduleというのが、ビルドしてできるロードモジュール名となります。
あとは、Cのプログラムを組むだけです。
Android.mkの作成
Android.mkは下記のような感じです。モジュール名とソースファイル名を指定しているだけです。
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := yuv2rgb_module LOCAL_SRC_FILES := yuv2rgb.c LOCAL_LDLIBS := -llog include $(BUILD_SHARED_LIBRARY)
yv2rgb.c
yuv2rgb.cの中身は下記のような感じです。ヘッダは2つです。Logを使いたいのでLogの定義をしています。
#include <jni.h> #include <android/log.h> #define LOG_TAG "YUV2RGB" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
関数の宣言は下記のようになります。intを返すのでjintが戻り値指定です。実際の関数名は、パッケージ名を アンダースコア(_)で繋げたものと、宣言したしたJavaのクラス名と、関数名をアンダースコアで繋げたものになります。
jint Java_com_luaridaworks_cameraviewpitcher_SmartWatchCameraView_yuv2rgb( JNIEnv* env, jobject thiz, jintArray int_rgb, jbyteArray yuv420sp, jint width, jint height, jint offsetX, jint offsetY, jint getWidth, jint getHeight )
引数については、必ず JNIEnv* env, jobject thiz が第一、第二引数となります。その後ろに、関数宣言した引数が続きます。JavaとCの引数の対応は以下のようです。
- int[] int_rgb → jintArray int_rgb
- byte[] yuv420sp → jbyteArray yuv420sp
- int width → jint width
- int height → jint height
- int offsetX → jint offsetX
- int offsetY → jint offsetY
- int getWidth → jint getWidth
- int getHeight → jint getHeight
配列はこのままでは使えないので、Cのポインタとして渡します。その受け渡し関数が下記です。C++でなくてCで書いているので、この書き方になります。Get
//配列のポインタを受け取る jint* rgbp=(*env)->GetIntArrayElements( env, int_rgb, 0 ); jbyte* yuvp=(*env)->GetByteArrayElements( env, yuv420sp, 0 );
使用後は開放する必要が有ります。allocで確保している場合は、開放しないとメモリリークを起こしますが、明示的にallocしていなくても、Get
//開放する (*env)->ReleaseIntArrayElements(env, int_rgb, rgbp, 0); (*env)->ReleaseByteArrayElements(env, yuv420sp, yuvp, 0);
もし、ソース内部でallocした配列をjavaに返したい場合は、下記のようにSet
(*env)->SetIntArrayRegion(env, int_rgb, 0, length, rgbp);
YUV420フォーマットの指定部分をRGBに変換する
YUV420フォーマットをRGBに変換するCのプログラムを以下に書きます。部分的に切り出すことができます。
//******************************************* // YUV420をRGBに変換する // データフォーマットは、最初に画面サイズ(Width*Height)分のY値が並び、 // 以降は、横方向、縦方向共に、V,Uの順番に2画素分を示して並ぶ // // 4×3ドットがあったとすると、YUV420のデータは // 0 1 2 3 // 0○○○○ Y00 Y01 Y02 Y03 Y10 Y11 Y12 Y13 Y20 Y21 Y22 Y23 V00 U00 V02 U02 V20 U20 V22 U22 となる。 // 1○○○○ V00はY00,Y01,Y10,Y11の4ピクセルの赤色差を表し、U00はY00,Y01,Y10,Y11の4ピクセルの青色差を表す // 2○○○○ // // width×heightの画像から (offsetX,offsetY)座標を左上座標としたgetWidth,GetHeightサイズのrgb画像を取得する //******************************************* jint Java_com_luaridaworks_cameraviewpitcher_SmartWatchCameraView_yuv2rgb( JNIEnv* env, jobject thiz, jintArray int_rgb, jbyteArray yuv420sp, jint width, jint height, jint offsetX, jint offsetY, jint getWidth, jint getHeight ) { //配列のポインタを受け取る jint* rgbp=(*env)->GetIntArrayElements( env, int_rgb, 0 ); jbyte* yuvp=(*env)->GetByteArrayElements( env, yuv420sp, 0 ); //全体ピクセル数を求める long frameSize = width * height; int uvp, y; int y1164, r, g, b; int i, j, yp; int u = 0; int v = 0; int uvs = 0; if(offsetY+getHeight>height){ getHeight = height - offsetY; } if(offsetX+getWidth>width){ getWidth = width - offsetX; } int qp = 0; //rgb配列番号 for ( j = offsetY; j < offsetY + getHeight; j++) { //1ライン毎の処理 uvp = frameSize + (j >> 1) * width; //offsetXが奇数の場合は、1つ前のU,Vの値を取得する if((offsetX & 1)!=0){ uvs = uvp + offsetX-1; // VとUのデータは、2つに1つしか存在しない。よって、iが偶数のときに読み出す v = (0xff & yuvp[uvs]) - 128; //無彩色(色差0)が128なので、128を引く u = (0xff & yuvp[uvs + 1]) - 128; //無彩色(色差0)が128なので、128を引く } for (i = offsetX; i < offsetX + getWidth; i++) { yp = j*width + i; //左からピクセル単位の処理 y = (0xff & ((int) yuvp[yp])) - 16; //Yの下限が16だから、16を引きます if (y < 0){ y = 0; } if ((i & 1) == 0) { uvs = uvp + i; // VとUのデータは、2つに1つしか存在しない。よって、iが偶数のときに読み出す v = (0xff & yuvp[uvs]) - 128; //無彩色(色差0)が128なので、128を引く u = (0xff & yuvp[uvs + 1]) - 128; //無彩色(色差0)が128なので、128を引く } //変換の計算式によりR,G,Bを求める(Cb=U, Cr=V) // R = 1.164(Y-16) + 1.596(Cr-128) // G = 1.164(Y-16) - 0.391(Cb-128) - 0.813(Cr-128) // B = 1.164(Y-16) + 2.018(Cb-128) y1164 = 1164 * y; r = (y1164 + 1596 * v); g = (y1164 - 391 * u - 813 * v); b = (y1164 + 2018 * u); if (r < 0){ r = 0; } else if (r > 262143){ r = 262143; } if (g < 0){ g = 0; } else if (g > 262143){ g = 262143; } if (b < 0){ b = 0; } else if (b > 262143){ b = 262143; } rgbp[qp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff); qp++; } } //(*env)->SetIntArrayRegion(env, int_rgb, 0, qp, rgbp); //開放する (*env)->ReleaseIntArrayElements(env, int_rgb, rgbp, 0); (*env)->ReleaseByteArrayElements(env, yuv420sp, yuvp, 0); LOGI("Tranport Finish."); return 0; }
サイズを半分にします
画像サイズを幅と高さを半分にするプログラムも試しに作ってみたので、ソースを載せておきます。
//******************************************* // サイズを半分にします //******************************************* jint Java_com_luaridaworks_cameraviewpitcher_SmartWatchCameraView_halfsize( JNIEnv* env, jobject thiz, jintArray int_rgb, jint width, jint height ) { //配列のポインタを受け取る jint* rgbp=(*env)->GetIntArrayElements( env, int_rgb, 0 ); int x,y; int i=0; for( y=0; y<height; y+=2 ){ for( x=0; x<width; x+=2 ){ rgbp[i] = rgbp[x + y * width ]; i++; } } //開放する (*env)->ReleaseIntArrayElements(env, int_rgb, rgbp, 0); LOGI("Shurink Finish."); return 0; }
SmartWatchCameraView
長くなりますが、新規に作成したSmartWatchCameraView.javaのソースも丸々載せておきます。
public class SmartWatchCameraView extends SurfaceView implements SurfaceHolder.Callback { public static final String LOG_TAG = "SmartWatchCameraView"; public static boolean SurfaceCreateFlag = false; private int surWidth = -1; private int surHeight = -1; private Matrix matrix90 = new Matrix(); //90度回転用 private int[] rgb_bitmap = new int[256 * 256]; //画像切り出しよう private long waittiming = 0; private Bitmap cameraBitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.RGB_565); private Canvas cameraCanvas = new Canvas(cameraBitmap); private Context context; private SurfaceHolder mholder; private Camera camera; public native int yuv2rgb(int[] int_rgb, byte[] yuv420sp, int width, int height, int offsetX, int offsetY, int getWidth, int getHeight); public native int halfsize(int[] int_rgb, int width, int height); static { System.loadLibrary("yuv2rgb_module"); } //******************************************* // コンストラクタ //******************************************* SmartWatchCameraView(Context context) { super(context); this.context = context; mholder = getHolder(); mholder.addCallback(this); mholder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); //横向き画面固定する ((Activity)getContext()).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); //90度回転用 matrix90.postRotate(90); } //******************************************* // サーフェイスの生成 //******************************************* @Override public void surfaceCreated(SurfaceHolder holder) { if (camera == null) { try { camera = Camera.open(); } catch (RuntimeException e) { ((Activity)context).finish(); Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show(); } } try { camera.setPreviewDisplay(holder); } catch (IOException e) { camera.release(); camera = null; ((Activity)context).finish(); Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show(); } SurfaceCreateFlag = true; } //******************************************* // サーフェイスの破壊 //******************************************* @Override public void surfaceDestroyed(SurfaceHolder holder) { if (camera != null) { camera.stopPreview(); camera.release(); camera = null; } SurfaceCreateFlag = false; } //******************************************* // 画面サイズ変更イベント //******************************************* @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d(LOG_TAG, "surfaceChanged"); if (camera == null) { ((Activity)context).finish(); return; } //画面が切り替わったのでストップする camera.stopPreview(); //プレビューCallbackを一応nullにする。 camera.setPreviewCallback(null); //プレビュ画面のサイズ設定 Log.d(LOG_TAG, "Width= " + width + " Height= " + height); surWidth = width; surHeight = height; setPictureFormat(format); setPreviewSize(surWidth, surHeight); //コールバックを再定義する camera.setPreviewCallback(_previewCallback); //プレビュスタート camera.startPreview(); } //******************************************* // カメラ画像フォーマットの設定 //******************************************* private void setPictureFormat(int format) { try { Camera.Parameters params = camera.getParameters(); List<Integer> supported = params.getSupportedPictureFormats(); if (supported != null) { for (int f : supported) { if (f == format) { params.setPreviewFormat(format); camera.setParameters(params); break; } } } } catch (Exception e) { e.printStackTrace(); } } //******************************************* // カメラ画像サイズの設定 //******************************************* private void setPreviewSize(int width, int height) { Camera.Parameters params = camera.getParameters(); List<Camera.Size> supported = params.getSupportedPreviewSizes(); if (supported != null) { for (Camera.Size size : supported) { if (size.width <= width && size.height <= height) { params.setPreviewSize(size.width, size.height); camera.setParameters(params); break; } } } } //******************************************* // フレームデータを取得するためのプレビューコールバック //******************************************* private final Camera.PreviewCallback _previewCallback = new Camera.PreviewCallback() { //******************************************* // dataは YUV420は 1画素が12ビット //******************************************* public void onPreviewFrame(byte[] data, Camera backcamera) { if (camera == null) { return; } //カメラが死んだ時用のブロック //プレビュを一時止める camera.stopPreview(); //一応コールバックをnullにする camera.setPreviewCallback(null); //YUV420からRGBに変換しつつ画像中心の256×256エリアを切り出す yuv2rgb(rgb_bitmap, data, surWidth, surHeight, surWidth/2-128, surHeight/2-128, 256, 256); //半分のサイズに圧縮する halfsize(rgb_bitmap, 256, 256); //ARGBデータをcameraBitmapに転送する cameraCanvas.drawBitmap(rgb_bitmap, 0, 128, 0, 0, 128, 128, false, null); //画像を90゜回転する Bitmap angle90Bitmap = Bitmap.createBitmap(cameraBitmap, 0, 0, 128, 128, matrix90, true); //intentを出す ByteArrayOutputStream baos = new ByteArrayOutputStream(); angle90Bitmap.compress(CompressFormat.PNG, 100, baos); byte[] bytebmp = baos.toByteArray(); Intent intent = new Intent("com.luaridaworks.extras.BITMAP_SEND"); intent.putExtra("BITMAP", bytebmp); getContext().sendBroadcast(intent); //5fpsとなるように待っている while(waittiming>System.currentTimeMillis()){} waittiming = System.currentTimeMillis() + 200L; if (camera == null) { return; } else{ //コールバックを再セットする camera.setPreviewCallback(_previewCallback); //プレビューを開始する camera.startPreview(); } } }; }