Demo: Experiments in Stereo Vision (ByteArrayキャッシュ利用)
実は何年か前に試みたことがあるのですが、たしかFlash CS3とかで作っていて、
その時の.flaファイルが行方不明、、、諦めてリトライしようかと。
今度はちょっと真面目にFlex (Flex SDK 4)で作ってみます。
ステレオグラムを寄り目とかにしながら見て立体視するみたいな、
「人間ががんばる」話ではなく、あくまで機械で立体視をシミュレーションします。
ただ、相当デリケートな処理が必要な難しい分野なので、入門レベルで留めたいと思います。
サブピクセルレベルでの推定などはFlashだと馬力不足な感じもしますので。。
今回は視差マップ(Disparity Map)を生成する所まで。
扱うステレオペア画像は定番のTsukuba stereo pairです。
使用するアルゴリズムは入門レベルということでブロックマッチングを、
一致度評価にはSAD (Sum of Absolute Differences)をコストとしています。
(ただし、エピポーラ拘束条件の下で探索を行うこととする)
このアルゴリズムは動的計画法(DP)を用いて実装されるのが普通ですが、
今回はアルゴリズムの形が分かりやすいように素朴な実装を載せます。
・StereoVision.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 |
<?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 paddingTop="10" verticalAlign="middle" horizontalAlign="center" /> </s:layout> <fx:Declarations> <local:ViewHelper id="viewHelper" /> </fx:Declarations> <s:Panel title="Experiments in Stereo Vision" width="600" height="500"> <s:VGroup horizontalAlign="center" horizontalCenter="0" paddingTop="10"> <s:HGroup> <s:BitmapImage id="LeftImage" width="256" height="192" /> <s:BitmapImage id="RightImage" width="256" height="192" /> </s:HGroup> <s:Button id="Button" label="compute disparity" /> <s:BitmapImage id="DisparityImage" width="256" height="192" /> <s:Label id="Label" paddingBottom="10" paddingLeft="10" paddingRight="10" paddingTop="10" /> </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 |
package { import br.com.stimuli.loading.BulkLoader; import com.rt.stereo.StereoMatcher; import com.rt.stereo.StereoPair; import flash.display.Bitmap; import flash.display.BitmapData; import flash.events.Event; import flash.geom.Rectangle; import flash.utils.ByteArray; import flash.utils.getTimer; import mx.core.IMXMLObject; import mx.events.FlexEvent; public class ViewHelper implements IMXMLObject { private var view:StereoVision; private var stereoMatcher:StereoMatcher; private var stereoPair:StereoPair; private var loader:BulkLoader; public function initialized(document:Object, id:String):void { view = document as StereoVision; view.addEventListener(FlexEvent.CREATION_COMPLETE, onCreationComplete, false, 0, true); } private function onCreationComplete(e:FlexEvent):void { try { loader = new BulkLoader('stereo_pair'); loader.add('./assets/left.png', {id:'left'}); loader.add('./assets/right.png', {id:'right'}); loader.start(); loader.addEventListener(BulkLoader.COMPLETE, onLoadComplete, false, 0, true); loader.addEventListener(BulkLoader.ERROR, function(e:Event):void { view.Label.text = 'load error: ' + e; }, false, 0, true); }catch(e:Error) { view.Label.text = e.message; } } private function onLoadComplete(e:Event):void { try { var leftBmp:Bitmap = loader.getBitmap('left'); var rightBmp:Bitmap = loader.getBitmap('right'); var disparityBmp:Bitmap = new Bitmap(); disparityBmp.bitmapData = new BitmapData(leftBmp.width, leftBmp.height); view.LeftImage.source = leftBmp; view.RightImage.source = rightBmp; stereoPair = new StereoPair(leftBmp, rightBmp); stereoMatcher = new StereoMatcher(stereoPair); view.Button.addEventListener(FlexEvent.BUTTON_DOWN, function(e:Event):void { view.Label.text = 'compute disparity'; var ts:Number = getTimer(); stereoMatcher.addEventListener(StereoMatcher.MATCH_COMPLETE, function(e:Event):void { var disparity:ByteArray = stereoMatcher.disparity; var rect:Rectangle = disparityBmp.bitmapData.rect; disparityBmp.bitmapData.setPixels(rect, disparity); var time:Number = (getTimer() - ts)/1000; view.Label.text = "compute time: " + time + ' sec'; view.DisparityImage.source = disparityBmp; }, false, 0, true); stereoMatcher.find(StereoMatcher.BM); }, false, 0, true); }catch(e:Error) { view.Label.text = e.message; } } } } |
・StereoMatcher.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 |
package com.rt.stereo { import flash.events.Event; import flash.events.EventDispatcher; import flash.utils.ByteArray; public class StereoMatcher extends EventDispatcher { public static const BM:int = 0; public static const SGBM:int = 1; // not used currentry public static const GC:int = 2; // not used currentry public static const MATCH_COMPLETE:String = 'match_complete'; private const STATES:Array = [StateBM]; private var _pair:StereoPair; private var _disparity:ByteArray; private var dataPair:Vector.<ByteArray>; public function StereoMatcher(pair:StereoPair) { this._pair = pair; this._disparity = new ByteArray(); this.dataPair = _pair.serialize(); } public function get pair():StereoPair { return _pair; } public function get disparity():ByteArray { return _disparity; } public function find(type:int = BM, wSize:int = 5, nDisparity:int = 10):void { var state:IState = new STATES[type](_pair.width); state.windowSize = wSize; state.numberOfDisparities = nDisparity; _disparity = new ByteArray(); computeDisparity(state); dispatchEvent(new Event(MATCH_COMPLETE)); } /** * computes the disparity for the rectified stereo pair, * without using dynamic programming. */ private function computeDisparity(state:IState):void { try { var w:int = _pair.width; var h:int = _pair.height; var step:int = _pair.widthStep; var winSize:int = state.windowSize; var threshold:int = state.textureThreshold; var minDisparity:int = state.minDisparity; var maxDisparity:int = state.numberOfDisparities - minDisparity; var maxDiff:int = 255*winSize*winSize; var localMin:int = maxDiff; var sad:int = 0, diff:int; var cur:int, ptr:int; var y:int, x:int, i:int, j:int, d:int, ly:int, lx:int; for(y=0, cur=step; y<h-1; y++, cur+=step) { for(x=0, i=cur+1; x<w; x++, i+=4) { for(d=minDisparity, ptr=i; d<maxDisparity; d++, ptr-=4) { if(ptr < cur) continue; for(ly=0; ly<winSize; ly++) { if(sad > threshold || y + ly > h) break; for(lx=0, j=ly*step; lx<winSize; lx++, j+=4) { if(x + lx > w) break; diff = dataPair[0][i + j] - dataPair[1][ptr + j]; sad += (diff ^ (diff>>31)) - (diff>>31); } } if(localMin > sad) { localMin = sad; _disparity[i - 1] = 0xff; _disparity[i] = _disparity[i + 1] = _disparity[i + 2] = d<<4; } sad = 0; } localMin = maxDiff; } } }catch(e:Error) { throw e; } } } } |
・StereoPair.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 |
package com.rt.stereo { import flash.display.Bitmap; import flash.utils.ByteArray; /* Stereo Pair Bitmap */ public class StereoPair { private var _left:Bitmap; private var _right:Bitmap; public function StereoPair(left:Bitmap, right:Bitmap) { validate(left, right); _left = left; _right = right; } internal function serialize():Vector.<ByteArray> { return new <ByteArray>[ _left.bitmapData.getPixels(_left.getRect(_left)), _right.bitmapData.getPixels(_right.getRect(_right)) ]; } internal function get width():int { return _left.width; } internal function get height():int { return _left.height; } internal function get widthStep():int { return _left.width<<2; } private function validate(l:Bitmap, r:Bitmap):void { var check:Boolean = (l.width == r.width && l.height == r.height); if(!check) throw new TypeError('Invalid images, required stereo pair'); } } } |
・IState.as (各ステレオマッチング用データクラスのインタフェース)
※ 現在は StateBM しか作っていないのであまり意味無いです
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.rt.stereo { import flash.geom.Rectangle; internal interface IState { function get maxIters():int; function get minDisparity():int; function get numberOfDisparities():int; function get occlusionCost():int; function get textureThreshold():int; function get windowSize():int; function get roi():Rectangle; } } |
・StateBM.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 |
package com.rt.stereo { import flash.geom.Rectangle; /* The structure for block matching stereo correspondence algorithm */ internal class StateBM implements IState { public static const BASIC:int = 1; public static const FISH_EYE:int = 2; // not used currentry public static const NARROW:int = 3; // not used currentry private var _minDisparity:int; private var _numberOfDisparities:int; private var _SADWindowSize:int; private var _textureThreshold:int; private var _roi:Rectangle; // not used currentry public function StateBM(imageWidth:int, preset:int = BASIC) { setParameters(imageWidth, preset); } public function get maxIters():int { return 0; // not used } public function get minDisparity():int { return _minDisparity; } public function get numberOfDisparities():int { return _numberOfDisparities; } public function get occlusionCost():int { return 0; // not used } public function get textureThreshold():int { return _textureThreshold; } public function get windowSize():int { return _SADWindowSize; } public function get roi():Rectangle { return _roi; } private function setParameters(imageWidth:int, type:int):void { switch(type) { case BASIC: case FISH_EYE: case NARROW: _minDisparity = 0; _numberOfDisparities = imageWidth/8; _SADWindowSize = 7; _textureThreshold = 1500; _roi = new Rectangle(); break; default: break; } } } } |
算出された視差マップを少し大きめに載せておきます。
良質なステレオペア画像を使っているせいもありますが、そこそこな精度が出ているかと。
(白色に近いほどカメラから近いことを表す)
この視差データを元に、三角測量による位置検出を行うことができるので、
Papervision3Dとかに算出した位置データを渡して三次元空間に再投影してやると、
ビジュアル的に分かりやすい結果を得られると思います。
・関連記事
ステレオ画像処理
One thought