コンピュータを楽しもう!!

今、自分が面白くていろいろやってみたことを書き綴りたいと思います。連絡先はtarosa.yでgmail.comです。

カメラ画像を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で書いているので、この書き方になります。GetArrayElementsは、第3引数が1のときは、C側でメモリをallocしてコピーします。0の場合はポインタを渡すだけです。

 //配列のポインタを受け取る
 jint* rgbp=(*env)->GetIntArrayElements( env, int_rgb, 0 );
 jbyte* yuvp=(*env)->GetByteArrayElements( env, yuv420sp, 0 );

使用後は開放する必要が有ります。allocで確保している場合は、開放しないとメモリリークを起こしますが、明示的にallocしていなくても、GetArrayElements内部で自動的にallocしている可能性もあるので、必ず開放する必要が有ります。

 //開放する
 (*env)->ReleaseIntArrayElements(env, int_rgb, rgbp, 0);
 (*env)->ReleaseByteArrayElements(env, yuv420sp, yuvp, 0);

もし、ソース内部でallocした配列をjavaに返したい場合は、下記のようにSetArrayRegionを使えばできます。

 (*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();
     }
   }
 };
}