lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

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