appC cloud 組み込んだ

 「ぞろぞろ」の顔認識は、ぼんやり考えてはいるものの1行もコード書かず。
 
 その代わり「カラフルフロア」の宣伝にと、レビュー掲載保障に釣られて appC cloud を組み込みました。
 リストビューを組み込んだのですが、あら簡単。雛形どおりでコード書くこと少なし。
device-2015-02-08-083140
 「おすすめアプリ」として、さりげなく大きめのアイコンで表示しました。
 
 広告表示だけのアップグレードもいやらしいかなと思ったので、ゲーム画面のキャプチャー機能を追加・・というか前々からあった機能を表示。使い道ないけどね。

public void captureScreen(int stage) {
	Calendar myCal = Calendar.getInstance();
	SimpleDateFormat myFormat = new SimpleDateFormat("MMddhhmm");
	final String fileName = myFormat.format(myCal.getTime()) + "-" + stage + ".png";
	ScreenGrabber screenCapture = new ScreenGrabber();
	hud.attachChild(screenCapture);

	final int viewWidth = camera.getSurfaceWidth();	
	final int viewHeight = camera.getSurfaceHeight();
	screenCapture.grab(0, 0, viewWidth, viewHeight, new IScreenGrabberCallback(){
		public void onScreenGrabbed(Bitmap pBitmap) {
			resourcesManager.saveBitmapToSDcard(pBitmap, fileName);
			messageSave(fileName);
		}

		public void onScreenGrabFailed(Exception pException) {}
	});
}

 
 まだ、エディット機能隠してある。

顔認識してみるか・・・まずはモザイク、グレースケールに

 作成中の「ぞろぞろ」はマップに従いパーティクルで髭を生やすだけなのですが、何人かの顔を写して髭を生やしてみたところ、面長の方は髭が宙に浮いてしまうのです。枠に合わないのですね。
 丸顔専用のアプリにするか、横方向に拡縮させて位置を合わせようかとも思ったのですが、拡縮はタッチ位置のパーティクルをリセットするために座標変換が必要になってくるはずで厄介そう。
 
 そこで、顔認識に挑戦してみるかと、顔を撮影しているのは判っているのだから目鼻口輪郭を切り出そうかと、そう思い立ったわけです。
 こんなアプリに何故にそこまで、と思われるかも知れませんが、この挑戦が次のアプリにつながったりするのですね。
 
 手始めに写真をモザイク可してみました。
device-2015-01-31-203934
 ブロック内のRGB値をそれぞれ足し上げて、平均値を戻せばモザイクになります。

	private Bitmap mosaicBitmap(Bitmap bm, int dot){
		Bitmap bitmap = bm.copy(Bitmap.Config.ARGB_8888, true);
		final int width = bitmap.getWidth();
		final int height = bitmap.getHeight();
		int[] pixels = new int[width * height];
		int[] mosaic = new int[(width/dot)*(height/dot)];
		bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
		int r, g, b;
		int color;
		for(int i = 0 , mx = 0 ; i < height ; i += dot , mx++){
			for(int j = 0 , my = 0 ; j < width ; j += dot , my++){
				r = 0;
				g = 0;
				b = 0;
				for(int x = i ; x < i + dot ; x++){
					for(int y = j ; y < j + dot ; y++){
				        r += Color.red(pixels[x*width + y]);
				        g += Color.green(pixels[x*width + y]);
				        b += Color.blue(pixels[x*width + y]);
					}
				}
				color = Color.rgb(r/(dot*dot), g/(dot*dot), b/(dot*dot));
				mosaic[mx*(width/dot) + my] = color;
				for(int x = i ; x < i + dot ; x++){
					for(int y = j ; y < j + dot ; y++){
						pixels[x*width + y] = color;
					}
				}
			}
		}
		bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
		return bitmap;
	}

 今回は、モザイク画像を表示する必要もなく mosaic[] に色情報を格納しておけば足りるのですが、正しく動いているか確認するために Bitmap を返しています。
 
 そしてグレースケール化してみます。
