l'essentiel est invisible pour les yeux

Sunday, February 17, 2008

as3filters: 画像処理用フィルターライブラリの公開とPhoto BoothをAS3で作った

第二回目のActionScript記事だ。前回は、AS3の練習にとas3zerobugを作ったが、ActionScript -> Firebugプロキシは既出だったようだ。ま、誰でも考えるアイデアだ。

as3filters



あなたのPCにカメラがついているならば、そいつは、いつもあなたを撮影する機会をうかがっている。Skypeのビデオチャットにしか使わないなんてもったいない。彼らをもっと面白く使ってやろう。リアルな世界がデジタルな世界に瞬時に取り込まれる面白さは、取り込んだリアルな情報を加工した時に現れると思う。例えば、あなたの顔をリアルタイムでとても美人に映し出すなんてどうだろうか?複合現実が当たり前になり、現実と仮想の区別がつかなくなった数世紀後には、人の顔なんて今の原型をなしていないかもしれない。

ブサイクだろうが、相手に見えている顔は現実の世界にオーバーレイされた仮想のあなたの顔だからだ。

さて、今回は画像処理用のフィルターを集めたライブラリを作ってみた。
残念ながら、(今の所)貴方の顔をかっこよく映し出したり、かわいく見せたりするフィルタではない。顔を回転させたり、つぶしたり、部分的に拡大したりと、Photoshopのフィルタ群でよくみられるフィルタだ。ま、ほとんとの場合ブサイクになるだろう。

CPUは消費するが、カメラで取り込んだ映像にリアルタイムでフィルターをかける事は、AS3では十分に可能だ。ハードウェアの進歩に感謝する。ブサイクフィルタだけでなくて、もっとおもしろいアイデアを持っている人を募集している。是非、as3filtersプロジェクトに参加してほしい。

Mac OSXユーザの皆様は、Photo Boothというアプリケーションを知っていると思う。このアプリケーションは、カメラで撮影と撮影した画像に面白いエフェクトをかけることのできる、シンプルなアプリケーションである。このアプリケーションをFlashで作った。

as3filters: Photo Booth Demo


このデモを実行するには、カメラが必要なのでカメラを持っていないユーザは、下記のプロジェクトページからスナップショットが見てください。さすがに、8つのフィルタを同時に処理するとCPU負荷が高いが、一つなら問題ない。

as3filters



as3filtersは、これらのエフェクトをActionScriptで簡単に当てる事のできるフィルターライブラリだ。

インストール方法
チェックアウトして、コンパイル時にリンクする。

% svn checkout http://as3filters.googlecode.com/svn/trunk/ as3filters
% cp as3filters/bin/as3filters.swc /path/to/your-project
% mxmlc -compiler.include-libraries as3filters.swc


Flex-configファイルを使用してもOK。

<?xml version="1.0"?>
<flex-config>
<compiler>
<library-path append="true">
<path-element>../bin/as3filters.swc</path-element>
</library-path>
</compiler>
</flex-config>



使い方
Filterクラスのスタティックメソッドで、フィルタを作成して、BitmapData#applyFilterメソッドでフィルタを適用する。第一引数は、フィルタ適用対象のBitmapDataオブジェクトを渡す。regionはフィルタを適用する領域となる。


var bmd:BitmapData = new BitmapData(width, height, true);
var region:Rectangle = new Rectangle(30, 30, 90, 90)
var twirlFilter:DisplacementMapFilter = Filter.twirlFilter(bmd, region);
bmd.draw(video);
bmd.applyFilter(bmd, bmd.rect, new Point(0, 0), twirlFilter);



詳しくは、以下のPhoto Booth Demoアプリケーションのソースをご覧あれ。

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
color="#000"
themeColor="#FFFFFF"
styleName="plain"
creationComplete="init();">

