AndroidでHSV変換
以前、2回ほどHSV変換に取り組んだ事がありました。
iOSデバイスでHSV変換をして一定以上の彩度・明度の要素を取り出し、光センサー代わりとするアプリを設計していました。
#「アプリ」といえるレベルではありませんが…
さて、この機能をAndroidとWindows Phone7.5でも実装出来るんじゃないかと考えた次第です。
高速化したいのと、解像度はいらないのでカメラのプレビュー画面をそのまま取得して画像処理します。(面倒くさいのでNDKはまだ使わない予定)
伝達関数のガンマ値補正などは明示的には行っていません。
ここでネックとなるのが画面遷移です。現状ではほとんどのスマートフォンがGUIインスタンスがシングルタスクですので、それぞれの手法に合わせなければいけない訳です。
iOSではUIViewControllerメソッドを作りUIViewのインスタンスを生成します。そして、その画面で表示する部品をUIViewに追加してデリゲートのメソッドで追加して遷移します。詳しくはこの時に書いたObj-Cコードを参考にしてください。
AndroidではActivityメソッドがiOSのUIViewControllerにあたると考えて良い様です。画面遷移は明示的インテントにより遷移先のActivityをスタートさせます。レイアウトはそれぞれの画面のxmlファイルを記述してやる必要があります。(iOSの.xibに相当)
余談ですが、Windows PhoneではSilverlightベース(XNA Game studioプラグインを使ったXNAアプリケーションもある) になっていて、webページの概念に基づいているため画面遷移はハイパーリンクを利用して移動します。 ページそれそれにレイアウトのxaml(読み:ザムル)ファイルを用意します。
でも今回は通常のView使わずにSurfaceViewを使おうと思います。SurfaceViewは高速に描画を行うための仕組みで、
別スレッドで描画するためある程度高速ですが、排他処理などマルチスレッドを意識する必要があります。カメラアプリケーションでは必須です。
SurfaceViewのインスタンスを生成して、SurfaceHolderインターフェースにコールバックすればよいことになります…
が、SurfaceViewのレイアウトをxmlに記述するにはどうやればいいのかが解りません。
- 参考にさせて頂いたブログ
SepiAndroid - Androidでアプリ開発体験 -::レイアウトxmlでSurfaceViewを使用する
助かりました。ありがとうございます。
要約すると
findViewByIdメソッドによってxmlファイルで設定したレイアウトを参照→ メンバ保持→ SurfaceViewクラスをインスタンス化する際の引数に渡す→ SurfaceViewを使用するためのコールバックとして登録
よって、Activityメソッドをこんな感じで生成。
(CameraCoderActivety.java)
package com.lynxeyed.cameracoder; import com.lynxeyed.cameracoder.R.id; import android.app.Activity; import android.os.Bundle; import android.widget.SeekBar; import android.view.SurfaceView; public class CameraCoderActivity extends Activity { /** Called when the activity is first created. */ SeekBar seek_S,seek_V; private SurfaceView mSurfaceView; private CameraPreview mCameraPreview; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mSurfaceView = (SurfaceView) findViewById(id.surfaceView1); //Surfaceレイアウト参照 seek_S = (SeekBar) findViewById(id.seekBar1); seek_V = (SeekBar) findViewById(id.seekBar2); mCameraPreview = new CameraPreview(this, mSurfaceView, seek_S, seek_V); //インスタンス化、引数として受け渡し } }
レイアウトはこんな感じ。(res/layout/main.xml)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="horizontal" android:layout_weight="1" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:layout_weight="2" > <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/textView2" android:textAppearance="?android:attr/textAppearanceLarge" /> <SeekBar android:id="@+id/seekBar1" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/textView3" android:textAppearance="?android:attr/textAppearanceLarge" /> <SeekBar android:id="@+id/seekBar2" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="false" android:text="@string/textView" /> <Button android:id="@+id/button_capture" android:layout_width="match_parent" android:layout_height="34dp" android:text="Capture" /> </LinearLayout> <SurfaceView android:id="@+id/surfaceView1" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" /> </LinearLayout> </LinearLayout>
SurfaceHolder(CameraPreview.java)
画像の彩度、明度のしきい値をSeekBarで設定し、その値以上の画素を抽出できるようにしました。下の画像で、ピンクの部分です。
抽出した画素をRGB空間に書き戻しています。
カメラから取得できるフォーマットはYUVなので、YUV→RGB→HSV→RGB(24bit)に直しています。
package com.lynxeyed.cameracoder; import java.lang.Math; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.hardware.Camera; import android.view.SurfaceView; import android.view.SurfaceHolder; import android.widget.SeekBar; import android.util.Log; public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder mHolder; private Camera mCamera; private Bitmap bitmap; private int[] rgb; private int width, height; private final String TAG = "CameraPreview"; private static SeekBar sb_S,sb_V; //preview callback private final Camera.PreviewCallback _previewCallback = new Camera.PreviewCallback() { public void onPreviewFrame(final byte[] data, Camera camera) { decodeYUV420SPHSV(rgb, data, width, height); bitmap.setPixels(rgb, 0, width, 0, 0, width, height); // 描画 // canvasをLock Canvas canv = mHolder.lockCanvas(); canv.drawBitmap(bitmap, 0, 0, null); // lockしたCanvasの解放 mHolder.unlockCanvasAndPost(canv); } }; // constructor public CameraPreview(Context context, SurfaceView sv,SeekBar sb_S,SeekBar sb_V) { super(context); this.sb_S = sb_S; this.sb_V = sb_V; // Install a SurfaceHolder.Callback so we get notified when the // underlying surface is created and destroyed. mHolder=sv.getHolder(); mHolder.addCallback(this); // deprecated setting, but required on Android versions prior to 3.0 mHolder.setType(SurfaceHolder.SURFACE_TYPE_NORMAL); sb_S.setMax(100); sb_V.setMax(100); sb_S.setProgress(50); sb_V.setProgress(50); } public void surfaceCreated(SurfaceHolder holder) { // The Surface has been created, now tell the camera where to draw the preview. try { mCamera=Camera.open(); mCamera.setPreviewCallback(_previewCallback); } catch (Exception e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } } public void surfaceDestroyed(SurfaceHolder holder) { // empty. Take care of releasing the Camera preview in your activity. mCamera.stopPreview(); mCamera.setPreviewCallback(null); mCamera.release(); mCamera = null; } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { // If your preview can change or rotate, take care of those events here. // Make sure to stop the preview before resizing or reformatting it. width = w; height = h; bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); rgb = new int[w * h]; // カメラのプレビュー開始 Camera.Parameters parameters = mCamera.getParameters(); parameters.setPreviewSize(w, h); mCamera.setParameters(parameters); mCamera.startPreview(); if (mHolder.getSurface() == null){ // preview surface does not exist return; } } // YUV420 to BMP to HSV to BMP static public void decodeYUV420SPHSV(int[] rgb, byte[] yuv420sp, int width, int height) { // ここで画像サイズの縮小をする。 // 及びYUV420→BMP→HSV→(SeekBarのデータ取得ししきい値以上であればその画素を抽出。) // →BMPに書き戻しする。長いので略 } }
HSV処理部は
// YUV420 to BMP static public void decodeYUV420SPHSV(int[] rgb, byte[] yuv420sp, int width, int height) { double dh = 0,ds,dv; double c; double cmax,cmin; double tHold_V = sb_V.getProgress()/100.0; double tHold_S = sb_S.getProgress()/100.0; final int frameSize = width * height; for (int j = 0, yp = 0; j < height; j++) { int uvp = frameSize + (j >> 1) * width, u = 0, v = 0; for (int i = 0; i < width; i++, yp++) { int y = (0xff & ((int) yuv420sp[yp])) - 16; if (y < 0) y = 0; if ((i & 1) == 0) { v = (0xff & yuv420sp[uvp++]) - 128; u = (0xff & yuv420sp[uvp++]) - 128; } int y1192 = 1192 * y; int r = (y1192 + 1634 * v); int g = (y1192 - 833 * v - 400 * u); int b = (y1192 + 2066 * 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; //HSV変換 //RGB->HSV変換時のR,G,Bは 0.0〜1.0 double dr = r / 262143.0; double dg = g / 262143.0; double db = b / 262143.0; //hsvに変換 if (dr >= dg) cmax = dr; else cmax = dg; if (db >= cmax) cmax = db; if (dr <= dg) cmin = dr; else cmin = dg; if ( db <= cmin) cmin = db; dv = cmax; c = cmax - cmin; if (cmax == 0)ds = 0; else ds = c/cmax; if (ds != 0){ if (dr == cmax) dh = 60 * (dg - db)/c; else if (dg == cmax) dh = 60 * (db - dr)/c + 120; else if (db == cmax) dh = 60 * (dr - dg)/c + 240; if (dh < 0) dh = dh + 360; } if(ds < tHold_S){ if(dv < tHold_V){ dh = 0; ds = 0; dv = 0; } else ds = tHold_S; } //rgb空間に再変換 int inn = (int)Math.floor( dh / 60.0 ); if(inn < 0) inn *= -1; double fl = ( dh / 60 ) - inn; //if((inn & 1)!=0) fl = 1 - fl; // if i is even double p = dv * ( 1 - ds ); double q = dv * ( 1 - ds * fl ); double t = dv * (1 - (1 - fl) * ds); ////計算結果のR,G,Bは0.0〜1.0なので255倍 dv = dv * 255.0; p = p * 255.0; q = q * 255.0; t = t * 255.0; switch( inn ) { case 0: r = (int)dv; g = (int) t; b = (int)p; break; case 1: r = (int)q; g = (int) dv; b = (int)p; break; case 2: r = (int)p; g = (int) dv; b = (int)q; break; case 3: r = (int)p; g = (int) q; b = (int)dv; break; case 4: r = (int)t; g = (int) p; b = (int)dv; break; case 5: r = (int)dv; g = (int) p; b = (int)q; break; } rgb[yp] = 0xff000000 | ((r << 16) & 0xff0000) | ((g << 8) & 0xff00) | ((b) & 0xff); } } }
しきい値をあげると…
こんな感じで、一定以上の彩度、明度に反応できるようにさせれば、簡易センサーの出来上がり。
ちょっとSeekBarの反応がヤバいんじゃないのかなと思ったけど想像通り、反応が鈍い。やっぱ別ハンドラで真面目に作らないとダメなのかなぁ…
- その他の参考ページ
Androidでモザイク画像を作ってみる::Android Techfirm Lab