8/28土曜日、東京てら子に参加しました。テーマは「エフェクト」だったので、MarilenaをフィルターやPixelBenderを使って高速化!っていうのをやりたかったのですが、挫折したので、あまり本質的じゃない部分で高速化をしてみました。
ObjectDetection.asを改造して、顔検知の大きさ、位置を前回検知を元にちょっとずつ変更しながら検地するようにしました。
多くの場合、前回検出位置とほぼ同じところに顔があることを前提にしています。
制作環境では、3~4FPSだったものが、10~12FPSでるようになりました。
ただ、これは簡易的な検知なので、同時にひとつしか検知できません。「見つかりそうな場所やサイズを予想して、見つかったら早々と検索を終了する」というのがこの手法の要だからです。
ただし、普通のフル検索と、今回の手法を組み合わせることによって、平均的な速度をあげる、という手法は可能かもしれません。
例えば、10フレームに一度だけフルに検索をして、あとの9回はそのヒットした位置、数に基づき、今回の簡易的な検索を行うという方法です。
そもそもがそれほど厳密に検知精度が求められているわけではないので、多少の精度低下は許容される場面も多そうです。
いずれにしても、顔の検知は今後、いろいろな場面で使われると思います。精度を上げる方法、速度を上げる方法、いろいろ試してみたいと思っています。
▼ActionScript AS3(FP10)
ObjectDetection.as
[sourcecode language=”as3″]
//
// Project Marilena
// Object Detection in Actionscript3
// based on OpenCV (Open Computer Vision Library) Object Detection
//
// Copyright (C) 2008, Masakazu OHTSUKA (mash), all rights reserved.
// contact o.masakazu(at)gmail.com
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
//   * Redistribution’s of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//
//   * Redistribution’s in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
// This software is provided by the copyright holders and contributors "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the Intel Corporation or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
//
package jp.maaash.ObjectDetection
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.display.Bitmap;
	import flash.geom.Rectangle;
	import flash.utils.setTimeout;
	public class ObjectDetector extends EventDispatcher{
		private var debug     :Boolean = false;
		private var tgt       :TargetImage;
		public  var detected  :Array;	// of Rectangles
		public  var cascade   :HaarCascade;
		private var _options  :ObjectDetectorOptions;
		private var xmlloader :HaarCascadeLoader;
		private var waiting   :Boolean = false;
		private var loaded    :Boolean = false;
		public function ObjectDetector() {
			tgt = new TargetImage;
		}
		public function detect( bmp :Bitmap = null ) :void {
			//logger("[detect]");
			if ( bmp && bmp.bitmapData ) {
				tgt.bitmapData = bmp.bitmapData;
			}
//return;
			if ( !loaded ) {
				waiting = true;
				return;
			}
			dispatchEvent( new ObjectDetectorEvent(ObjectDetectorEvent.DETECTION_START) );
			//trace(cascade)
			_detect();
		}
		private var _factor:Array = [];
		private var _check:Array = [];
		private var _detected:Array = [];
		private function _detect() :void {
			detected = new Array;
			var imgw :int = tgt.width, imgh :int = tgt.height;
			var scaledw :int, scaledh :int, limitx  :int, limity  :int, stepx :int, stepy :int, result :int, factor:Number = 1;
			if (_factor.length == 0) {
				for( factor = 1;
					factor*cascade.base_window_w < imgw && factor*cascade.base_window_h < imgh;
					factor *= _options.scale_factor )
				{
					scaledw = int( cascade.base_window_w * factor );
					scaledh = int( cascade.base_window_h * factor );
					if( scaledw < _options.min_size || scaledh < _options.min_size ){
						continue;
					}
					_factor.push([factor, scaledw, scaledh]);
					_check.push(_check.length);
				}
			}
			var n:int = _factor.length;
			for (var i:int = 0; i < n; i++) {
				var i2:int = _check[i];
				factor = _factor[i2][0];
				scaledw = _factor[i2][1];
				scaledh = _factor[i2][2];
				limitx = tgt.width  – scaledw;
				limity = tgt.height – scaledh;
				if( _options.endx != ObjectDetectorOptions.INVALID_POS && _options.endy != ObjectDetectorOptions.INVALID_POS ){
					limitx = Math.min( _options.endx, limitx );
					limity = Math.min( _options.endy, limity );
				}
				logger("[detect]limitx,y: "+limitx+","+limity);
				//stepx  = Math.max(_options.MIN_MARGIN_SEARCH,factor);
				stepx  = scaledw>>3;
				stepy  = stepx;
logger("[detect] w,h,step: "+scaledw+","+scaledh+","+stepx);
				var ix:int=0, iy:int=0, startx:int=0, starty:int=0;
				if( _options.startx != ObjectDetectorOptions.INVALID_POS && _options.starty != ObjectDetectorOptions.INVALID_POS ){
					startx = Math.max( ix, _options.startx );
					starty = Math.max( iy, _options.starty );
				}
				logger("[detect]startx,y: "+startx+","+starty);
				var yArray:Array = [];
				var yIndexArray:Array = [];
				for (iy = starty; iy < limity; iy += stepy ) {
					yArray.push(iy);
				}
				yIndexArray = narabe(yArray, _detected[0]);
				var xArray:Array = [];
				var xIndexArray:Array = [];
				for( ix = startx; ix < limitx; ix += stepx ){
					xArray.push(ix);
				}
				xIndexArray = narabe(xArray, _detected[1]);
				var m:int = yArray.length;
				for (var j:int = 0; j < m; j++) {
					if (detected.length > 0) { continue };
					iy = yArray[yIndexArray[j]];
					var p:int = xArray.length;
					for (var k:int = 0; k < p; k++) {
						if (detected.length > 0) { continue };
						ix = xArray[xIndexArray[k]];
						if( _options.search_mode & ObjectDetectorOptions.SEARCH_MODE_NO_OVERLAP &&
							overlaps(ix,iy,scaledw,scaledh) ){
							// do nothing
						}else{
							//logger("[checkAndRun]ix,iy,scaledw,scaledh: "+ix+","+iy+","+scaledw+","+scaledh);
							cascade.scale = factor;
							result = runHaarClassifierCascade(cascade,ix,iy,scaledw,scaledh);
							if ( result > 0 ) {
								var faceArea :Rectangle = new Rectangle(ix,iy,scaledw,scaledh);
								detected.push( faceArea );
								logger("[createCheckAndRun]found!: " + ix + "," + iy + "," + scaledw + "," + scaledh);
								_check = narabe(_check, i2);
								_detected = [iy, ix];
								// doesnt mean anything cause detection is not time-divided (now)
								var ev1 :ObjectDetectorEvent = new ObjectDetectorEvent( ObjectDetectorEvent.FACE_FOUND );
								ev1.rect = faceArea;
								dispatchEvent( ev1 );
							}
						}
					}
				}
			}
// integrate redundant candidates …
			var ev2 :ObjectDetectorEvent = new ObjectDetectorEvent( ObjectDetectorEvent.DETECTION_COMPLETE );
			ev2.rects = detected;
			//trace(detected);
			//return;
			dispatchEvent( ev2 );
		}
		private function narabe(targetArray:Array, i2:int):Array {
			var n:int = targetArray.length;
			targetArray.sort(Array.NUMERIC);
			var ar:Array = [];
			for (var i:int = 0; i < n; i++) {
				ar.push(Math.abs(targetArray[i] – i2 + 0.1));
			}
			return ar.sort(Array.NUMERIC | Array.RETURNINDEXEDARRAY).concat();
		}
		private function runHaarClassifierCascade(c:HaarCascade,x:int,y:int,w:int,h:int):int{
			//logger("[runHaarClassifierCascade] c:",c,x,y,w,h);
			var mean :Number                 = tgt.getSum(x,y,w,h) * c.inv_window_area;
			var variance_norm_factor :Number = tgt.getSum2(x,y,w,h)* c.inv_window_area – mean*mean;
			if( variance_norm_factor >= 0 ){
				variance_norm_factor = Math.sqrt(variance_norm_factor);
			}else{
				variance_norm_factor = 1;
			}
			var trees :Array = c.trees, treenums :int = trees.length, tree: FeatureTree, features :Array, featurenums :int, val :Number = 0, sum :Number = 0, feature :FeatureBase, i :int=0, j :int=0, st_th:Number = 0;
			for( i=0; i<treenums; i++ ){
				tree        = trees[i];
				features    = tree.features;
				featurenums = features.length;
				val         = 0;
				st_th       = tree.stage_threshold;
				for( j=0; j<featurenums; j++ ){
					feature = features[j];
					sum  = feature.getSum( tgt, x, y );
//					val += (sum < feature.threshold * variance_norm_factor) ?
//						feature.left_val : feature.right_val;
//
//					* Ternary operation causes coersion and makes slower. 
					if (sum < feature.threshold * variance_norm_factor)
						val += feature.left_val;
					else
						val += feature.right_val;
					if( val > st_th ){
						// left_val, right_val are always plus
						break;
					}
				}
				if( val < st_th ){
					return 0;
				}
			}
			return 1;
		}
		private function overlaps(_x:int,_y:int,_w:int,_h:int):Boolean{
			// if the area we’re going to check contains, or overlaps the square which is already picked up, ignore it
			var i:int=0;
			var l:int=detected.length;
			var tg: Rectangle;
			var x:int = _x, y:int = _y, w:int = _w, h:int = _h, tx1:int, tx2:int, ty1:int, ty2:int;
			for( i=0; i<l; i++ ){
				tg = detected[i];
				tx1 = tg.x;
				tx2 = tg.x + tg.width;
				ty1 = tg.y;
				ty2 = tg.y + tg.height;
				if(  ( ( x <= tx1 && tx1 < x+w )
				     ||( x <= tx2 && tx2 < x+w ) )
				  && ( ( y <= ty1 && ty1 < y+h )
				     ||( y <= ty2 && ty2 < y+h ) )  )
				{
					return true;
				}
			}
			return false;
		}
		public function loadHaarCascades( url :String ) :void {
			xmlloader = new HaarCascadeLoader( url );
			xmlloader.addEventListener(Event.COMPLETE,function(e:Event):void{
				xmlloader.removeEventListener(Event.COMPLETE,arguments.callee);
				dispatchEvent( new ObjectDetectorEvent(ObjectDetectorEvent.HAARCASCADES_LOAD_COMPLETE) );
				cascade = xmlloader.cascade;
				loaded = true;
				if( waiting ){
					waiting = false;
					detect();
				}
			});
			loaded = false;
			dispatchEvent( new ObjectDetectorEvent(ObjectDetectorEvent.HAARCASCADES_LOADING) );
			xmlloader.load();	// kick it!
		}
		public function set bitmap( bmp :Bitmap ) :void {
			tgt.bitmapData = bmp.bitmapData;
		}
		public function set options( opt :ObjectDetectorOptions ) :void {
			_options = opt;
		}
		private function logger(… args):void{
			if(!debug){ return; }
			log(["[ObjectDetector]"+args.shift()].concat(args));
		}
	}
}
[/sourcecode]
▼ActionScript AS3(FP10)
Main.as
[sourcecode language=”as3″]
/*
 * マリレーナ
 * Marilenaを使って顔検知してみる。
 * http://www.libspark.org/wiki/mash/Marilena
 *
 * */
