« 2008年10月 | トップページ | 2009年2月 »

2008年11月13日 (木)

PixelBenderで輪郭抽出&輪郭に雪を積もらせる

最近カメラで取得した映像を使って遊んでます。
映像の中の人とかに雪が積もるといいなーと思ってやってみました。

境界線が分かればなんとかなるので、↓こちらを参考にやってみました。
http://web.sfc.keio.ac.jp/~shokai/archives/2007/05/proce55ing-webcam-edge-detect.html

●まずはActionScriptで

元ソースはProcessingなので、ASに書き直す必要があります。
そのままいけないので試行錯誤した結果が下記のような感じです。
(※省略してます)

考え方としては、まず画像全体の色を4段階にして処理をしやすくし、それから境界線を調べていくようです。

//RGBの各チャンネルを3つのBitmapDataのBLUEチャンネルにコピー
bd1Red.copyChannel(bd1,bd1.rect,pt,BitmapDataChannnel.RED,BitmapDataChannnel.BLUE);
bd1Blue.copyChannel(bd1,bd1.rect,pt,BitmapDataChannnel.GREEN,BitmapDataChannnel.BLUE);
bd1Green.copyChannel(bd1,bd1.rect,pt,BitmapDataChannnel.BLUE,BitmapDataChannnel.BLUE);


//4段階に量子化
//各座標の色を取得し、0x00~0xFFまでの値にし、その中でさらに4段階化する。
//0x00~0xFFまでの値にしやすいように全部BLUEチャンネルにコピー
//その状態でgetPixelしたら0x00~0xFF(0x000000~0x0000FF)の範囲で取得できるかと思いきや・・・
//他のチャンネルも同じ値になるみたい。
//仕方なくgetPixelしてから0xFFFF00で割った余りを使って0x00~0xFFの範囲にする
//一度64で割ってから小数点以下を切り捨てて、また64倍することで4段階の色に分かれる
//(0~255の256段階なので4段階にするには4で割った64で割れば0~3の4段階になる)
for(var yy:int = 0; yy < 240; yy++){
	quants_pixels[yy] = [];
	for(var xx:int = 0; xx < 320; xx++){
		var red:Number = bd1Red.getPixel(xx,yy) % 0xFFFF00;
		var blue:Number = bd1Blue.getPixel(xx,yy) % 0xFFFF00;
		var green:Number = bd1Green.getPixel(xx,yy) % 0xFFFF00;
		var level:Number = Math.floor((red + blue + green)/3/64) * 64;
		quants_pixels[yy][xx] = level; //draw each pixel to the screen
	}
}

// 周囲(4ピクセル)の平均値と比較する
//各ピクセルを上下左右の平均と比べて、それより大きいか小さいかで境界線かどうかを判定するようです
for(yy=1; yy<240-1; yy++) {
	for(xx=1; xx<320-1; xx++) {
		//bd2.setPixel(xx,yy,quants_pixels[yy][xx]);
		var around:Number = (quants_pixels[yy - 1][xx] + quants_pixels[yy][xx - 1] + quants_pixels[yy][xx + 1] + quants_pixels[yy + 1][xx]) / 4;
		if (around < quants_pixels[yy][xx]) {
			bd3.setPixel(xx, yy, 0x000000);
		} else {
			bd3.setPixel(xx, yy, 0xFFFFFF);
		}
	}
}

以上のような考え方でやってます。
後半は元のやつとだいたい同じなのでいいと思うけど、前半の処理はかなり効率が悪そう。。
でもとりあえずは境界線を取得できたので、これで雪を降らそうと思ったけど、処理がかなり重いことに気付きました。。

●PixelBenderで

処理を軽くしないといけないんだけどいい方法も思いつかず、検索するのも面倒だなあと思ってたときに「PixelBender使えばいいんじゃない??」って思いついて試しにやってみました。
考え方は一緒で、色を4段階にして、その後に各ピクセルごとに境界線の判定をします。
この2つを1つのPixelBenderの処理でやりたかったけど、PixelBenderも分け分からないので2つに分けました。
でもこれで処理は全然軽くなりましたー。といってもまだ重いけど、ASだけに比べれば全然速い。
考え方は同じでやってるので、ASでやった時とPixelBenderでやった時で特に違いはなく、ただ処理が速くなりました。

これをもっとキレイにして忘年会で使おうと思います。

↓カメラ映像の境界線に雪が積もります。クリックで画面切り替わります。
雪の表現はこちらを参考にしました。


<PixelBender>


kernel FourColor
<	namespace:			"";
	vendor:				"";
	version:			1;
	description:		"";
>
{
	input image4 source;
	output pixel4 result;
	void evaluatePixel()
	{
		pixel4 color = sampleLinear( source, outCoord() );
		float b1 = (color.r + color.g + color.b) / 3.0;
		
		float r = 0.0;
		float g = 0.0;
		float b2 = floor(b1 * 100.0 / 25.0);
		float b = b2 * 25.0 / 100.0;
	
		result = pixel4(r,g,b,1.0);
				
	}
}

<PixelBender>