device-2015-01-31-205058
 ピクセル毎にRGB値をNTSC系加重平均法で計算した値に置き換えています。

	private Bitmap grayScaleBitmap(Bitmap bm){
		Bitmap bitmap = bm.copy(Bitmap.Config.ARGB_8888, true);
		final int width = bitmap.getWidth();
		final int height = bitmap.getHeight();
		int[] pixels = new int[width * height];
		bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
		int r = 0;
		int g = 0;
		int b = 0;
		int color = 0;
		for(int i = 0 ; i < height ; i++){
			for(int j = 0 ; j < width ; j++){
		        r = Color.red(pixels[i*width + j]);
		        g = Color.green(pixels[i*width + j]);
		        b = Color.blue(pixels[i*width + j]);
				color = (int) (r * 0.298912 + g * 0.586611 + b * 0.114478);
				pixels[i*width + j] = Color.rgb(color, color, color);
			}
		}
		bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
		return bitmap;
	}

 mosaic[] をグレースケール化する肝心の処理は書いていなかったりします。画像表示は完全にいらん処理です。
 
 ちなみに AndEngine で Bitmap picture をグレーモザイクにするために、各メソッドから戻る Bitmap を予め作成してある BitmapTextureAtlas pictureTextureAtlas や ITextureRegion picture_region とする方法はこんなコードです。
 詳しいことは聞かないでください。

	public void test(int dot){
		final Bitmap bitmap = grayScaleBitmap(mosaicBitmap(picture, dot));
		if(pictureTextureAtlas != null){
			picture_region = null;
			pictureTextureAtlas.unload();
			pictureTextureAtlas = null;
		}
		this.pictureTextureAtlas = new BitmapTextureAtlas(activity.getTextureManager(), 1024, 1024, TextureOptions.BILINEAR);
		final IBitmapTextureAtlasSource baseTextureSource = new EmptyBitmapTextureAtlasSource(bitmap.getWidth(), bitmap.getHeight());
		final IBitmapTextureAtlasSource decoratedTextureAtlasSource = new BaseBitmapTextureAtlasSourceDecorator(baseTextureSource){
			@Override
			protected void onDecorateBitmap(Canvas pCanvas) throws Exception{
				pCanvas.drawBitmap(bitmap, 0, 0, this.mPaint);
			}
			@Override
			public BaseBitmapTextureAtlasSourceDecorator deepCopy(){
				return this;
			}
		};
		this.picture_region = BitmapTextureAtlasTextureRegionFactory.createFromSource(this.pictureTextureAtlas, decoratedTextureAtlasSource, 0, 0);
		this.pictureTextureAtlas.load();
	}

 
 さて、このグレーモザイク画像から目鼻口輪郭を認識することができるのか!?
 
 できない時には「ぞろぞろ」は丸顔専用アプリとなります。

カメラ周りがエラーまみれ

 「ぞろぞろ」というタイトルのアプリを作り始めました。
device-2015-01-12-114341
 カメラ機能を使いたいので、以前作った「秘密のスライドパズル」のソースを利用・・
 ic_launcher-web
したところ、カメラ周りでエラーが出ていることが発覚し(うすうす気付いてい)ました。
 
 「秘密のスライドパズル」のカメラ(この先出てくる「カメラ」は android.hardware.Camera の方ね。org.andengine.engine.camera ではありません。)は、標準アプリを使わずに SurfaceView を継承した CameraPreviewSurfaceView を表示して、その上に背景を透明にした AndEngine の Scene を乗せています。画像は、カメラ機能の takePicture() を使わずに、プレビュー画面を取得して decodeYUV420SP() メソッドでデコードするという、いわゆる無音カメラの作り方ですね。無音にするのが目的ではないので、シャッター音は付けていますが。
 
 で、改めてASUS ME176 シリーズ タブレットPC ホワイト ( Android 4.4.2 / 7 inch / Atom Z3745 / 1GB / eMMC 16G / WIFI対応 ) ME176-WH16で起動してみたところ、decodeYUV420SP() で ArrayIndexOutOfBoundsException でごわす。
 
 「秘密のスライドパズル」は、480×800 の画面サイズを固定表示しているので、カメラのプレビューサイズも 480×800 に一番近い解像度を選択しているのですが、Android のカメラは横起動なので 800×480 サイズで近いカメラは見つけなければならない。これを誤っていた。否、やっていなかった。
 コード直してテストしたら、上手く動いている模様。
 
 ここで少し欲が出た。
 フロントカメラも使えるようにしたい・・。今までフロントカメラがある機種ありませんでしたからね。
 
 早速、作業に入ったところエラー連発。
 カメラの切り替え時に動いているカメラをリリースしてから改めてフロント(バック)カメラをセットしたのですが、setPreviewCallback() や setPreviewDisplay() を無視したので、リリース後にコールバックがかかりエラー。たぶん、そんなところ。
 直して、取得できるようになっても、今度はフロントカメラから取得した画像が上下逆さまになってしまう。取得元に応じて回転させて対応しました。
 ふ~。
 
 せっかく直したので「秘密のスライドパズル」をアップデートしました。

