/* * PROJECT: FLARManager * http://transmote.com/flar * Copyright 2009, Eric Socolofsky * -------------------------------------------------------------------------------- * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this framework; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * For further information please contact: * * http://transmote.com/flar */ package com.transmote.flar.source { import com.transmote.utils.time.Timeout; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.BlendMode; import flash.display.Sprite; import flash.events.ActivityEvent; import flash.events.ErrorEvent; import flash.events.StatusEvent; import flash.geom.Matrix; import flash.geom.Rectangle; import flash.media.Camera; import flash.media.Video; /** * Use the contents of a Camera feed as a source image for tracker target detection. * * @author Eric Socolofsky * @url http://transmote.com/flar * @see com.transmote.flar.FLARManager */ public class FLARCameraSource extends Sprite implements IFLARSource { private static const CAMERA_VALIDATION_TIME:Number = 3000; private static const VALID_CAMERA_MIN_FRAME_DIFFERENCE:uint = 10; private static const ACTIVITY_TIMEOUT:int = 1000; private var _trackerToDisplayRatio:Number; private var _mirrored:Boolean; private var _useDefaultCamera:Boolean; private var _inited:Boolean; private var _activityThreshold:int; private var camera:Camera; private var video:Video; private var manualCameraIndex:int = -1; private var cameraInitialActivityDetected:Boolean; private var cameraValidationTimeout:Timeout; private var attemptedCameras:Vector.; private var cameraValidationBmpData:BitmapData; private var displayBmpData:BitmapData; private var displayBitmap:Bitmap; private var displayMatrix:Matrix; private var sampleWidth:Number; private var sampleHeight:Number; private var sampleBmpData:BitmapData; private var sampleBitmap:Bitmap; private var sampleMatrix:Matrix; private var trackerToSourceRatio:Number; /** * Constructor. */ public function FLARCameraSource () {} /** * Initialize this FLARCameraSource. * * @param captureWidth Width at which to capture video. * @param captureHeight Height at which to capture video. * @param fps Framerate of camera capture. * @param mirrored If true, video is flipped horizontally. * @param displayWidth Width at which to display video. * @param displayHeight Height at which to display video. * @param trackerToSourceRatio Amount to downsample camera input. * The captured video is scaled down by this value * before being sent to tracker for analysis. * Trackers run faster with more downsampling, * but also have more difficulty recognizing marker patterns and targets. * A value of 1.0 results in no downsampling; * A value of 0.5 (the default) downsamples the camera input by half. * * @throws Error If no camera is found. * (Thrown by initCamera, called from this method.) */ public function init ( captureWidth:int=320, captureHeight:int=240, fps:Number=30, mirrored:Boolean=true, displayWidth:int=-1, displayHeight:int=-1, trackerToSourceRatio:Number=0.5, activityThreshold:int=0) :void { if (displayWidth == -1) { displayWidth = captureWidth; } if (displayHeight == -1) { displayHeight = captureHeight; } this._activityThreshold = activityThreshold; this.initCamera(captureWidth, captureHeight, fps); this.trackerToSourceRatio = trackerToSourceRatio; // sampleWidth/Height describe size of BitmapData sent to tracker every frame this.sampleWidth = captureWidth * this.trackerToSourceRatio; this.sampleHeight = captureHeight * this.trackerToSourceRatio; // scale and crop camera source to fit within specified display width/height. var fitWidthRatio:Number = displayWidth / captureWidth; var fitHeightRatio:Number = displayHeight / captureHeight; var videoWidth:Number, videoHeight:Number; var videoX:Number=0, videoY:Number=0; if (fitHeightRatio > fitWidthRatio) { // fit to height, center horizontally, crop left/right edges videoWidth = fitHeightRatio * captureWidth; videoHeight = displayHeight; videoX = -0.5 * (videoWidth - displayWidth); this._trackerToDisplayRatio = 1 / fitHeightRatio; this.sampleWidth = this.sampleHeight * (displayWidth/displayHeight); } else { // fit to width, center vertically, crop top/bottom edges videoWidth = displayWidth; videoHeight = fitWidthRatio * captureHeight; videoY = -0.5 * (videoHeight - displayHeight); this._trackerToDisplayRatio = 1 / fitWidthRatio; this.sampleHeight = this.sampleWidth / (displayWidth/displayHeight); } this._trackerToDisplayRatio *= this.trackerToSourceRatio; // source video this.video = new Video(videoWidth, videoHeight); this.video.x = videoX; this.video.y = videoY; this.video.attachCamera(this.camera); // BitmapData downsampled from source video, sent to tracker every frame this.sampleBmpData = new BitmapData(this.sampleWidth, this.sampleHeight, false, 0); this.sampleBitmap = new Bitmap(this.sampleBmpData); this.sampleBitmap.width = displayWidth; this.sampleBitmap.height = displayHeight; // cropped, full-res video displayed on-screen this.displayBmpData = new BitmapData(displayWidth, displayHeight, false, 0); this.displayBitmap = new Bitmap(this.displayBmpData); // cropped, full-res video for display this.addChild(this.displayBitmap); // uncomment to view source video // this.addChild(this.video); // uncomment to view downsampled video sent to tracker // this.addChild(this.sampleBitmap); // calls buildSampleMatrices this.mirrored = mirrored; this._inited = true; } /** * Update the BitmapData source. */ public function update () :void { this.displayBmpData.draw(this.video, this.displayMatrix); // only update source sent to FLARToolkit if there is motion. if (this.camera && this.camera.activityLevel >= this.camera.motionLevel) { this.sampleBmpData.draw(this.video, this.sampleMatrix); } } /** * Validate that the selected Camera is active, * by checking Camera.activityLevel. * If camera is not active, FLARCameraSource attempts * to reinitialize with next available camera. * * @param bSuppressReinit If false (default), this method will reinitialize the camera with the next available camera. * @return true If selected Camera is active (activityLevel != -1). */ public function validateCamera (bSuppressReinit:Boolean=false) :Boolean { if (this.camera.activityLevel == -1) { if (!bSuppressReinit) { this.initCamera(this.camera.width, this.camera.height, this.camera.fps); } return false; } else { this.onInitialCameraValidation(); return true; } } /** * Retrieve the BitmapData source used for analysis. * NOTE: returns the actual BitmapData object, not a clone. */ public function get source () :BitmapData { return this.sampleBmpData; } /** * Size of BitmapData source used for analysis. */ public function get sourceSize () :Rectangle { return new Rectangle(0, 0, this.sampleWidth, this.sampleHeight); } /** * Ratio of area of tracker's reported results to display size. * Use to scale (multiply) results of tracker analysis to correctly fit display area. */ public function get trackerToDisplayRatio () :Number { return this._trackerToDisplayRatio; } /** * Set to true to flip the camera image horizontally. */ public function get mirrored () :Boolean { return this._mirrored; } public function set mirrored (val:Boolean) :void { this._mirrored = val; this.buildSampleMatrices(); } /** * If true, FLARCameraSource will use the default camera, * acquired via a Camera.getCamera() with no parameters. * If false (the default), FLARCameraSource will loop through the camera drivers * available on the system until it finds one that reports activity. */ public function get useDefaultCamera () :Boolean { return this._useDefaultCamera; } public function set useDefaultCamera (val:Boolean) :void { this._useDefaultCamera = val; if (this.camera) { this.initCamera(this.camera.width, this.camera.height, this.camera.fps); } } /** * When camera.activityLevel is less than this value, * the source will not be sent to FLARToolkit. * This freezes the marker in place when there is very little motion. * Defaults to 16; valid values range from 0 (no suppression) to 100 (full suppression). * Set higher to further reduce jitter in stationary markers, * and set lower to allow more freedom of motion with stationary markers. * * Thanks to Deepanjan Das for this idea: * http://deepanjandas.wordpress.com/2010/07/08/augmented-reality-using-flartoolkit-restrict-unnecessary-model-jumping/ */ public function get activityThreshold () :int { return this._activityThreshold; } public function set activityThreshold (val:int) :void { this._activityThreshold = val; if (this.camera) { this.camera.setMotionLevel(this._activityThreshold, ACTIVITY_TIMEOUT); } } /** * Returns true if initialization is complete. */ public function get inited () :Boolean { return this._inited; } /** * Halts all processes and frees this instance for garbage collection. */ public function dispose () :void { this.camera = null; if (this.video) { this.video.clear(); this.video.attachCamera(null); } this.video = null; if (this.cameraValidationTimeout) { this.cameraValidationTimeout.cancel(); } this.cameraValidationTimeout = null; this.attemptedCameras = null; if (this.displayBmpData) { this.displayBmpData.dispose(); } this.displayBmpData = null; this.displayBitmap = null; this.displayMatrix = null; if (this.sampleBmpData) { this.sampleBmpData.dispose(); } this.sampleBmpData = null; this.sampleBitmap = null; this.sampleMatrix = null; } /** * The index, in Camera.names, of the active camera. * If no camera is currently active, returns -1. * * Setting this value will destroy any active camera, * and reinitialize with the camera at the specified index. */ public function get cameraIndex () :int { if (this.camera) { return this.camera.index; } else { return -1; } } public function set cameraIndex (index:int) :void { this.manualCameraIndex = index; if (this.camera) { this.initCamera(this.camera.width, this.camera.height, this.camera.fps); } } private function initCamera (captureWidth:int, captureHeight:int, fps:int) :void { if (this.cameraValidationTimeout) { this.cameraValidationTimeout.cancel(); } if (this.camera) { this.destroyCamera(); } var names:Array = Camera.names; if (this.useDefaultCamera) { this.camera = Camera.getCamera(); } else { // set up Camera to capture source video if (this.manualCameraIndex >= 0) { // use camera index specified via cameraIndex accessor (setter) this.camera = Camera.getCamera(this.manualCameraIndex.toString()); } else { // attempt to init cameras one-by-one until an active camera is selected, // or all options are exhausted, starting with the default camera. if (!this.attemptedCameras || this.attemptedCameras.length != Camera.names.length) { // if no cameras attempted yet, start with default camera this.attemptedCameras = new Vector.(Camera.names.length, true); this.camera = Camera.getCamera(); if (this.camera) { this.attemptedCameras[this.camera.index] = true; } } else { // else, loop through available camera drivers that have not yet been attempted for (var i:int=0; i", 0xFF111111, 0xFF00FF00, 0x00FFFFFF); this.cameraValidationBmpData.dispose(); currentCameraBmpData.dispose(); if (difference < VALID_CAMERA_MIN_FRAME_DIFFERENCE) { trace("[FLARManager] Secondary camera validation failed for camera '"+ this.camera.name +"'. Reiniting camera."); this.initCamera(this.camera.width, this.camera.height, this.camera.fps); } else { trace("[FLARManager] Validated camera '"+ this.camera.name +"'."); } } private function destroyCamera () :void { this.camera.removeEventListener(ActivityEvent.ACTIVITY, this.onCameraActivity); this.camera.removeEventListener(StatusEvent.STATUS, this.onCameraStatus); this.camera = null; } private function buildSampleMatrices () :void { if (!this.video) { return; } // construct transformation matrix used when updating displayed video // and when updating BitmapData source for trackers if (this._mirrored) { this.displayMatrix = new Matrix(-1, 0, 0, 1, this.video.width+this.video.x, this.video.y); } else { this.displayMatrix = new Matrix(1, 0, 0, 1, this.video.x, this.video.y); } // source does not get mirrored; // trackers must be able to recognize non-mirrored patterns. // transformation mirroring happens in FLARManager.detectMarkers(). this.sampleMatrix = new Matrix( this._trackerToDisplayRatio, 0, 0, this._trackerToDisplayRatio, this._trackerToDisplayRatio*this.video.x, this._trackerToDisplayRatio*this.video.y); } } }