<mx:Script> <![CDATA[
import flash.display.*;
import flash.filters.*;
import flash.media.Camera;
import as3.Filter;
import mx.controls.*;
import mx.core.*;

private var camera:Camera;
private var fps:int = 18;
private const VIDEO_WIDTH:int = 150,
VIDEO_HEIGHT:int = 150;

private function setupVideo():void
{
bulge_video.width = dent_video.width = twirl_video.width = VIDEO_WIDTH;
squeeze_video.width = video.width = mirror_video.width = VIDEO_WIDTH;
tunnel_video.width = fisheye_video.width = strech_video.width = VIDEO_WIDTH;
bulge_video.height = dent_video.height = twirl_video.height = VIDEO_HEIGHT;
squeeze_video.height = video.height = mirror_video.height = VIDEO_HEIGHT;
tunnel_video.height = fisheye_video.height = strech_video.height = VIDEO_HEIGHT;
}

public function init():void
{
// setup camera
if(connectCamera()) {
// Setup video
setupVideo();
video.attachCamera(camera);

setInterval((function():Function {
// setup filters
var width:int = video.width;
var height:int = video.height;
var bmd:BitmapData = new BitmapData(width, height, true);
var region:Rectangle = new Rectangle(30, 30, 90, 90);
var bulgeFilter:DisplacementMapFilter = Filter.bulgeFilter(bmd, region);
var twirlFilter:DisplacementMapFilter = Filter.twirlFilter(bmd, region);
var squeezeFilter:DisplacementMapFilter = Filter.squeezeFilter(bmd);
var pinchFilter:DisplacementMapFilter = Filter.pinchFilter(bmd, region);
var tunnelFilter:DisplacementMapFilter = Filter.photicTunnelFilter(bmd, region);
var fisheyeFilter:DisplacementMapFilter = Filter.fisheyeFilter(bmd);
var strechFilter:DisplacementMapFilter = Filter.strechFilter(bmd);

// TODO: Dent filter is not supported yet.
var dentFilter:DisplacementMapFilter = Filter.squeezeFilter(bmd, region, 0.3);

return function():void {
// original
bmd.draw(video);

// bulge effect
var bulgeBmd:BitmapData = new BitmapData(width, height, false);
bulgeBmd.applyFilter(bmd, bmd.rect, new Point(0, 0), bulgeFilter);
bulge_video.addChild(new Bitmap(bulgeBmd));

// twirl effect
var twirlBmd:BitmapData = new BitmapData(width, height, false);
twirlBmd.applyFilter(bmd, bmd.rect, new Point(0, 0), twirlFilter);
twirl_video.addChild(new Bitmap(twirlBmd));

// squeeze effect
var squeezeBmd:BitmapData = new BitmapData(width, height, false);
squeezeBmd.applyFilter(bmd, bmd.rect, new Point(0, 0), squeezeFilter);
squeeze_video.addChild(new Bitmap(squeezeBmd));

// pinch effect
var tunnelBmd:BitmapData = new BitmapData(width, height, false);
tunnelBmd.applyFilter(bmd, bmd.rect, new Point(0, 0), tunnelFilter);
tunnel_video.addChild(new Bitmap(tunnelBmd));

// mirror effect
// NOTE: A little tricky because return BitmapData object directly.
mirror_video.addChild(new Bitmap(Filter.mirror(bmd)));

// fisheye effect
var fisheyeBmd:BitmapData = new BitmapData(width, height, false);
fisheyeBmd.applyFilter(bmd, bmd.rect, new Point(0, 0), fisheyeFilter);
fisheye_video.addChild(new Bitmap(fisheyeBmd));

// strech effect
var strechBmd:BitmapData = new BitmapData(width, height, false);
strechBmd.applyFilter(bmd, bmd.rect, new Point(0, 0), strechFilter);
strech_video.addChild(new Bitmap(strechBmd));

// dent effect
// TODO: Dent effect is not supported yet
var dentBmd:BitmapData = new BitmapData(width, height, false);
dentBmd.applyFilter(bmd, bmd.rect, new Point(0, 0), dentFilter);
dent_video.addChild(new Bitmap(dentBmd));
};
})(), 1000 / fps);
}
}
// {{{
private function rotate(source:BitmapData):BitmapData
{
var width:int = source.width;
var height:int = source.height;
var matrix:Matrix = new Matrix();
var rotation:Number = Math.PI / 2;
var dx:int = Math.floor(width / 2);
var dy:int = Math.floor(height / 2);
matrix.rotate(rotation);
matrix.translate(width, 0);
var bmd:BitmapData = new BitmapData(width, height, false);
bmd.draw(source, matrix);

return bmd;
}
// }}}

private function connectCamera():Boolean
{
camera = Camera.getCamera();
if(camera) {
camera.setMode(VIDEO_WIDTH, VIDEO_HEIGHT, fps);
return true;
} else {
return false;
}
}
]]></mx:Script>
<mx:VBox>
<mx:Label text="AS3 Photo Booth." fontSize="16"/>
<mx:HBox>
<mx:VideoDisplay id="bulge_video"/>
<mx:VideoDisplay id="dent_video"/>
<mx:VideoDisplay id="twirl_video"/>
</mx:HBox>
<mx:HBox>
<mx:VideoDisplay id="squeeze_video"/>
<mx:VideoDisplay id="video"/>
<mx:VideoDisplay id="mirror_video"/>
</mx:HBox>
<mx:HBox>
<mx:VideoDisplay id="tunnel_video"/>
<mx:VideoDisplay id="fisheye_video"/>
<mx:VideoDisplay id="strech_video"/>
</mx:HBox>
</mx:VBox>
</mx:Application>



