2012年12月28日金曜日

AndroidでWifiカメラから受け取った動画を画像処理する(1)ピクセルを直接操作する方法

前回はWifiカメラからのMJPEGストリームをandroidで受け取って表示する方法を紹介しましたが、今回は受け取った画像を画像処理してみます。取り扱うのは簡単なフィルタリングですが、この手法を発展させると、画像から対象物を見つけるというような処理も可能になります。

今回はandroidが持っているBitmap画像のピクセル値を直接処理する手法を取扱います。NDKを用いてCygwin上でC言語のモジュールをビルドする必要がありますので注意してください。

画像処理ライブラリであるOpenCVを用いる方法は次回紹介します。

準備として、前回紹介したSimpleMjpegViewをダウンロードし、eclipseからandroidにインストールできるようにしておいてください。ダウンロード法はいろいろありますが、「Downloads→Branches→masterのzip」と辿るのが簡単です。展開して得られたフォルダをお好みの場所(例えばeclipseのworkspace)に移動します。そのままではフォルダ名が長いので、短い名前に変更した方がNDKによるビルド時に楽です。

そのフォルダをeclipseに取り込むには「ファイル→インポート→一般→既存プロジェクトをワークスペースへ」とたどり、ルートディレクトリを設定して完了します。私はeclipseをpleiadesによって日本語化しているのでこのような表記になりますが、英語版を使っている方は適宜読みかえてください。




このアプリケーションをandroidにインストールして動作を確認しておきます。Wifiカメラは前回紹介した方法のどれでも構いません。Ai-Ballの場合は変更なしでそのまま動作します。スマホ+IP WebCamの場合、メニューからIP Webcamを動かすスマートフォンのURLをあらかじめ指定しておきます。どのようなURLを指定すべきかは、Wifiカメラ側のスクリーンに表示されていますので参照してください(URLの末尾には「videofeed」をつけます)。

もちろん、映像を見るandroidとWifiカメラは同一のネットワークに属している必要がありますので注意してください。

横画面で表示した結果は図の通りです。画質を落としたjpegが送られてきますので、静止画でみるとかなり粗いですね。

MjpegView.javaの編集

動作を確認したら、ここから画像処理に入っていきます。ここではグレースケール化を試してみます。src以下のMjpegView.javaの130行目に下記のような記述があるのを見つけます。
if(bmp==null){
  bmp = Bitmap.createBitmap(IMG_WIDTH, IMG_HEIGHT, Bitmap.Config.ARGB_8888);
}
int ret = mIn.readMjpegFrame(bmp);

if(ret == -1)
{
  ((MjpegActivity)saved_context).setImageError();
  return;
}
ここではBitmapクラスのインスタンスbmpのメモリを必要に応じて確保し、readMjpegFrameという関数によってWifiカメラからの映像をbmpに格納しています。なお、readMjpegFrameはC言語で書かれた関数であり、JNI経由で呼び出しています。

この処理が終わった時点でbmpにはWifiカメラから得た画像が格納されており、これがそのまま画面に表示されますので、これを加工すればそれがそのまま画面に現れます。やってみましょう。

ここでは、引数にBitmapをとるimageprocessingを関数を作ることを考えましょう。つまり、先ほどのreadMjpegFrameの後に下記の内容を追加します。
if(bmp==null){
  bmp = Bitmap.createBitmap(IMG_WIDTH, IMG_HEIGHT, Bitmap.Config.ARGB_8888);
}
int ret = mIn.readMjpegFrame(bmp);

if(ret == -1)
{
  ((MjpegActivity)saved_context).setImageError();
  return;
}               
// 下記のJNIによる画像処理を追加
imageprocessing(bmp);
次にこのimageprocessingをどのように実装するかですが、画像処理は非常に計算量が多く時間がかかる処理であるため、Javaでは書かずにJNIを利用してC言語で記述することにします。

そのために、同じくMjpegView.javaでIMG_WIDTHとIMG_HEIGHTを設定している部分を見つけ、その後ろに下記の4行を追加します。
public int IMG_WIDTH=640;
public int IMG_HEIGHT=480;

// 下記の4行を追加 
static {
  System.loadLibrary("ImageProc");
}
public native void imageprocessing(Bitmap bmp); 
これは、libImageProc.soという自作ライブラリを呼び出す設定と、その中の(これから作成する)imageprocessing関数の宣言を記述しています。

