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

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

こちらもどうぞ

2012年12月17日月曜日

AndroidでWifiカメラからのMJPEGストリームを表示する

最近ひとりぶろぐさんのエントリ
でも紹介されていましたが、Ai-BallのようにMJPEGストリームを送信できるカメラが楽しいです。

ここではその映像をandroidで受信する方法について紹介します。利用形態は左図のようなイメージです。

サンプルコードはstack overflowの「Android ICS and MJPEG using AsyncTask」で既に紹介されています。しかし、このコードを用いると
  • メモリ使用量が多くGarbage Collectionが一秒間に多数回実行される
  • 端末によっては映像の遅れが1秒以上起こる
となり、少し性能が物足りないです。

MJPEGを表示するアプリケーションでGoogle Playでダウンロードできるものとしては「MJpeg Viewer」がありますが、これも同様の問題を抱えています。

この問題を解決するため、その高速版として「SimpleMjpegView」を作成して公開しました。
(ソースだけではなく、ビルド済みアプリもGoogle Playからダウンロードできるようにしました。2013.4.29)


メモリ使用量を削減し、jpegのデコードにJNI経由でlibjpegを利用しているのが主な変更点です。上記の問題に対しては下記のように解決されています。
  • Garbage Collection は1秒に1回以下(端末依存、後述)
  • 映像の遅れは1秒より大幅に短い(<< 1秒)
このコードをベースに作成したのが下記の、WifiカメラAi-Ballを搭載したプラレールです。


このプラレールを2012年10月20日(土)に仙台にて行われたICT ERA + ABC 2012 東北や2012年12月1日(土)、2日(日)に日本未来科学館で行われたMaker Faire Tokyo 2012にて展示させて頂きました。

ここでは、そこでの体験なども含め、MJPEGストリームをAndroidで表示する方法についてまとめてみたいと思います。

利用形態

(1)Wifiカメラ+スマホ

Trek Ai-Ballが用いている方式で、カメラ自体がアクセスポイントになります。メリットはとにかく小型であることです。

Planexからも多数のネットワークカメラが発売されていますが、それらで動作するかは試していません。

URLはデフォルトでは
http://192.168.2.1:80/?action=stream
となります。


(2)スマホ+Wifiルーター+スマホ

カメラデータ送信用スマートフォンにIP WebcamというフリーソフトをインストールしてWifiカメラ化し、そこからの映像をWifiルーター経由で受信するという方式です。(1)よりも無線が安定するのではないかと導入してみました。

URLはデフォルトでは
http://192.168.x.y:8080/videofeed
となります。

(3)スマホ(テザリング)+スマホ

(2)と同じですが、Wifiルーターの代わりにスマートフォンのテザリング機能を用いるものです。

部品点数が減るのがメリットですが、送信側のスマートフォンの電池持ちが悪くなるのがデメリットです。


(4)PC+Wifiルーター+スマホ

LinuxなどのUnix系PCにmjpeg streamerというソフトウェアを導入して、UVC対応のUSBカメラの映像をルーター経由で送信します。

様々なカメラを用いることができるのがメリットですが、PCとカメラを有線接続する必要があるため、規模が大きくなるのがデメリットです。

こちらの方法を(ノートPCなどでなく)Raspberry Piで使っている方々がいらっしゃいます。

(5)Raspberry Pi+カメラモジュール

(4)と少し似ていますが、こちらはUSBカメラではなく、Raspberry Pi専用のカメラモジュールを用いる方法です。画質も良いのでお勧めの方法です。下記で解説しています。



性能

stack overflowで公開されているサンプルと、性能改善したSimpleMjpegViewとで性能を比較したのが下記の表です。カメラは「利用形態」の(1)で紹介したTrek Ai-Ballを用いました。解像度640x480、30fpsの映像が送られてきます。

なお、たまたま手元にあった端末が全てカスタムOSが載ったものだったのですが、結果は公式OSでも変わりません。また、OSのバージョンにもあまり依存しないようです。

まず、オリジナルのサンプルから。

stack overflow のサンプル
端末 フレーム数 (fps) 1秒あたりのGC回数 時間遅れ (s)
Nexus 7
OS: 4.2.1, JCROM
15 25 << 1
Galaxy S3 (docomo)
OS: 4.1.2, cm10-based JCROM
17 4 << 1
Galaxy Nexus
OS: 4.2.1, JCROM
16 26 > 1
Xperia Arc
OS: 4.1.2, CM10-based JCROM
17 18 << 1
Nexus S
OS: 4.2.1, JCROM
11 30 > 1

ここで見るべきポイントは以下の通りです。
  • どの端末も、1秒間に多数回のGarbage Collection (GC)が実行される
  • フレーム数はそれほど悪くないが、1秒以上の映像の時間遅れがある端末がある
  • 新しい端末の方が性能が良いとは限らない。最新機種のGalaxy S3の性能が良い一方で、古い端末であるXperia Arcの性能も良い
一方、性能改善したアプリでは下記のようになります。

性能改善したSimpleMjpegView
端末 フレーム数 (fps) 1秒あたりのGC回数 時間遅れ (s)
Nexus 7
OS: 4.2.1, JCROM
17 1 << 1
Galaxy S3 (docomo)
OS: 4.1.2, cm10-based JCROM
17 およそ1/10 << 1
Galaxy Nexus
OS: 4.2.1, JCROM
17 1 << 1
Xperia Arc
OS: 4.1.2, CM10-based JCROM
17 1/4 << 1
Nexus S
OS: 4.2.1, JCROM
17 1 << 1

見るべきポイントは下記の通りです。
  • どの端末もGCの回数は秒間1回以下に改善(性能が良いのはやはりGalaxy S3とXperia Arc)
  • 時間遅れはどの端末でも1秒以下に改善された