Filter.asのソース

package as3 {
import flash.display.*;
import flash.geom.*;
import flash.filters.*;

/**
* as.Filter
*
* as3.Filter class includes methods generate variety of filters for image processing.
*/
public class Filter
{
/**
* Generate the DisplacementMapFilter for twirling effect.
*
* You can apply the filter is as follows:
*
* var bmd:BitmapData = new BitmapData(width, height, false);
* var filter:DisplacementMapFilter = Filter.twirlFilter(bmd);
* bmd.draw(video);
* bmd.applyFilter(bmd, bmd.rect, new Point(0, 0), filter);
*
* You can also apply the filter specified region:
*
* var region:Rectangle = new Rectangle(100, 100, 100, 100);
* var filter:DisplacementMapFilter = Filter.twirlFilter(bmd, rect);
*
* @params source BitmapData The input bitmap data to apply the twirling effect.
* @params region Rectangle The region to apply the twirling effect.
* @params rotation Number The max amount to rotate, in radius.
* Default is Math.PI / 2.
*
* @return DisplacementMapFilter Filter to apply twirl effect
*/
static public function twirlFilter(source:BitmapData, region:Rectangle=null,
rotation:Number=0):DisplacementMapFilter
{
var width:int = source.width;
var height:int = source.height;
region ||= new Rectangle(0, 0, width, height);
rotation ||= Math.PI / 2;
var dbmd:BitmapData = new BitmapData(width, height, false, 0x8080);
var radius:Number = Math.min(region.width, region.height) / 2;
var centerX:int = region.x + region.width / 2;
var centerY:int = region.y + region.height / 2;
for(var y:int=0;y<height;++y) {
var ycoord:int = y - centerY;
for(var x:int=0;x<width;++x) {
var xcoord:int = x - centerX;
var dr:Number = radius - Math.sqrt(xcoord * xcoord + ycoord * ycoord);
if(dr > 0) {
var angle:Number = dr / radius * rotation;
var dx:Number = xcoord * Math.cos(angle) - ycoord * Math.sin(angle) - xcoord;
var dy:Number = xcoord * Math.sin(angle) + ycoord * Math.cos(angle) - ycoord;
var blue:int = 0x80 + Math.round(dx / width * 0xff);
var green:int = 0x80 + Math.round(dy / height * 0xff);
dbmd.setPixel(x, y, green << 8 | blue);
}
}
}
return new DisplacementMapFilter(dbmd,
new Point(0, 0),
BitmapDataChannel.BLUE,
BitmapDataChannel.GREEN,
width,
height,
DisplacementMapFilterMode.IGNORE);
}

/**
* Generate the BitmapData which applied mirror effect.
*
* You can create the mirrored BitmapData is as follows:
*
* var bmd = new BitmapData(video.width, video.height, false);
* bmd.draw(video);
* var mirroredBmd = Effect.mirror(bmd);
*
* @params source BitmapData The input bitmap data to apply the mirror effect.
* @params region Rectangle The region to apply the twirling effect. Default is entire region.
* @return BitmapData BitmapData which applied the mirror effect
*/
static public function mirror(source:BitmapData):BitmapData
{
var bmd:BitmapData = new BitmapData(source.width, source.height, false);
var halfWidth:int = Math.round(source.width / 2);
source.lock();
bmd.copyPixels(source, new Rectangle(0, 0, halfWidth, source.height), new Point(0,0));
for(var i:int=0;i<source.height;++i) {
for(var j:int=0;j<halfWidth;++j) {
bmd.setPixel32(halfWidth + j, i, source.getPixel32(halfWidth - j, i));
}
}
source.unlock();
return bmd;
}

/**
* Generate the DisplacementMapFilterMode for pinch effect.
*
* You can apply the filter is as follows:
*
* var bmd:BitmapData = new BitmapData(width, height, false);
* var filter:DisplacementMapFilter = Effect.pinchFilter(bmd);
* bmd.draw(video);
* bmd.applyFilter(bmd, bmd.rect, new Point(0, 0), filter);
*
* You can also apply the filter specified region:
*
* var region:Rectangle = new Rectangle(100, 100, 100, 100);
* var amount:Number = 0.5;
* var filter:DisplacementMapFilter = Effect.pinchFilter(bmd, rect, amount);
*
* @params source BitmapData The input bitmap data to apply the twirling effect.
* @params region Rectangle The region to apply the twirling effect.
* @params amount Number Amount of pinch. (-1 <= x <= 1)
* Default is 0.35.
*/
static public function pinchFilter(source:BitmapData, region:Rectangle=null,
amount:Number=0.35):DisplacementMapFilter
{
var width:int = source.width;
var height:int = source.height;
region ||= new Rectangle(0, 0, width, height);
var radius:Number = Math.min(region.width, region.height) / 2;
var centerX:int = region.x + region.width / 2;
var centerY:int = region.y + region.height / 2;
var dbmd:BitmapData = new BitmapData(width, height, false, 0x8080);
for(var y:int=0;y<height;++y) {
var ycoord:int = y - centerY;
for(var x:int=0;x<width;++x) {
var xcoord:int = x - centerX;
var d:Number = Math.sqrt(xcoord * xcoord + ycoord * ycoord);
if(d < radius) {
var t:Number = d == 0 ? 0 : Math.pow(Math.sin(Math.PI / 2 * d / radius), -amount);
var dx:Number = xcoord * (t - 1) / width;
var dy:Number = ycoord * (t - 1) / height;
var blue:int = 0x80 + dx * 0xff;
var green:int = 0x80 + dy * 0xff;
dbmd.setPixel(x, y, green << 8 | blue);
}
}
}
return new DisplacementMapFilter(dbmd,
new Point(0, 0),
BitmapDataChannel.BLUE,
BitmapDataChannel.GREEN,
width,
height,
DisplacementMapFilterMode.CLAMP);
}

/**
* Generate the DisplacementMapFilter for photic tunnel effect.
* Photic tunnel effect is as same as effect of Photo Booth application in Mac OS.
*
* You can apply the filter is as follows:
*
* var bmd:BitmapData = new BitmapData(width, height, false);
* var filter:DisplacementMapFilter = Effect.pinchFilter(bmd);
* bmd.draw(video);
* bmd.applyFilter(bmd, bmd.rect, new Point(0, 0), filter);
*
* @params source BitmapData The input bitmap data to apply the twirling effect.
* @params region Rectangle The region to apply the twirling effect.
* @return DisplacementMapFilter Filter to apply photic tunnel effect.
*/
static public function photicTunnelFilter(source:BitmapData, region:Rectangle=null):DisplacementMapFilter
{
var width:int = source.width;
var height:int = source.height;
region ||= new Rectangle(0, 0, width, height);
var centerX:int = region.x + region.width / 2;
var centerY:int = region.y + region.height / 2;
var dbmd:BitmapData = new BitmapData(width, height, false, 0x8080);
var radius:Number = Math.min(region.width, region.height) / 2;
for(var y:int=0;y<height;++y) {
var ycoord:int = y - centerY;
for(var x:int=0;x<width;++x) {
var xcoord:int = x - centerX;
var d:Number = Math.sqrt(xcoord * xcoord + ycoord * ycoord);
if(radius < d) {
var angle:Number = Math.atan2(Math.abs(ycoord), Math.abs(xcoord));
var dx:Number = (xcoord > 0? -1 : 1) * (d - radius) * Math.cos(angle) / width;
var dy:Number = (ycoord > 0? -1 : 1) * (d - radius) * Math.sin(angle) / height;
var blue:int = 0x80 + dx * 0xff;
var green:int = 0x80 + dy * 0xff;
dbmd.setPixel(x, y, green << 8 | blue);
}
}
}
return new DisplacementMapFilter(dbmd,
new Point(0, 0),
BitmapDataChannel.BLUE,
BitmapDataChannel.GREEN,
width,
height,
DisplacementMapFilterMode.CLAMP);
}

/**
* Generate the DisplacementMapFilter for bulge effect.
* Bulge effect is wrapper of pinchFilter method.
*
* @params source BitmapData The input bitmap data to apply the effect.
* @params region Rectangle The region to apply the bulge effect.
* @params amount Number Amount of bulge. (0 <= x <= 1)
* @return DisplacementMapFilter The filter to apply bulge effect.
*/
static public function bulgeFilter(source:BitmapData, region:Rectangle=null,
amount:Number=0.5):DisplacementMapFilter
{
// wrapper method of pinchFilter
return pinchFilter(source, region, Math.min(-amount, -1));
}
/**
* Generate the DisplacementMapFilter for squeeze effect.
* Dent effect is wrapper of pinchFilter method.
*
* @params source BitmapData The input bitmap data to apply the effect.
* @params region Rectangle The region to apply the bulge effect
* @params amount Number Amount of squeeze. (0 <= x <= 1)
*/
static public function squeezeFilter(source:BitmapData, region:Rectangle=null,
amount:Number=0.5):DisplacementMapFilter
{
// wrapper method of bulge filter
return pinchFilter(source, region, amount);
}

/**
* Generate the DisplacementMapFilter for fisheye effect.
*
* @params source BitmapData The input bitmap data to apply the twirling effect.
* @params amount Number Amount of fisheye (0 <= x <= 1)
* @return DisplacementMapFilter The filter to apply the fisheye effect.
*/
static public function fisheyeFilter(source:BitmapData, amount:Number=0.8):DisplacementMapFilter
{
var width:int = source.width;
var height:int = source.height;
var dbmd:BitmapData = new BitmapData(width, height, false, 0x8080);
var centerX:int = width / 2;
var centerY:int = height / 2;
var radius:Number = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
for(var y:int=0;y<height;++y) {
var ycoord:int = y - centerY;
for(var x:int=0;x<width;++x) {
var xcoord:int = x - centerX;
var d:Number = Math.sqrt(xcoord * xcoord + ycoord * ycoord);
if(d < radius) {
var t:Number = d == 0 ? 0 : Math.pow(Math.sin(Math.PI / 2 * d / radius), amount);
var dx:Number = xcoord * (t - 1) / width;
var dy:Number = ycoord * (t - 1) / height;
var blue:int = 0x80 + dx * 0xff;
var green:int = 0x80 + dy * 0xff;
dbmd.setPixel(x, y, green << 8 | blue);
}
}
}
return new DisplacementMapFilter(dbmd,
new Point(0, 0),
BitmapDataChannel.BLUE,
BitmapDataChannel.GREEN,
width,
height,
DisplacementMapFilterMode.CLAMP);
}

/**
* Generate the DisplacementMapFilter for strech effect.
*
* @params source BitmapData The input bitmap data to apply the twirling effect.
* @params amount Number Amount of strech (0 <= x <= 1), default is 0.6;
* @return DisplacementMapFilter The filter to apply the strech effect.
*/
static public function strechFilter(source:BitmapData, amount:Number=0.6):DisplacementMapFilter
{
var width:int = source.width;
var height:int = source.height;
var dbmd:BitmapData = new BitmapData(width, height, false, 0x8080);
var centerX:int = width / 2;
var centerY:int = height / 2;
var vregion:Rectangle = new Rectangle(0, 0 , width / 3, height);
var hregion:Rectangle = new Rectangle(0, 0, width, height / 3);
var blue:int;
var green:int;
for(var y:int=0;y<height;++y) {
var ycoord:int = y - centerY;
for(var x:int=0;x<width;++x) {
var xcoord:int = x - centerX;
var dx:int = (Math.abs(xcoord) < vregion.width)?
xcoord * (Math.pow(Math.abs(xcoord) / vregion.width, amount) - 1) : 0x0;
var dy:int = (Math.abs(ycoord) < hregion.height)?
ycoord * (Math.pow(Math.abs(ycoord) / hregion.height, amount) - 1) : 0x0;
blue = 0x80 + 0xff * dx / width;
green = 0x80 + 0xff * dy / height;
dbmd.setPixel(x, y, green << 8 | blue);
}
}
return new DisplacementMapFilter(dbmd,
new Point(0, 0),
BitmapDataChannel.BLUE,
BitmapDataChannel.GREEN,
width,
height,
DisplacementMapFilterMode.CLAMP);
}
}
}



TODO
Dent Filter(Photoshopのフィルタ -> 球面でマイナス指定と同じ)が正しくないのに気づかれたかもしれません。屈折率や曲座標の式などを使えばできると思うのですが、数式モデルがまだわかっていません。as3filtersにコミッタとして参加してもらえるか、数式を教えてください。