ImageProc.hとImageProc.cの編集

次に、imageprocessing関数をC言語のモジュールとして記述していきます。このアプリケーションでは、jpegのデコードで既にJNIによるC言語モジュールを利用していますので、そこに追記していく形式をとります。まず、jni/ImageProcフォルダにあるヘッダファイルImageProc.hを開き、末尾に下記の2行を追加します。
void Java_com_camera_simplemjpeg_MjpegView_imageprocessing(JNIEnv* env,jobject thiz, jobject bmp);
int getIndex(int i, int j);
一つ目がこれから作成するimageprocessing関数ですが、Javaにおけるパッケージ名とクラス名をアンダーバーでつないだ形になっていることに注意してください。これはJNIの規約です。また、2行目は画像の座標を配列のインデックスに変換する関数です。これはC言語の内部からしか用いないので、パッケージ名やクラス名は入っていません。

次にjni/ImageProcフォルダにあるImageProc.cを開き、末尾に下記の内容を追加します。
void Java_com_camera_simplemjpeg_MjpegView_imageprocessing(JNIEnv* env,jobject thiz, jobject bmp){

  AndroidBitmapInfo  info;
  int*              pixels;
  int                ret;
  int i,j;

  if(bmp==NULL) return;

  if ((ret = AndroidBitmap_getInfo(env, bmp, &info)) < 0) {
    LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
    return;
  }

  if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
    LOGE("Bitmap format is not RGBA_8888 !");
    return;
  }

  // Java側にあるBitmap画像のピクセルの配列をpixelとして利用
  if ((ret = AndroidBitmap_lockPixels(env, bmp, (void *)&pixels)) < 0) {
    LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    return;
  }

  //グレースケール化
  for(j=0 ; j<IMG_HEIGHT ; j++){
    for(i=0 ; i<IMG_WIDTH ; i++){
      int blue =(0xff0000 & pixels[getIndex(i, j)])>>16;
      int green =(0xff00 & pixels[getIndex(i, j)])>>8;
      int red =0xff & pixels[getIndex(i, j)];

      // 浮動小数演算を行うとパフォーマンスが悪いので整数演算で
      //int Y = (int)(0.299f*red + 0.587f*green + 0.114f*blue);
      int Y = (299*red + 587*green + 114*blue)/1000;
      blue = green = red = Y;

      pixels[getIndex(i, j)] = 0xff000000 | blue<<16 | green<<8 | red ;
    }
  }

  // ※ (この位置、後で使います)

  // 配列pixelのロックを解除して終了
  AndroidBitmap_unlockPixels(env, bmp);
}

// 座標を配列のインデックスに変換
// インライン関数にしないとパフォーマンスが悪い
inline int getIndex(int i, int j){
  return i + j*IMG_WIDTH;
}
imageprocessing関数の方は、「Javaから渡されたbitmapオブジェクトからピクセルの配列を取得→画像処理→ピクセルの解放」という流れで処理が進んでいます。この手法はandroid 2.2およびNDKバージョン4bにて導入された手法を用いていますので、それ以前のandroidでは動作しません。

得られたピクセルは、上位ビットから順に8ビットずつ不透明度、blue、green、redの順で並んでいます。これはJava側でBitmapを構築する際にBitmap.Config.ARGB_8888を指定したためです。不透明度はここでは使わずに0xffで固定しています。

グレースケールの輝度値(Y)を得るための演算は実数演算として定義されますが、floatで浮動小数点演算を行うとパフォーマンスが落ちますので、ここでは整数演算として実行します。浮動小数点演算でどれくらいパフォーマンスが落ちるかは後で試してみても良いでしょう。

また、画像の座標を配列に変換する関数getIndexはインライン関数として実装します。C言語のマクロ機能を用いても良いのですが、(後で)ビルドに用いるコンパイラのgccはC言語でもinline関数をサポートしていますので利用してみました。「inline」を削除して通常の関数とするとパフォーマンスが落ちますので、後で試してみても良いでしょう。

このように、androidでは通常のWindowsなどでのプログラミングの感覚で記述するとパフォーマンスが落ちることがあります。

以上を記述して保存したら、ビルドしてみます。

ビルド

上記のコードはC言語のコードを含んでいますので、ビルドにはAndroid NDK(Android Native Development Kit)を用いる必要があります。

NDKのセットアップはネットでは
などが詳しいです。また、書籍では出村成和著「Android NDKネイティブプログラミング」などがあります。