kernel OutLine
<	namespace:			"";
	vendor:				"";
	version:			1;
	description:		"";
>
{
	input image4 source;
	output pixel4 result;
	void evaluatePixel()
	{
		//ある座標
		float2 xy = outCoord();
		//その座標の上下左右の座標
		float2 xy1 = float2(xy.x, xy.y-1.0);
		float2 xy2 = float2(xy.x-1.0, xy.y);
		float2 xy3 = float2(xy.x+1.0, xy.y);
		float2 xy4 = float2(xy.x, xy.y+1.0);
		//それぞれの色を取得
		pixel4 cl = sampleLinear(source, xy);
		pixel4 cl1 = sampleLinear(source, xy1);
		pixel4 cl2 = sampleLinear(source, xy2);
		pixel4 cl3 = sampleLinear(source, xy3);
		pixel4 cl4 = sampleLinear(source, xy4);
		//各色のBlueチャンネルの色を取得(渡される画像は青のチャンネルしかないはず)
		pixel1 b = cl.b;
		pixel1 b1 = cl1.b;
		pixel1 b2 = cl2.b;
		pixel1 b3 = cl3.b;
		pixel1 b4 = cl4.b;
		
		pixel4 aaa;
		//上下左右の色の平均値よりも大きいか小さいかで境界線を判定
		if ((b1+b2+b3+b4)/4.0 < b) {
			aaa = pixel4(0.0,0.0,0.0,1.0);
		} else {
			aaa = pixel4(1.0,1.0,1.0,1.0);
		}
		
		result = aaa;
	}
}

