何日か前に、はてなでハイブリッドイメージが話題になっていたみたいです。
僕も数年前にJavaで作った記憶があり、懐かしさも覚えつつも今回はFlashで。
例によってマリリンシュタイン。
>> Demo (ハイパス/ローパスフィルタの半径をスライダーで変更可能)
今回のデモではアインシュタインの方にローパスフィルタを、
マリリンモンローの方にハイパスフィルタをかけています。
近視の人だとマリリンモンローはほとんど見えないはずです。
ただ、はてブの例ほど良い画像を組み合わせていないので、
近くで見てもアインシュタインが普通に浮かんで見えてしまいますが;;
2年前にASでFFTの処理は一度書いていたので、あまりすることがなかったです。
入力画像にハイパス/ローパスフィルタをかけてから足すだけ。
元論文ではガウシアンフィルタを併用しているようなのでそれもついでにかけておきます。
(FFTについては2年前のものを改修して高速化)
wonderfl にも投稿しようかと思ったんですが、既にやっておられる方がいました。
(合成方法が論文とは違う独自の方法? みたいですが、ちょっとわかりません;)
遠くから見ると別画像(Hybrid Image) – wonderfl build flash online
それにしても人間の視覚っておもしろいですねー
・関連記事
論文: OlivaTorralb_Hybrid_Siggraph06.pdf
画像信号処理 – Flex
・HybridImage.mxml (メインビュー)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?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> |
・ViewHelper.as (ヘルパークラス)
※ debert さんのBulkLoaderクラスを使用させていただいています。感謝!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
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.<Vector.<uint>>; 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'; } } } |
・Generator.as (ハイブリッドイメージ合成用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
package { import com.rt.utils.FFT; import flash.display.BitmapData; /** * Generate Hybrid Image */ public class Generator { private var lowSpatial:Vector.<Vector.<Number>>; private var highSpatial:Vector.<Vector.<Number>>; private var dstLow:Vector.<Vector.<Number>>; private var dstHigh:Vector.<Vector.<Number>>; 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.<Number> = new Vector.<Number>(w*h, true); lowSpatial = new <Vector.<Number>>[base.concat(), base.concat()]; highSpatial = new <Vector.<Number>>[base.concat(), base.concat()]; dstLow = new <Vector.<Number>>[]; dstHigh = new <Vector.<Number>>[]; fft = new FFT(w); serialize(low, high); } public function run(lowpathRadius:uint, highpathRadius:uint):Vector.<Vector.<uint>> { // [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.<uint> = new Vector.<uint>(len, true); var dstData2:Vector.<uint> = new Vector.<uint>(len, true); var bias:int = 30; for(var i:int=0; i<len; i++) { var value1:int = int(dstLow[0][i]); var value2:int = int(dstHigh[0][i]); if(value1 < 0) value1 = 0; if(value1 > 0xff) value1 = 0xff; 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 <Vector.<uint>>[dstData1, dstData2]; } private function serialize(low:BitmapData, high:BitmapData):void { var imageData:Vector.<Vector.<uint>> = new <Vector.<uint>>[low.getVector(low.rect), high.getVector(high.rect)]; var len:int = imageData[0].length; for(var i:int=0; i<len; i++) { lowSpatial[0][i] = imageData[0][i] & 0xff; highSpatial[0][i] = imageData[1][i] & 0xff; } fft.fft2d(lowSpatial[0], lowSpatial[1]); fft.fft2d(highSpatial[0], highSpatial[1]); } private function validate(l:BitmapData, h:BitmapData):void { var check:Boolean = (l.width == h.width && l.height == h.height); if(!check) throw new TypeError('Invalid images, required same size'); } } } |
・FFT.as (高速フーリエ変換 改修版)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
package com.rt.utils { public class FFT { private var n:int; private var bitrev:Vector.<int>; private var cstb:Vector.<Number>; 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.<Number>(n + (n>>2), true); this.bitrev = new Vector.<int>(n, true); makeCstb(); makeBitrev(); } } // 1D-FFT public function fft(re:Vector.<Number>, im:Vector.<Number>, inv:Boolean=false):void { if(!inv) { fftCore(re, im, 1); }else { fftCore(re, im, -1); for(var i:int=0; i<n; i++) { re[i] /= n; im[i] /= n; } } } // 2D-FFT public function fft2d(re:Vector.<Number>, im:Vector.<Number>, inv:Boolean=false):void { var tre:Vector.<Number> = new Vector.<Number>(re.length, true); var tim:Vector.<Number> = new Vector.<Number>(im.length, true); var i:uint; if(inv) swapQuadrant(re, im); // x-axis for(var y:int=0; y<n; y++) { i = y*n; for(var x1:int=0; x1<n; x1++) { tre[x1] = re[x1 + i]; tim[x1] = im[x1 + i]; } if(!inv) fft(tre, tim); else fft(tre, tim, true); for(var x2:int=0; x2<n; x2++) { re[x2 + i] = tre[x2]; im[x2 + i] = tim[x2]; } } // y-axis for(var x:int=0; x<n; x++) { for(var y1:int=0; y1<n; y1++) { i = x + y1*n; tre[y1] = re[i]; tim[y1] = im[i]; } if(!inv) fft(tre, tim); else fft(tre, tim, true); for(var y2:int=0; y2<n; y2++) { i = x + y2*n; re[i] = tre[y2]; im[i] = tim[y2]; } } if(!inv) swapQuadrant(re, im); } // windowing function using hamming window. public function windowing(data:Vector.<Number>, inv:int):void { var len:int = data.length; var pi:Number = Math.PI; for(var i:int=0; i<len; i++) { if(inv == 1) data[i] *= 0.54 - 0.46*Math.cos(2*pi*i/(len - 1)); else data[i] /= 0.54 - 0.46*Math.cos(2*pi*i/(len -1)); } } // spatial frequency filtering. public function applyFilter(re:Vector.<Number>, im:Vector.<Number>, rad:uint, type:String, bandWidth:uint = 0):void { var r:int = 0; // radius var n2:int = n>>1; var i:int, ptr:int; for(var y:int=-n2; y<n2; y++) { i = n2 + (y + n2)*n; for(var x:int=-n2; x<n2; x++) { r = Math.sqrt(x*x + y*y); ptr = x + i; if((type == HPF && r < rad) || (type == LPF && r > rad) || (type == BPF && (r < rad || r > (rad + bandWidth)))) { re[ptr] = im[ptr] = 0; } } } } // Fast Fourier Transform core operation. private function fftCore(re:Vector.<Number>, im:Vector.<Number>, sign:int):void { 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<n; l++) { m = bitrev[l]; if(l<m) { tmp = re[l]; re[l] = re[m]; re[m] = tmp; tmp = im[l]; im[l] = im[m]; im[m] = tmp; } } // butterfly operation for(var k:int=1; k<n; k<<=1) { h = 0; d = n/(k<<1); for(var j:int=0; j<k; j++) { wr = cstb[h + (n>>2)]; wi = sign*cstb[h]; for(var i:int=j; i<n; i+=(k<<1)) { ik = i+k; xr = wr*re[ik] + wi*im[ik] xi = wr*im[ik] - wi*re[ik]; re[ik] = re[i] - xr; re[i] += xr; im[ik] = im[i] - xi; im[i] += xi; } h += d; } } } // swap quadrant. private function swapQuadrant(re:Vector.<Number>, im:Vector.<Number>):void { 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<len; y++) { yn = y + len; for(var x:int=0; x<len; x++) { xn = x + len; i = x + y*n; j = xn + yn*n; k = x + yn*n; l = xn + y*n; tmp = re[i]; re[i] = re[j]; re[j] = tmp; tmp = re[k]; re[k] = re[l]; re[l] = tmp; tmp = im[i]; im[i] = im[j]; im[j] = tmp; tmp = im[k]; im[k] = im[l]; im[l] = tmp; } } } // make table of trigonometric function. private function makeCstb():void { var n2:int = n>>1, n4:int = n>>2, n8:int = n>>3; 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<n8; i++) { c -= dc; dc += t*c; s += ds; ds -= t*s; cstb[i] = s; cstb[n4 - i] = c; } if(n8 != 0) cstb[n8] = Math.sqrt(0.5); for(var j:int=0; j<n4; j++) cstb[n2 - j] = cstb[j]; for(var k:int=0; k<(n2 + n4); k++) cstb[k + n2] = -cstb[k]; } // make table of bit reversal. private function makeBitrev():void { var i:int = 0, j:int = 0, k:int = 0; bitrev[0] = 0; while(++i<n) { k = n >> 1; while(k<=j) { j -= k; k >>= 1; } j += k; bitrev[i] = j; } } } } |