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

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

カメラ画像をSmartWatchに送信する

自作SmartWatch初アプリのBitmapキャッチャをリリースしたので、試しにカメラのプレビュ画像をBitmapキャッチャの機能を用いて、SmartWatchに送信するプログラムを作ってみました。折角なので、カメラのプログラムについてブログで紹介したいと思います。
プログラムはgithubSmartWatchCameraViewというリポジトリ名でアップしました。
(NDKを使った高速版をこちらのブログにアップしました)

Youtube動画

SmartWatch画面に表示している様子の動画です。後半は動画表示の様子です。

Manifestへの追記

カメラライブラリを使用するため、Manifestに下記の項目を追記しました。

 <uses-permission android:name="android.permission.CAMERA" />
 <uses-feature android:name="android.hardware.camera" />

Activity

SmartWatchCameraViewというアプリ名にしました。Activityは SmartWatchCameraView.javaをセットするだけなので、超簡単です。

public class SmartWatchCameraViewActivity extends Activity {
 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   //全画面使用
   getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
   getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
   //タイトルを非表示
   requestWindowFeature(Window.FEATURE_NO_TITLE);
   setContentView(new SmartWatchCameraView(this));
 }
}

SmartWatchCameraView

SmartWatchCameraView.javaは、CameraPreview.javaを継承して作りました。CameraPreview.javaは、@ITサイトの「もはやケータイに必須のカメラをAndroidで制御しよう」でサンプルとして配布されているCameraExample.zipに入っていたものを使用させていただきました。必要な方は、上記リンクからダウンロードしてください。
このプログラムは、あくまで現在愛用しているXPERIA mini Proのカメラに対応させて作っているので、異機種では正常に動かないかもしれません。
先ずは、SmartWatchCameraViewクラスを作成したとき、下記のようなプライベート変数を用意しました。

public class SmartWatchCameraView extends CameraPreview {
 public static final String LOG_TAG = "SmartWatchCameraView";
 private int surWidth = -1;
 private int surHeight = -1;
 private Matrix matrix90 = new Matrix();         //90度回転用
 private int[] rgb_bitmap = new int[128 * 128];  //画像切り出しよう

Constructor

コンストラクタで、画面横向き固定とSmartWatchに画像を転送するときに90度回転する必要が有るので、それようのMatrixの初期化を行っています。

 SmartWatchCameraView(Context context) {
   super(context);
   //横向き画面固定する
   ((Activity)getContext()).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
   //90度回転用
   matrix90.postRotate(90);
 }

surfaceChanged

画面サイズ変更イベントが走るときに、画面サイズの取得とPreviewSizeのセットを行います。継承元のsurfaceCreateでPreviewCallbackがセットされていますが、surfaceChangedのオーバーライドで自作のメソッドにコールバックするように再定義します。
PreviewCallbackのonPreviewFrame()でプレビュ画像データが取得できます。

 @Override
 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
   if (camera == null) {
     ((Activity)context).finish();
     return;
   }
   //画面が切り替わったのでストップする
   camera.stopPreview();
   //プレビューCallbackを一応nullにする。
   camera.setPreviewCallback(null);

   //プレビュ画面のサイズ設定
   surWidth = width;
   surHeight = height;
   setPictureFormat(format);
   setPreviewSize(surWidth, surHeight);

   //コールバックを再定義する
   camera.setPreviewCallback(_previewCallback);
   //プレビュスタート
   camera.startPreview();
 }

Camera.PreviewCallback

プレビューコールバックのonPreviewFrame()を実装します。プレビュー画像データはbyte[] data配列にYUV420フォーマットで格納されています。YUV420フォーマットはBitmapクラスでデコードできないので、一度RGBフォーマットに変換します。
今回、YUV420フォーマットからRGBフォーマットに変換する再に、画像中の任意の位置の128×128エリアを取り出しています。
RGBフォーマットに変換した後は、Canvas#drawBitmapを用いてBitmapデータに変換しています。さらに、Bitmap#createBitmapとMatrixクラスを用いて画像を90度回転させています。
後はBitmapキャプチャに向かって、Broadcast送信を行っています。そして、再びコールバックをセットして、Previewを再スタートさせています。

 private final Camera.PreviewCallback _previewCallback =
        new Camera.PreviewCallback() {
   public void onPreviewFrame(byte[] data, Camera backcamera) {
     if (camera == null) { return; }  //カメラが死んだ時用のブロック
     //プレビュを一時止める
     camera.stopPreview();
     //一応コールバックをnullにする
     camera.setPreviewCallback(null);

     //YUV420からRGBに変換しつつ画像中心の128×128エリアを切り出す
     decodeYUV420SP2(rgb_bitmap, data, surWidth, surHeight, surWidth/2-64, surHeight/2-64, 128, 128);

     //Bitmapを生成して、画像データを作る
     Bitmap motoBitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.RGB_565);
     Canvas motoCanvas = new Canvas(motoBitmap);
     motoCanvas.drawBitmap(rgb_bitmap, 0, 128, 0, 0, 128, 128, false, null);
     //画像を90゜回転する
     motoBitmap = Bitmap.createBitmap(motoBitmap, 0, 0, 128, 128, matrix90, true);

     //intentを出す
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     motoBitmap.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);

     if (camera == null) {
       return;
     }
     else{
       //コールバックを再セットする
       camera.setPreviewCallback(_previewCallback);
       //プレビューを開始する
       camera.startPreview();
     }
   }

decodeYUV420SP2

YUV420フォーマットをRGBフォーマットに変換する関数はネットを検索するとたくさん見つかるのですが、YUV420フォーマットの画像の指定エリアを切り出してRGBフォーマットに変換する関数は見つからなかったので自作してみました。たぶん、これで行けると思います。Cで書き直したいのですが、Javaの配列をCに持って行ったり、Cの計算配列をJavaに戻したりするやり方が勉強不足でわかりません。
いずれ、勉強したいと思っています。

//*******************************************
// 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画像を取得する
//*******************************************
public void decodeYUV420SP2(int[] int_rgb, byte[] yuv420sp, int width, int height, int offsetX, int offsetY, int getWidth, int getHeight) {
 final int frameSize = width * height;  //全体ピクセル数を求める
 int uvp, y;
 int y1164, r, g, b;
 int i, 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 (int j = offsetY; j < offsetY + getHeight; j++) {
   uvp = frameSize + (j >> 1) * width;

   //offsetXが奇数の場合は、1つ前のU,Vの値を取得する
   if((offsetX & 1)!=0){
     uvs = uvp + offsetX-1;
     // VとUのデータは、2つに1つしか存在しない。よって、iが偶数のときに読み出す
     v = (0xff & yuv420sp[uvs]) - 128;      //無彩色(色差0)が128なので、128を引く
     u = (0xff & yuv420sp[uvs + 1]) - 128;  //無彩色(色差0)が128なので、128を引く
   }

   for (i = offsetX; i < offsetX + getWidth; i++) {
     yp = j*width + i;
     //左からピクセル単位の処理
     y = (0xff & ((int) yuv420sp[yp])) - 16; //Yの下限が16だから、16を引きます
     if (y < 0){ y = 0; }
     if ((i & 1) == 0) {
       uvs = uvp + i;
       // VとUのデータは、2つに1つしか存在しない。よって、iが偶数のときに読み出す
       v = (0xff & yuv420sp[uvs]) - 128;     //無彩色(色差0)が128なので、128を引く
       u = (0xff & yuv420sp[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; }
     int_rgb[qp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
     qp++;
   }
 }
}