<ActionScript>

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.BitmapDataChannel;
	import flash.display.Shader;
	import flash.filters.ShaderFilter;
	import flash.filters.BlurFilter;
	import flash.geom.ColorTransform;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.media.Camera;
	import flash.media.Video;
	import flash.display.BlendMode;
	import flash.utils.ByteArray;
	[SWF(width=640,height=480,backgroundColor=0xFFFFFF,frameRate=120)]
	public class Snow extends Sprite {
		//青の4段階色に変換するためのPixelBender
		[Embed(source = "test09.pbj", mimeType = "application/octet-stream")]
		private var shaderData:Class;
		//↑の境界線にするためのPixelBender
		[Embed(source = "test10.pbj", mimeType = "application/octet-stream")]
		private var shaderData2:Class;
				
		private var shader:Shader;
		private var shaderFilter:ShaderFilter;
		private var shader2:Shader;
		private var shaderFilter2:ShaderFilter;

		private var particleList:Array = [];
		
		private var my_cam:Camera;
		private var video:Video;
		private var bd1:BitmapData;
		private var bd2:BitmapData;
		private var bd3:BitmapData;
		private var bdSnow:BitmapData;
		private var bdBlack:BitmapData;
		private var bdSmall:BitmapData;
		private var bmBlack:Bitmap;
		private var pt:Point;
		private var cl:ColorTransform;
		private var mtSmall:Matrix;
		private var mtBig:Matrix;
		public function Snow() {
			addEventListener(Event.ADDED_TO_STAGE, added);
		}
		private function added(e:Event):void {
			removeEventListener(Event.ADDED_TO_STAGE, added);
			init();
		}
		private function init():void {
			//PixelBenderのやつをFilterとして使うための処理
			shader = new Shader(ByteArray(new shaderData()));
			shaderFilter = new ShaderFilter(shader);
			shader2 = new Shader(ByteArray(new shaderData2()));
			shaderFilter2 = new ShaderFilter(shader2);

			pt = new Point();
			cl = new ColorTransform();
			
			//カメラ
			my_cam = Camera.getCamera();
			my_cam.setMode(320, 240, 30, false);
			//カメラの映像の表示
			//デフォルトで全画面、ステージクリックで裏の処理過程のBitmapDataが見れるようにサイズ変更
			video = new Video();
			video.attachCamera(my_cam);
			video.width = 640;
			video.height = 480;
			video.x = 0;
			video.y = 0;
			stage.addEventListener(MouseEvent.CLICK, clickVideo);
			function clickVideo(e:MouseEvent):void {
				if (video.width > 500) {
					video.width = 320;
					video.height = 240;
					bmBlack.alpha = 0.0;
				} else {
					video.width = 640;
					video.height = 480;
					bmBlack.alpha = 0.5;
				}
			}

			//カメラの映像をBitmapData化してBlurを少しかけた状態を出力する用
			bd1 = new BitmapData(320, 240, false);
			var bm1:Bitmap = new Bitmap(bd1);
			bm1.x = 320;
			bm1.y = 0;
			addChild(bm1);
			
			//PixelBenderを使って青の4段階色に変換した状態を出力する用
			bd2 = new BitmapData(320, 240, false);
			var bm2:Bitmap = new Bitmap(bd2);
			bm2.x = 0;
			bm2.y = 240;
			addChild(bm2);

			//PixelBenderを使って境界線の状態にしたのを出力する用
			bd3 = new BitmapData(320, 240, false);
			var bm3:Bitmap = new Bitmap(bd3);
			bm3.x = 320;
			bm3.y = 240;
			addChild(bm3);

			//カメラの映像は上のレイヤーにしたいのでここでaddChild
			addChild(video);
			
			//雪用のBitmapDataを毎回黒で塗りなおすので、それ用
			bdBlack = new BitmapData(640, 480, false, 0x000000);
			//カメラ映像は少し暗めにすると雪が見やすくなるので↑を使いまわしてアルファかけて配置
			bmBlack = new Bitmap(bdBlack);
			addChild(bmBlack);
			bmBlack.blendMode = BlendMode.MULTIPLY;
			bmBlack.alpha = 0.5;
			
			//雪を降らす用のBitmapData
			bdSnow = new BitmapData(640, 480, false, 0x000000);
			var bmSnow:Bitmap = new Bitmap(bdSnow);
			addChild(bmSnow);
			//BitmapDataの背景色(黒)を消すためにBlendModeを使う。
			//ScreenでもいいかもしれないけどADDのほうが明るくなるので。
			bmSnow.blendMode = BlendMode.ADD;
			
			//雪で使うのを作っとく
			bdSmall = new BitmapData(320, 240, false, 0x000000);
			mtSmall = new Matrix(0.5, 0, 0, 0.5, 0, 0);
			mtBig = new Matrix(2, 0, 0, 2, 0, 0);
			
			addEventListener(Event.ENTER_FRAME, loop);
			
			
		}
		
		private function loop(e:Event):void {
			//カメラの映像をdraw
			bd1.draw(video);
			//境界線が複雑になり過ぎる場合はブラーかける。かけすぎると境界線が取得しにくくなる。
			//bd1.applyFilter(bd1, bd1.rect, pt, new BlurFilter(2, 2, 1));
			
			//PixelBenderを使って青の4段階色に変換
			bd2.draw(bd1);
			bd2.applyFilter(bd2, bd2.rect, pt, shaderFilter);
			
			//PixelBenderを使って境界線に変換
			bd3.draw(bd2);
			bd3.applyFilter(bd3, bd3.rect, pt, shaderFilter2);
			//ブラーをかけて境界線を太くする
			//これもPixelBenderでやっちゃうほうが速いかも
			bd3.applyFilter(bd3, bd3.rect, pt, new BlurFilter(2, 2, 1));
			
			//雪のオブジェクト(Particle)をaddし、配列particleListに入れてく
			addParticle();
			addParticle();
			addParticle();
			addParticle();
			addParticle();
			
			//雪の表示用BitmapDataを黒く塗りつぶす
			bdSnow.draw(bdBlack);
			
			//particleListに入ってる雪のオブジェクトを1つずつ座標更新、表示していく
			for (var i:int = particleList.length - 1; i >= 0; i--) {
				//雪オブジェクトを取り出す
				var tw:Particle = Particle(particleList[i]);
				//境界線のBitmapData(bd3)の中で、雪オブジェクトと同じ座標の色を取得
				if (tw.y < 4 || tw.y > 476 || bd3.getPixel(Math.round(tw.x/2), Math.round(tw.y/2)) == 0xFFFFFF) {
					//それが白だったら何もない
					tw.update(false);
				} else {
					//白じゃなかったら境界線上
					//元は白or黒だけど、ブラーかけたのでグレーの部分も境界線となるので「白以外は境界線」とする
					tw.update(true);
				}
				
				//雪オブジェクトと同じ座標のピクセルを白で塗りつぶす
				bdSnow.setPixel(tw.x, tw.y, 0xFFFFFF);
				
				//雪オブジェクトが画面から消えてたら配列から削除
				if (tw.del) {
					particleList.splice(i, 1);
				}
			}
			
			//雪のキラキラをするために1回小さくする
			bdSmall.draw(bdSnow, mtSmall);
			bdSmall.applyFilter(bdSmall, bdSmall.rect, pt, new BlurFilter(2, 2, 1));
			bdSnow.draw(bdSmall, mtBig, cl, BlendMode.SCREEN);
			
			//軽くブラーをかけてみる
			bdSnow.applyFilter(bdSnow, bdSnow.rect, pt, new BlurFilter(2, 2, 1));
		}
		
		private function addParticle():void {
			//雪オブジェクトの追加
			var tw:Particle = new Particle();
			particleList.push(tw);
			tw.x = Math.random() * 640;
			tw.y = 1;
		}
	}
}
import flash.display.Bitmap;
class Particle {
	public var x:Number;
	public var y:Number;
	public var del:Boolean = false;
	private var vy:Number;
	private var ay:Number;
	function Particle() {
		vy = 0;
		ay = 0.03;
	}
	public function update(b:Boolean):void {
		if (b) {
			//境界線上の速度
			vy = 0.01;
		} else {
			//何もないとこの速度
			vy += ay;
			if (vy > 3) {
				vy = 3;
			}
		}
		y += vy;
		if (y > 480) {
			//画面から消えたらdel=trueにすることで削除対象になる
			del = true;
		}
	}
}


| | コメント (0) | トラックバック (0)

« 2008年10月 | トップページ | 2009年2月 »