端末による性能差

上の表で見た通り、映像表示の性能には端末による差があり、なおかつ新しい端末の方が性能が良いとは限らない、という現象が見られます。

性能改善したSimpleMjpegViewを用いる場合、映像表示するだけならそれほど差は見られませんが、上のプラレールの動画のように、画像処理に基づいて制御を行う場合、微妙な性能差が結果に影響を及ぼすことがあるので注意が必要です。

下記の端末で特に性能が悪かったのが印象的です。
  • Galaxy Nexus
  • Nexus S
上記のプラレールの動画でXperia Arcを用いているのは、古い端末で安価に入手できるにも関わらず映像表示の性能が良いためです。今後、Galaxy S3の価格が下がって入手しやすくなればそちらに乗り換えるかもしれません。

なお、この端末による性能差の原因はわかっていませんが、ハードウェアの違いやkernelのドライバに起因する問題ではないかと現在のところは考えています。

Wifiの干渉についての体験記

このようなWifiカメラの利用法の一つとして、自作のロボットなどに搭載して遊ぶ、というものが考えられます。個人で遊ぶぶんには何の問題もないのですが、大勢の人が集まる展示会でデモしようとすると、Wifiの干渉により映像が遅れるなどして、期待の動作をしないということがあります。

この現象は個人で再現することがなかなか難しく、有効な対策ができない場合が多いのですが、ここでは私が展示会で体験した経験をいくつか紹介したいと思います。

結論から先に述べると、「利用形態」の(3)で紹介した「スマホ(テザリング)+スマホ」が、Wifiの干渉に最も強い組み合わせでした。

2012年10月20日(土)ICT ERA + ABC 2012 東北(Trek Ai-Ball + Xperia Arc, 802.11g)

動画で紹介したプラレールを展示した初めての展示会でしたが、「利用形態」(1)で紹介したWifiカメラ(Trek Ai-Ball)で送信した映像をXperia Arcで受信しました。

この場合、映像が3~5秒くらい遅れて届いたため、画像処理に基づく制御はあきらめ、Bluetooth経由での手動制御を中心に紹介しました。なお、Trek Ai-BallはWifiのチャンネルを手動で変更する機能がついているのですが、それを行っても焼け石に水という感じでした。

2012年12月1日(土)Maker Faire Tokyo 2012 (Xperia Ray + Router + Xperia Arc, 802.11n)

ABC2012東北の約1か月後に行われたMaker Faire Tokyo 2012の初日です。Trek Ai-Ballでの展示はあきらめ、「利用形態」(2)で紹介した「スマホ+ルーター+スマホ」の組み合わせで行いました。

カメラはXperia RayにIP Webcamを入れてWifiカメラ化し、プラレールに搭載します(左図)。ルーターはNECのAtermWR8370N、受信はいつも通りXperia Arcです。

結果はボチボチ、といったところでした。カメラの映像は基本的にはスムーズに届き、画像処理に基づく制御もうまく機能しました。ただしやや不安定で、1~2分おきに映像が届かなくなる現象が起こりました。そんなときはルーターとスマホの位置関係を変えると突然映像が復帰することが多かったので、そのまま展示を続けることができました。

不安定ではありましたが、少なくとも昼から17時までの展示はなんとか乗り切ることができました。

2012年12月2日(日)Maker Faire Tokyo 2012 (Xperia Ray + Xperia Arc, 802.11g)

Maker Faire Tokyo 2012の2日目です。前日と同様に「スマホ+ルーター+スマホ」の組み合わせで行こうとしたのですが、前日と異なり映像が頻繁に途切れてうまく流れません。

想像ですが、恐らく会場では私と同様に2.4GHz帯の無線を使って何かデモをしようとした方が大勢いらっしゃったと思います。初日に無線の干渉の問題でデモに失敗した方が、2日目にリベンジとして別の無線機器を持ち込んだ、ということがあったかもしれません。

いずれにせよ前日の組み合わせではうまくいかなかったので、「利用形態」の(3)で紹介した「スマホ(テザリング)+スマホ」の組み合わせを試してみたところ、(何故か)非常に安定して映像が送受信でき、終日それで乗り切りました。1日目のルーターを噛ませる場合に比べて安定性はかなり上でした。

テザリングを行ったスマホはXperia Rayで、この上でIP Webcamも動作させました。テザリングはdocomoの公式OSでもカスタムOSでも問題なくできます(SIMは刺さずにデモしました)。ただし、当然電池の持ちは悪いので予備バッテリーは必須です。10:00~17:00の展示で、Rayの電池は2回交換したと思います。

Wifi干渉についてのまとめ

以上、展示会3日分の体験を紹介しました。少なくともこの3日間の体験から判断するなら、安定性は「スマホ(テザリング)+スマホ」で決まり、なのですが、もう少し検証をしたい気もします。展示会ほど無線が飛び交う状況をなかなか作れないので、新たに展示会に出展することでしか検証は難しいのですが。

あと、この3日間で用いた無線の規格は802.11gか11nのどちらかです。802.11aを用いたらどうなるだろう、という興味もありますが、この場合、映像の送信側も受信側も802.11aに対応している必要があります。個人的にはXperia SXが小型で11aに対応しているので送信側に適してそうな気がしています。

おしまい

以上、私自身の体験も交えてWifiカメラからのMJPEGストリームをandroidで表示する方法についてまとめてみました。

気が向いたら、画像処理の方法(ピクセルを直接いじる方法、OpenCVを用いる方法)や複数個のカメラからの映像を一つの端末で受信する方法などについても書いてみたいと思います。

こちらもどうぞ