ラベル 画像処理 の投稿を表示しています。 すべての投稿を表示
ラベル 画像処理 の投稿を表示しています。 すべての投稿を表示

2012年12月29日土曜日

AndroidでWifiカメラから受け取った動画を画像処理する(2)OpenCVを用いる方法

前回、Wifiカメラから受け取った映像をピクセル操作によって画像処理してみました。しかし、簡単な処理でも記述量が多くなるという問題がありました。

そこで、今回はOpenCVと呼ばれるライブラリを用いてお手軽に画像処理をしてみます(といっても、OpenCVを用いるのは私も今回が初めてなのですが)。

準備として、前回の前半に書いてあるように、SimpleMjpegViewがandroid上で動作するようにしておいてください。前回の続きでも良いですし、新規に始めても構いません。新規の場合は図のように映像が表示されます。

OpenCVのインストール

まずはeclipseがインストールされたマシンにandroid用のOpenCVをインストールします。こちらからopencv-android→(最新の数字)とたどり、OpenCV-x.x.x-android-sdk.zipをダウンロードします。私が試したときの最新版はOpenCV-2.4.7.1-android-sdk.zipでした。解凍して現れるファイルを、例えばeclipseのワークスペースにコピーします。

次に、OpenCVのフォルダに含まれるsdk/javaフォルダをeclipseにインポートします。通常通り、「ファイル→インポート→既存プロジェクトをワークスペースへ」で行います。

次に、SimpleMjpegViewからOpenCVを参照する設定を行います。eclipse上のプロジェクトエクスプローラー上でSimpleMjpegViewを右クリック→プロパティーとたどり、項目の中のandroidをクリックすると、下半分にライブラリーという項目があるので、追加ボタンをクリックして、OpenCVライブラリを追加します。

最後に、アプリをインストールするandroid端末に、Google PlayからOpenCV Managerをインストールしておきます。

以上の準備が済んだら、ファイルの編集を行います。

ファイルの編集

まず、初期化処理のためにSimpleMjpegViewのsrcフォルダ以下にあるMjpegActivity.javaを編集します。冒頭のimport文に、下記の 3項目を追加します。
import org.opencv.android.OpenCVLoader;
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
次に、MjpegViewのインスタンスmvの宣言の直後に下記の初期化処理を記述します(※ソース更新に合わせて、若干の変更 2013.1.30/2013.4.29)。
private MjpegView mv = null;
String URL;

// mv と URL の宣言の直後に下記を記述

private BaseLoaderCallback mOpenCVCallBack = new BaseLoaderCallback(this) {
  @Override
  public void onManagerConnected(int status) {
    switch (status) {
      case LoaderCallbackInterface.SUCCESS:
      {
        Log.i(TAG, "OpenCV loaded successfully");
        setContentView(R.layout.main);
        mv = (MjpegView) findViewById(R.id.mv);
        if(mv != null){
          mv.setResolution(width, height);
        }
        new DoRead().execute(URL); 
      } break;
      default:
      {
         super.onManagerConnected(status);
      } break;
    }
  }
};
そして、同じファイル内で、MjpegViewをnewしている部分をみつけ、下記のようにコメントアウトした上で初期化処理の追記を行います(※ソース更新に合わせて、若干の変更 2013.1.30/2013.4.29)。
// 下記の6行を見つけてコメントアウトし、代わりに下記を追記

// setContentView(R.layout.main);
// mv = (MjpegView) findViewById(R.id.mv);
// if(mv != null){
//   mv.setResolution(width, height);
// }
// new DoRead().execute(URL);

if (!OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_3, this, mOpenCVCallBack))
{
  Log.e(TAG, "Cannot connect to OpenCV Manager");
}
次に、画像処理の本体を記述します。SimpleMjpegViewのsrcフォルダ以下にあるMjpegView.javaを編集します。まず冒頭のimport文の中に、下記の四項目を追加します。
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
次に、同じくMjpegView.java内で下記のように readMjpegViewを呼び出している部分を見つけます。前回の続きとして実行する場合は、imageprocessing関数が記述されていると思いますが、その場合はその行をコメントアウトしてください。

そしてその後ろに5行のOpenCV命令を記述します。これは画像のグレースケール化を行う処理なのですが、詳細は末尾に記すTechBoosterさんのサイトを参考にしてください。
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;
}        
// 前回の続きとして実行している場合は下記1行をコメントアウトしておく
//imageprocessing(bmp);

//OpenCVによる画像処理
Mat bmp_mat = new Mat(IMG_HEIGHT, IMG_WIDTH, CvType.CV_8UC4);
Utils.bitmapToMat(bmp, bmp_mat);
Imgproc.cvtColor(bmp_mat, bmp_mat, Imgproc.COLOR_RGB2GRAY);
Imgproc.cvtColor(bmp_mat, bmp_mat, Imgproc.COLOR_GRAY2RGBA, 4);
Utils.matToBitmap(bmp_mat, bmp);
以上の記述ができたらandroidにアプリをインストールします。
成功すると、図のようにグレースケール画像が得られます。

このように、OpenCVを用いると少ない記述量で様々な処理を行うことができます。OpenCVが流行っている理由がわかる気がします。

計算時間の比較

前回の「(JNIによる)ピクセル直接編集」と今回のOpenCVの計算にかかる時間はどの程度か調べてみます。対象は今回取り扱ったグレースケール処理の部分のみで、かかった計算時間を100回以上平均をとって計測しました。結果は以下の通りです。

端末ピクセル直接編集OpenCV
Galaxy S3
cm10-based JCROM
7.7ms14.8ms (*)
Xperia Arc
cm10-based JCROM
13ms23ms

みてわかるように、2つの端末の両方でピクセル直接編集の方が高速であることがわかりました。といっても、私はOpenCVを今回初めて使ったので、もっとOpenCVを効率的に用いる方法があるのかもしれませんが。なお、参考までに、表中で (*) のついた 14.8ms について、OpenCVで用いた5行の画像処理命令の内訳を記すと下記のようになります。

Mat bmp_mat = new Mat(IMG_HEIGHT, IMG_WIDTH, CvType.CV_8UC4); 0.2ms
Utils.bitmapToMat(bmp, bmp_mat); 3.1ms
Imgproc.cvtColor(bmp_mat, bmp_mat, Imgproc.COLOR_RGB2GRAY); 6.5ms
Imgproc.cvtColor(bmp_mat, bmp_mat, Imgproc.COLOR_GRAY2RGBA, 4); 4.4ms
Utils.matToBitmap(bmp_mat, bmp); 0.6ms

RGB2GRAYがグレースケール化の実体だと思いますが、その前後のbitmapToMatとGRAY2RGBAに地味に時間がかかるのが痛いように思います。

おしまい

今回私も初めてのOpenCV体験でしたので、下記の薄い本とサイトを参考にさせて頂きました。ありがとうございました。

こちらもどうぞ

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では数秒の計算遅延が起こります。

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

こちらもどうぞ