AndEngineでアニメーション中のタッチイベントを制御する

 パズルゲーム「カラフルフロア」の問題面を作りながらバグ取りをしていて、コードの中身も段々と思い出してきました。
 最後まで悩んでいたのが、タッチイベント制御周りのエラーだった・・。
 
 フォァファファファ~(回想時のSE)
 
 このゲーム、プレーヤーと同じ色のパネルには移動できるというルールなので、画面をタッチして「行ける」と判定された場合は、プレーヤーをひょこひょこアニメーションさせてタッチしたパネルまで移動させています。
 始めは、まあ誰もがされるであろうタッチ時のフラグ boolean isTouchOK を作って制御していたのですが、タッチダウン:いけるかどうか判定→タッチアップ:行ければアニメーション開始・移動という処理中に頻繁にエラーが出るのです。

 調べてみると、タッチアップ後のロック処理 isTouchOK = false を先回りして次のタッチダウン処理に入ってしまう場合があることが判明。
 アニメーションの開始時に条件を付けてみたり(←今思えば、アニメーション開始後の制御が問題なのだから関係なかったね・・)したのですが、娘の超絶タッチ連打にはなす術もなくエラーとなりフリーズしてしまいます。娘はエラーを出そうとすさまじいタップをするのです。
 
 試行錯誤の末、たどり着いたのが次のコードです。

	float x, y;
	int mx, my, pastX, pastY;

	public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent){
		// 生座標値を取得
		x = pSceneTouchEvent.getX();
		y = pSceneTouchEvent.getY();

		// パネルの位置に変換
		mx = (int)Math.floor(((x - OFFSET_X) / (TILE_WIDTH / 2) + (y - (OFFSET_Y + 12)) / (TILE_HEIGHT / 2)) / -2);
		my = (int)Math.floor(((y - (OFFSET_Y + 12)) / (TILE_HEIGHT / 2) - (x - OFFSET_X) / (TILE_WIDTH / 2)) / -2);

		// ロックされていなければ処理開始
		// 生座標値の取得などが前にあるのは、実際のコードでは他にもいろいろ処理しているため
		if(isTouchOK){
			// タッチダウン時
			if (pSceneTouchEvent.isActionDown()){
				// pastX と pastY は前回タッチダウン時の mx と my
				// 前回とタッチ位置が異なり、アニメーションが動いていないで、範囲内であれば処理開始 X、Y はパネルの縦横最大数
				if(!(pastX == mx && pastY == my && player.isAnimationRunning()) && mx >= 0 && mx < X && my >= 0 && my < Y){
					// タッチダウン時の処理
					touchDown(mx, my);
					// 今回位置を格納
					pastX = mx;
					pastY = my;
				}
			}
			// タッチアップ時
			else if (pSceneTouchEvent.isActionUp()){
				// ロックして
				isTouchOK = false;
				// タッチダウン時と同じパネル位置で(実際には isActionMove 時の処理もあるので再度)範囲内なら処理開始
				if(pastX == mx && pastY == my && mx >= 0 && mx < X && my <= 0 && my < Y){
					// タッチアップ時の処理
					touchUp(mx, my);
					// アニメーション終了後にロックを解除して、前回値にダミーを入れる
					// isTouchOK = true;
					// pastX = -1;
					// pastY = -1;
				}
				else isTouchOK = true;
			}
		}
		return false;
	}

 これでエラーが激減しました。
 
 フォァファファファ~(回想時のSE)
 
 1年前に悩んでいたことが1年前のことのように思い出されます。

 ひとまず落ち着いているので更に直すつもりはないのですが、他の方々は、タッチイベントをキューに入れたりして任意のタイミングで取り出して処理しているようですね。よくわからんけど。

 しかし、でも、まだ、やはり、時たまフリーズするのです・・原因不明。

進捗悪い上にハマる

device-2014-09-22-015212
 これ、横幅を 540 ピクセルに設定し AndEngine の RatioResolutionPolicy で比率そのままに最大限の表示を試みているのですが、ZoomCamera を使っているものだから、最小と最大の倍率を定めるところでつまずいた。
 
 横幅 480 ピクセルの実機でテストしていたところ、最小倍率の画面がどうにもフィットしないのです。
device-2014-09-22-020905
 左にずれて微妙に大きい・・。
 
 ウィスキーロックを飲んで頭を冷やし、ズームの倍率は「実機の画面上での値」となることに思いついた。
 実機の横幅/Cameraの横幅(我テスト機の場合480/540)の値をズーム倍率に掛けて設定しなくてはならないのですね。ふ~っ。
 普通に設定の違いのようでした。
 
 もう、こんな時間(2:20)。