NDKのセットアップが済んだらビルドをCygwin上でコマンドラインで行います。eclipseのプラグインであるADTを最新にするとeclipse上でビルドできるはずなのですが、今回それを試したところ「ビルドは通るがエディタでエラーが消えない」状態になったので、面倒でもコマンドラインで実行してください。

ビルドの流れは
  1. Cygwinを起動し、「cd  (プロジェクトがある場所)」でプロジェクトのフォルダに移動
  2. 「ndk-build」を実行し、ビルドを行う
となります。

1.のフォルダの移動ですが、WindowsのCドライブのトップは「/cygdrive/c/」となることに注意してください。Cygwin解説サイトなどでフォルダの移動に慣れてからの方が良いかもしれません。

2.のビルドではwarningがいくつかでますが、最後に以下が表示されていればビルドに成功しています。

[x86] SharedLibrary  : libImageProc.so
[x86] Install        : libImageProc.so => libs/x86/libImageProc.so
ndk-buildによるC言語のモジュールのビルドが終わったら、androidにアプリをインストールします。ndk-buildを実行したのにandroidアプリに変更が反映されなかった場合、eclipseで「プロジェクト→クリーン」を実行して、apkを作り直せばよいでしょう。

実行すると、図のようなグレースケール画像が得られます。

エッジ画像風にしてみる

最後にもう一つ、画像処理らしさを出すために、エッジ画像風の画面表示にしてみます。ここでは数学的な厳密さよりも、記述のシンプルさと見た目のわかりやすさを重視して、「水平方向微分と垂直方向微分の絶対値和」を画像として表示してみます。

上で記述したグレースケールのコード中に「※」を記した位置があるのですが、そこに下記のコードを挿入してください。
  // ※部に下記を挿入

  // |水平微分| + |垂直微分| の整数倍を配列pixelsに格納
  for(j=0 ; j<IMG_HEIGHT-1 ; j++){
    for(i=0 ; i<IMG_WIDTH-1 ; i++){

      int Y1 =0xff & pixels[getIndex(i, j)];
      int Y2 =0xff & pixels[getIndex(i+1, j)];
      int Y3 =0xff & pixels[getIndex(i, j+1)];
      int Y4 =0xff & pixels[getIndex(i+1, j+1)];

      int x_diff = (Y1-Y2+Y3-Y4)/2; // 水平方向微分
      int y_diff = (Y1+Y2-Y3-Y4)/2; // 垂直方向微分

      //それぞれ絶対値をとる
      if(x_diff<0){
        x_diff = -x_diff;
      }
      if(y_diff<0){
        y_diff = -y_diff;
      }
      
      int diff = x_diff + y_diff;

      diff *= 5; // 画面表示用に明るくするため、任意数倍

      if(diff > 255){
        diff = 255; // 255以下に丸める
      }

      //緑色のエッジ風に表示
      pixels[getIndex(i, j)] = 0xff000000 |  diff<<8  ;
    }
  }
  //右端を黒に
  for(j=0 ; j<IMG_HEIGHT ; j++){
    pixels[getIndex(IMG_WIDTH-1, j)] = 0xff000000;
  }
  //下端を黒に
  for(i=0 ; i<IMG_WIDTH ; i++){
    pixels[getIndex(i, IMG_HEIGHT-1)] = 0xff000000;
  }
これをndk-buildによりビルドしてインストールすると下記のような画像が得られます。

なかなかいい感じです。


おしまい

以上、Wifiカメラから得られた画像をピクセルを直接操作することで画像処理してみました。

前回の紹介で「古い端末でも映像の取得が高速な場合がある」という趣旨の記述をしました。それはWifi経由での画像取得の速度についての記述だったのですが、今回の画像処理は、純粋に計算能力で速度が決まりますので、ほとんどの場合、新しい端末の方が高速に動作します。

ちなみに、最後に行った「エッジ画像風表示」をAi-Ballで行った場合、Galaxy S3では1秒以内に処理が完了しますが、Xperia Arcでは数秒の計算遅延が起こります。

実際の応用では無理に全画面を処理の対象とせず、画像の一部だけにするなど、何らかの工夫が必要になる場合もあります。そういう場合でもこのピクセルを直接いじる方法は柔軟に対応できます。

こちらもどうぞ

0 件のコメント:

コメントを投稿