package
{
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;
	import flash.display.Sprite;
	import flash.display.Graphics;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.events.Event;
	import flash.geom.Matrix;
	import flash.geom.Rectangle;
	import net.hires.debug.Stats;
	public class Main extends Sprite {
		private var _faceDetect:FaceDetect;
		private var _faceRectContainer:Sprite;
		private var _bitmap:Bitmap;
		private var _snapShot:SnapShot;
		public function Main() {
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			this.addEventListener(Event.ADDED_TO_STAGE, ini);
		}
		private function ini(event:Event):void {
			this.removeEventListener(Event.ADDED_TO_STAGE, ini);
			_bitmap = new Bitmap(new BitmapData(320, 240));
			this.addChild(_bitmap);
			this.addChild(new Stats());
			_faceRectContainer = new Sprite();
			this.addChild(_faceRectContainer);
			_snapShot = new SnapShot();
			_snapShot.onStart = onStart;
			_snapShot.y = 240;
			_faceDetect = new FaceDetect();
		}
		private function onStart():void {
			_faceDetect = new FaceDetect();
			this.addEventListener(Event.ENTER_FRAME, atEnter);
			_faceDetect.onComplete = onComplete;
		}
		private function atEnter(event:Event):void {
			_faceDetect.setBitmap(_snapShot.getBitmap());
		}
private function onComplete(rects:*):void {
_bitmap.bitmapData.draw(_faceDetect.getBitmap());
			if ( rects.length > 0) {
				var g:Graphics = _faceRectContainer.graphics;
				g.clear();
rects.forEach(function(r:Rectangle, idx:int, arr:Array):void {
					var mosaic:BitmapData = new BitmapData(320/8, 240/8);
					mosaic.draw(_bitmap,new Matrix(1/8,0,0,1/8));
					//g.beginFill(0xFF6666, 0.8);
					g.beginBitmapFill(mosaic,new Matrix(8,0,0,8));
					g.drawEllipse(r.x – r.width * 0.15, r.y – r.height * 0.28, r.width * 1.3, r.height * 1.3);
					g.endFill();
				});
			}
		}
	}
}
//webカムからbitmapを取得するクラス
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.ActivityEvent;
import flash.media.Camera;
import flash.media.Video;
class SnapShot extends Sprite {
	private var _video:Video;
	private var _camera:Camera;
	public var onStart:Function = function():void { };
	public function SnapShot() {
		_camera = Camera.getCamera();
		_camera.setMode(320, 240, 10);
		if (_camera != null) {
			_video = new Video(320, 240);
			_video.attachCamera(_camera);
			this.addChild(_video);
		} else {
			trace("You need a camera.");
		}
		_camera.addEventListener(ActivityEvent.ACTIVITY, _onActivity);
	}
	private var _isStart:Boolean;
	//カメラから画像が取得できたら、一度だけ動く
	private function _onActivity(event:ActivityEvent):void {
		_camera.removeEventListener(ActivityEvent.ACTIVITY, _onActivity);
		if (!_isStart) {
			_isStart = true;
			onStart();
		}
	}
	//カメラの画像をBitmapとして取り出す。
	public function getBitmap():Bitmap {
		var bitmap:Bitmap = new Bitmap(new BitmapData(_video.width, _video.height));
		if(_video){
			bitmap.bitmapData.draw(_video);
		}
		return bitmap;
	}
}
//Bitmapを投げると、顔の範囲をあれば返すクラス。
import flash.display.Bitmap;
import jp.maaash.ObjectDetection.ObjectDetector;
import jp.maaash.ObjectDetection.ObjectDetectorOptions;
import jp.maaash.ObjectDetection.ObjectDetectorEvent;
class FaceDetect {
	private var _detector:ObjectDetector;
	public var onComplete:Function = function(rects:*):void { };
	private var _bitmap:Bitmap;
	public function FaceDetect() {
		_detector = new ObjectDetector();
		_detector.options = getDetectorOptions();
		_detector.addEventListener(ObjectDetectorEvent.DETECTION_COMPLETE, DETECTION_COMPLETE);
		//_detector.loadHaarCascades("face.zip");
		//アップされているswfではblogに乗せるように↓にしている。
		_detector.loadHaarCascades("http://www.mztm.jp/wp/wp-content/uploads/2010/04/face.zip");
	};
	private function DETECTION_COMPLETE(event:ObjectDetectorEvent):void {
		onComplete(event.rects);
	}
	public function setBitmap(bitmap:Bitmap):void {
		//_detector.loadHaarCascades("face.zip");
		_detector.detect(bitmap);
		_bitmap = bitmap;
	}
	public function getBitmap():Bitmap {
		return _bitmap;
	}
	private function getDetectorOptions() :ObjectDetectorOptions {
		var options:ObjectDetectorOptions = new ObjectDetectorOptions();
		options.min_size  = 50;
		options.startx    = ObjectDetectorOptions.INVALID_POS;
		options.starty    = ObjectDetectorOptions.INVALID_POS;
		options.endx      = ObjectDetectorOptions.INVALID_POS;
		options.endy      = ObjectDetectorOptions.INVALID_POS;
		return options;
	}
}
[/sourcecode]

