何日か前に、はてなでハイブリッドイメージが話題になっていたみたいです。
僕も数年前にJavaで作った記憶があり、懐かしさも覚えつつも今回はFlashで。
例によってマリリンシュタイン。
>> Demo (ハイパス/ローパスフィルタの半径をスライダーで変更可能)
今回のデモではアインシュタインの方にローパスフィルタを、
マリリンモンローの方にハイパスフィルタをかけています。
近視の人だとマリリンモンローはほとんど見えないはずです。
ただ、はてブの例ほど良い画像を組み合わせていないので、
近くで見てもアインシュタインが普通に浮かんで見えてしまいますが;;
2年前にASでFFTの処理は一度書いていたので、あまりすることがなかったです。
入力画像にハイパス/ローパスフィルタをかけてから足すだけ。
元論文ではガウシアンフィルタを併用しているようなのでそれもついでにかけておきます。
(FFTについては2年前のものを改修して高速化)
wonderfl にも投稿しようかと思ったんですが、既にやっておられる方がいました。
(合成方法が論文とは違う独自の方法? みたいですが、ちょっとわかりません;)
遠くから見ると別画像(Hybrid Image) – wonderfl build flash online
それにしても人間の視覚っておもしろいですねー
・関連記事
論文: OlivaTorralb_Hybrid_Siggraph06.pdf
画像信号処理 – Flex
・HybridImage.mxml (メインビュー)
[xml]
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns:local="*">
<s:layout>
<s:VerticalLayout verticalAlign="middle" horizontalAlign="center" />
</s:layout>
<fx:Declarations>
<local:ViewHelper id="viewHelper" />
</fx:Declarations>
<s:Panel title="Hybrid Image" width="700" height="670">
<s:VGroup horizontalAlign="center" horizontalCenter="0">
<s:HGroup paddingBottom="20">
<s:VGroup horizontalAlign="left">
<s:BitmapImage id="InputImg1" width="256" height="256" />
<s:HSlider id="LowRadius" maximum="128" value="12" snapInterval="2" />
</s:VGroup>
<s:VGroup horizontalAlign="right">
<s:BitmapImage id="InputImg2" width="256" height="256" />
<s:HSlider id="HighRadius" maximum="128" value="16" snapInterval="2" />
</s:VGroup>
</s:HGroup>
<s:Label text="Filter Radius" paddingTop="-32" />
<s:BitmapImage id="HybridImg" width="256" height="256" />
<s:Button id="Button" label="Generate Hybrid Image" />
<s:Label id="Label" />
</s:VGroup>
</s:Panel>
</s:Application>
[/xml]
・ViewHelper.as (ヘルパークラス)
※ debert さんのBulkLoaderクラスを使用させていただいています。感謝!
[as]
package {
import br.com.stimuli.loading.BulkLoader;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.events.Event;
import flash.filters.ConvolutionFilter;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.utils.getTimer;
import mx.core.IMXMLObject;
import mx.events.FlexEvent;
public class ViewHelper implements IMXMLObject {
private var view:HybridImage;
private var generator:Generator;
private var hybridBmp:Bitmap;
private var loader:BulkLoader;
private var mat:Array;
private var gaussianFilter:ConvolutionFilter;
private var pt:Point;
private var rect:Rectangle;
public function initialized(document:Object, id:String):void {
view = document as HybridImage;
view.addEventListener(FlexEvent.CREATION_COMPLETE,
onCreationComplete, false, 0, true);
}
private function onCreationComplete(e:FlexEvent):void {
try {
loader = new BulkLoader(‘hybrid_image’);
loader.add(‘./assets/Einstein.png’, {id:’Einstein’});
loader.add(‘./assets/Monroe.png’, {id:’Monroe’});
loader.start();
loader.addEventListener(BulkLoader.COMPLETE,
onLoadComplete, false, 0, true);
loader.addEventListener(BulkLoader.ERROR,
function(e:Event):void {
view.Label.text = ‘cannot load image: ‘ + e;
}, false, 0, true);
}catch(e:Error) {
view.Label.text = e.message;
}
}
private function onLoadComplete(e:Event):void {
try {
var inputBmp1:Bitmap = loader.getBitmap(‘Einstein’); // apply lowpath filter
var inputBmp2:Bitmap = loader.getBitmap(‘Monroe’); // apply highpath filter
hybridBmp = new Bitmap();
hybridBmp.bitmapData = new BitmapData(inputBmp1.width, inputBmp1.height, false);
pt = new Point(0, 0);
rect = new Rectangle(0, 0, inputBmp1.width, inputBmp1.height);
mat = [0.1096, 0.1118, 0.1096, // gaussian kernel
0.1118, 0.1141, 0.1118,
0.1096, 0.1118, 0.1096];
gaussianFilter = new ConvolutionFilter(3, 3, mat, 1, 20);
generator = new Generator(inputBmp1.bitmapData, inputBmp2.bitmapData);
view.InputImg1.source = inputBmp1;
view.InputImg2.source = inputBmp2;
view.Button.addEventListener(FlexEvent.BUTTON_DOWN, compute, false, 0, true);
view.LowRadius.addEventListener(FlexEvent.CHANGE_END, compute, false, 0, true);
view.HighRadius.addEventListener(FlexEvent.CHANGE_END, compute, false, 0, true);
}catch(e:Error) {
trace(e);
view.Label.text = e.message;
}
}
private function compute(e:Event):void {
CursorManager.setBusyCursor();
var lowpathBmp:Bitmap = new Bitmap(new BitmapData(hybridBmp.width, hybridBmp.height, false));
var highpathBmp:Bitmap = new Bitmap(lowpathBmp.bitmapData.clone());
var dstData:Vector.
var ts:Number = getTimer();
dstData = generator.run(view.LowRadius.value, view.HighRadius.value);
lowpathBmp.bitmapData.setVector(rect, dstData[0]);
lowpathBmp.bitmapData.applyFilter(lowpathBmp.bitmapData, rect, pt, gaussianFilter);
highpathBmp.bitmapData.setVector(rect, dstData[1]);
highpathBmp.bitmapData.applyFilter(highpathBmp.bitmapData, rect, pt, gaussianFilter);
hybridBmp.bitmapData.draw(lowpathBmp.bitmapData);
hybridBmp.bitmapData.draw(highpathBmp.bitmapData, null, null, BlendMode.ADD);
view.HybridImg.source = hybridBmp;
view.Label.text = ‘generate time: ‘ + (getTimer() – ts)/1000 + ‘ sec’;
}
}
}
[/as]
・Generator.as (ハイブリッドイメージ合成用)
[as]
package {
import com.rt.utils.FFT;
import flash.display.BitmapData;
/**
* Generate Hybrid Image
*/
public class Generator {
private var lowSpatial:Vector.
private var highSpatial:Vector.
private var dstLow:Vector.
private var dstHigh:Vector.
private var fft:FFT;
public function Generator(low:BitmapData, high:BitmapData) {
validate(low, high);
var w:int = low.width, h:int = low.height;
var base:Vector.
lowSpatial = new
highSpatial = new
dstLow = new
dstHigh = new
fft = new FFT(w);
serialize(low, high);
}
public function run(lowpathRadius:uint, highpathRadius:uint):Vector.
// [index 0]: real part, [index 1]: imaginary part.
dstLow[0] = lowSpatial[0].concat();
dstLow[1] = lowSpatial[1].concat();
dstHigh[0] = highSpatial[0].concat();
dstHigh[1] = highSpatial[1].concat();
fft.applyFilter(dstLow[0], dstLow[1], lowpathRadius, FFT.LPF);
fft.applyFilter(dstHigh[0], dstHigh[1], highpathRadius, FFT.HPF);
fft.fft2d(dstLow[0], dstLow[1], true);
fft.fft2d(dstHigh[0], dstHigh[1], true);
var len:int = dstLow[0].length;
var dstData1:Vector.
var dstData2:Vector.
var bias:int = 30;
for(var i:int=0; i
if(value2 < 0) value2 = 0;
if(value2 > 0xff) value2 = 0xff;
dstData1[i] = value1 << 16 | value1 << 8 | value1;
dstData2[i] = value2 << 16 | value2 << 8 | value2;
}
return new
}
private function serialize(low:BitmapData, high:BitmapData):void {
var imageData:Vector.
new
high.getVector(high.rect)];
var len:int = imageData[0].length;
for(var i:int=0; i
private var cstb:Vector.
public static const HPF:String = “high”;
public static const LPF:String = “low”;
public static const BPF:String = “band”
public function FFT(n:int) {
if(n != 0 && (n & (n-1)) == 0) {
this.n = n;
this.cstb = new Vector.
this.bitrev = new Vector.
makeCstb();
makeBitrev();
}
}
// 1D-FFT
public function fft(re:Vector.
if(!inv) {
fftCore(re, im, 1);
}else {
fftCore(re, im, -1);
for(var i:int=0; i
var tre:Vector.
var tim:Vector.
var i:uint;
if(inv) swapQuadrant(re, im);
// x-axis
for(var y:int=0; y
var len:int = data.length;
var pi:Number = Math.PI;
for(var i:int=0; i
var r:int = 0; // radius
var n2:int = n>>1;
var i:int, ptr:int;
for(var y:int=-n2; y
(type == BPF && (r < rad || r > (rad + bandWidth)))) {
re[ptr] = im[ptr] = 0;
}
}
}
}
// Fast Fourier Transform core operation.
private function fftCore(re:Vector.
var h:int, d:int, wr:Number, wi:Number, ik:int, xr:Number, xi:Number, m:int, tmp:Number;
// bit reversal
for(var l:int=0; l
wi = sign*cstb[h];
for(var i:int=j; i
var tmp:Number, xn:int, yn:int, i:int, j:int, k:int, l:int;
var len:int = n>>1;
for(var y:int=0; y
var t:Number = Math.sin(Math.PI/n);
var dc:Number = 2*t*t;
var ds:Number = Math.sqrt(dc*(2 – dc));
var c:Number = cstb[n4] = 1;
var s:Number = cstb[0] = 0;
t = 2*dc;
for(var i:int=1; i
while(k<=j) {
j -= k;
k >>= 1;
}
j += k;
bitrev[i] = j;
}
}
}
}
[/as]