Repository: lonekorean/diff-cam-engine Branch: master Commit: 6226b1c4fef1 Files: 4 Total size: 13.4 KB Directory structure: gitextract_50974trg/ ├── .gitignore ├── LICENSE.md ├── README.md └── diff-cam-engine.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store Thumbs.db ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2016 Will Boyd This software is released under the MIT license: http://opensource.org/licenses/MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # diff-cam-engine Core engine for building motion detection web apps. ### Usage `diff-cam-engine.js` provides a `DiffCamEngine` object that accesses the webcam, captures images from it, and evaluates motion. You'll want to use the `adapter.js` shim, which is available here: https://github.com/webrtc/adapter. Add it before `diff-cam-engine.js`. With that in place, call `DiffCamEngine.init()` to initialize. This will set things up and ask the user for permission to access the webcam. ``` javascript DiffCamEngine.init({ // config options go here }); ``` Then call `start()` to begin the actual motion sensing. Do not call `start()` before `init()` has finished. It's a good idea to call `start()` from `initSuccessCallback()` (more on this later). Call `stop()` to turn things off. ### Configuration You can customize how `DiffCamEngine` behaves by passing an object of name/value pairs into `init()`. For example: ``` javascript DiffCamEngine.init({ video: myVideo, captureIntervalTime: 50, captureCallback: myCaptureHandler // etc. }); ``` ##### Variables The following variables can be passed into `init()`: | Name | Default | Description | | --- | --- | --- | | video | internal <video> (not visible) | The <video> element for showing the live webcam stream | | motionCanvas | internal <canvas> (not visible) | The <canvas> element for showing the visual motion heatmap | | captureIntervalTime | 100 | Number of ms between capturing images from the stream | | captureWidth | 640 | Width of captured images from stream | | captureHeight | 480 | Height of capture images from stream | | diffWidth | 64 | Width of (usually downsized) images used for diffing and showing motion | | diffHeight | 48 | Height of (usually downsized) images used for diffing and showing motion | | pixelDiffThreshold | 32 | Minimum difference in a pixel to be considered changed | | scoreThreshold | 16 | Minimum number of changed pixels for an image to be considered as having motion | | includeMotionBox | false | Flag to calculate and display (on motionCanvas) the bounding box of motion | | includeMotionPixels | false | Flag to include data indicating all the changed pixels | ##### Callbacks There are also a couple callback functions you can specify. This is the primary way of interacting with `DiffCamEngine` during execution. Pass these into `init()` just like the variables above. | Name | Description | | --- | --- | | initSuccessCallback | Called when init has successfully completed | | initErrorCallback | Called when init fails | | startCompleteCallback | Called once the webcam has begun streaming | | captureCallback | Called after a captured image from the stream has been evaluated | `captureCallback` is the only one with a parameter. This parameter is an object with multiple properties on it. These properties are: | Property | Description | | --- | --- | | imageData | The imageData object for the captured image | | score | The evaluated score for the captured image | | hasMotion | Whether or not the score meets the score threshold for motion | | motionBox | An object containg x min/max and y min/max for a box wrapping the region in which motion occurred, only returned if includeMotionBox is `true` | | motionPixels | An object containg each pixel in the image that changed (indicating motion), only returned if includeMotionPixels is `true` | | getURL | Convenience function, returns imageData converted to a URL suitable for setting as the src of an image | | checkMotionPixel | Convenience function, takes in an x and y cooridnate, returns a boolean indicating if the pixel at that location has changed | ### Functions `DiffCamEngine` exposes the following public functions: | Function | Description | | --- | --- | | init | Initializes everything and requests permission to access the webcam | | start | Begin streaming from the webcam | | stop | Stop streaming from the webcam | | getPixelDiffThreshold | Get pixelDiffThreshold during execution | | setPixelDiffThreshold | Set pixelDiffThreshold during execution | | getScoreThreshold | Get scoreThreshold during execution | | setScoreThreshold | Set scoreThreshold during execution | ### Examples Check out https://github.com/lonekorean/diff-cam-scratchpad (specifically /diff-cam-example and /turret-security) for examples that use diff-cam-engine. Check out https://github.com/lonekorean/diff-cam-feed for a more complex web app powered by diff-cam-engine. ================================================ FILE: diff-cam-engine.js ================================================ var DiffCamEngine = (function() { var stream; // stream obtained from webcam var video; // shows stream var captureCanvas; // internal canvas for capturing full images from video var captureContext; // context for capture canvas var diffCanvas; // internal canvas for diffing downscaled captures var diffContext; // context for diff canvas var motionCanvas; // receives processed diff images var motionContext; // context for motion canvas var initSuccessCallback; // called when init succeeds var initErrorCallback; // called when init fails var startCompleteCallback; // called when start is complete var captureCallback; // called when an image has been captured and diffed var captureInterval; // interval for continuous captures var captureIntervalTime; // time between captures, in ms var captureWidth; // full captured image width var captureHeight; // full captured image height var diffWidth; // downscaled width for diff/motion var diffHeight; // downscaled height for diff/motion var isReadyToDiff; // has a previous capture been made to diff against? var pixelDiffThreshold; // min for a pixel to be considered significant var scoreThreshold; // min for an image to be considered significant var includeMotionBox; // flag to calculate and draw motion bounding box var includeMotionPixels; // flag to create object denoting pixels with motion function init(options) { // sanity check if (!options) { throw 'No options object provided'; } // incoming options with defaults video = options.video || document.createElement('video'); motionCanvas = options.motionCanvas || document.createElement('canvas'); captureIntervalTime = options.captureIntervalTime || 100; captureWidth = options.captureWidth || 640; captureHeight = options.captureHeight || 480; diffWidth = options.diffWidth || 64; diffHeight = options.diffHeight || 48; pixelDiffThreshold = options.pixelDiffThreshold || 32; scoreThreshold = options.scoreThreshold || 16; includeMotionBox = options.includeMotionBox || false; includeMotionPixels = options.includeMotionPixels || false; // callbacks initSuccessCallback = options.initSuccessCallback || function() {}; initErrorCallback = options.initErrorCallback || function() {}; startCompleteCallback = options.startCompleteCallback || function() {}; captureCallback = options.captureCallback || function() {}; // non-configurable captureCanvas = document.createElement('canvas'); diffCanvas = document.createElement('canvas'); isReadyToDiff = false; // prep video video.autoplay = true; // prep capture canvas captureCanvas.width = captureWidth; captureCanvas.height = captureHeight; captureContext = captureCanvas.getContext('2d'); // prep diff canvas diffCanvas.width = diffWidth; diffCanvas.height = diffHeight; diffContext = diffCanvas.getContext('2d'); // prep motion canvas motionCanvas.width = diffWidth; motionCanvas.height = diffHeight; motionContext = motionCanvas.getContext('2d'); requestWebcam(); } function requestWebcam() { var constraints = { audio: false, video: { width: captureWidth, height: captureHeight } }; navigator.mediaDevices.getUserMedia(constraints) .then(initSuccess) .catch(initError); } function initSuccess(requestedStream) { stream = requestedStream; initSuccessCallback(); } function initError(error) { console.log(error); initErrorCallback(); } function start() { if (!stream) { throw 'Cannot start after init fail'; } // streaming takes a moment to start video.addEventListener('canplay', startComplete); video.srcObject = stream; } function startComplete() { video.removeEventListener('canplay', startComplete); captureInterval = setInterval(capture, captureIntervalTime); startCompleteCallback(); } function stop() { clearInterval(captureInterval); video.src = ''; motionContext.clearRect(0, 0, diffWidth, diffHeight); isReadyToDiff = false; } function capture() { // save a full-sized copy of capture captureContext.drawImage(video, 0, 0, captureWidth, captureHeight); var captureImageData = captureContext.getImageData(0, 0, captureWidth, captureHeight); // diff current capture over previous capture, leftover from last time diffContext.globalCompositeOperation = 'difference'; diffContext.drawImage(video, 0, 0, diffWidth, diffHeight); var diffImageData = diffContext.getImageData(0, 0, diffWidth, diffHeight); if (isReadyToDiff) { var diff = processDiff(diffImageData); motionContext.putImageData(diffImageData, 0, 0); if (diff.motionBox) { motionContext.strokeStyle = '#fff'; motionContext.strokeRect( diff.motionBox.x.min + 0.5, diff.motionBox.y.min + 0.5, diff.motionBox.x.max - diff.motionBox.x.min, diff.motionBox.y.max - diff.motionBox.y.min ); } captureCallback({ imageData: captureImageData, score: diff.score, hasMotion: diff.score >= scoreThreshold, motionBox: diff.motionBox, motionPixels: diff.motionPixels, getURL: function() { return getCaptureUrl(this.imageData); }, checkMotionPixel: function(x, y) { return checkMotionPixel(this.motionPixels, x, y) } }); } // draw current capture normally over diff, ready for next time diffContext.globalCompositeOperation = 'source-over'; diffContext.drawImage(video, 0, 0, diffWidth, diffHeight); isReadyToDiff = true; } function processDiff(diffImageData) { var rgba = diffImageData.data; // pixel adjustments are done by reference directly on diffImageData var score = 0; var motionPixels = includeMotionPixels ? [] : undefined; var motionBox = undefined; for (var i = 0; i < rgba.length; i += 4) { var pixelDiff = rgba[i] * 0.3 + rgba[i + 1] * 0.6 + rgba[i + 2] * 0.1; var normalized = Math.min(255, pixelDiff * (255 / pixelDiffThreshold)); rgba[i] = 0; rgba[i + 1] = normalized; rgba[i + 2] = 0; if (pixelDiff >= pixelDiffThreshold) { score++; coords = calculateCoordinates(i / 4); if (includeMotionBox) { motionBox = calculateMotionBox(motionBox, coords.x, coords.y); } if (includeMotionPixels) { motionPixels = calculateMotionPixels(motionPixels, coords.x, coords.y, pixelDiff); } } } return { score: score, motionBox: score > scoreThreshold ? motionBox : undefined, motionPixels: motionPixels }; } function calculateCoordinates(pixelIndex) { return { x: pixelIndex % diffWidth, y: Math.floor(pixelIndex / diffWidth) }; } function calculateMotionBox(currentMotionBox, x, y) { // init motion box on demand var motionBox = currentMotionBox || { x: { min: coords.x, max: x }, y: { min: coords.y, max: y } }; motionBox.x.min = Math.min(motionBox.x.min, x); motionBox.x.max = Math.max(motionBox.x.max, x); motionBox.y.min = Math.min(motionBox.y.min, y); motionBox.y.max = Math.max(motionBox.y.max, y); return motionBox; } function calculateMotionPixels(motionPixels, x, y, pixelDiff) { motionPixels[x] = motionPixels[x] || []; motionPixels[x][y] = true; return motionPixels; } function getCaptureUrl(captureImageData) { // may as well borrow captureCanvas captureContext.putImageData(captureImageData, 0, 0); return captureCanvas.toDataURL(); } function checkMotionPixel(motionPixels, x, y) { return motionPixels && motionPixels[x] && motionPixels[x][y]; } function getPixelDiffThreshold() { return pixelDiffThreshold; } function setPixelDiffThreshold(val) { pixelDiffThreshold = val; } function getScoreThreshold() { return scoreThreshold; } function setScoreThreshold(val) { scoreThreshold = val; } return { // public getters/setters getPixelDiffThreshold: getPixelDiffThreshold, setPixelDiffThreshold: setPixelDiffThreshold, getScoreThreshold: getScoreThreshold, setScoreThreshold: setScoreThreshold, // public functions init: init, start: start, stop: stop }; })();