├── .gitignore ├── LICENSE.md ├── README.md └── diff-cam-engine.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Will Boyd 2 | 3 | This software is released under the MIT license: http://opensource.org/licenses/MIT 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff-cam-engine 2 | 3 | Core engine for building motion detection web apps. 4 | 5 | ### Usage 6 | 7 | `diff-cam-engine.js` provides a `DiffCamEngine` object that accesses the webcam, captures images from it, and evaluates motion. 8 | 9 | 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`. 10 | 11 | With that in place, call `DiffCamEngine.init()` to initialize. This will set things up and ask the user for permission to access the webcam. 12 | 13 | ``` javascript 14 | DiffCamEngine.init({ 15 | // config options go here 16 | }); 17 | ``` 18 | 19 | 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). 20 | 21 | Call `stop()` to turn things off. 22 | 23 | ### Configuration 24 | 25 | You can customize how `DiffCamEngine` behaves by passing an object of name/value pairs into `init()`. For example: 26 | 27 | ``` javascript 28 | DiffCamEngine.init({ 29 | video: myVideo, 30 | captureIntervalTime: 50, 31 | captureCallback: myCaptureHandler 32 | // etc. 33 | }); 34 | ``` 35 | 36 | ##### Variables 37 | 38 | The following variables can be passed into `init()`: 39 | 40 | | Name | Default | Description | 41 | | --- | --- | --- | 42 | | video | internal <video> (not visible) | The <video> element for showing the live webcam stream | 43 | | motionCanvas | internal <canvas> (not visible) | The <canvas> element for showing the visual motion heatmap | 44 | | captureIntervalTime | 100 | Number of ms between capturing images from the stream | 45 | | captureWidth | 640 | Width of captured images from stream | 46 | | captureHeight | 480 | Height of capture images from stream | 47 | | diffWidth | 64 | Width of (usually downsized) images used for diffing and showing motion | 48 | | diffHeight | 48 | Height of (usually downsized) images used for diffing and showing motion | 49 | | pixelDiffThreshold | 32 | Minimum difference in a pixel to be considered changed | 50 | | scoreThreshold | 16 | Minimum number of changed pixels for an image to be considered as having motion | 51 | | includeMotionBox | false | Flag to calculate and display (on motionCanvas) the bounding box of motion | 52 | | includeMotionPixels | false | Flag to include data indicating all the changed pixels | 53 | 54 | ##### Callbacks 55 | 56 | 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. 57 | 58 | | Name | Description | 59 | | --- | --- | 60 | | initSuccessCallback | Called when init has successfully completed | 61 | | initErrorCallback | Called when init fails | 62 | | startCompleteCallback | Called once the webcam has begun streaming | 63 | | captureCallback | Called after a captured image from the stream has been evaluated | 64 | 65 | `captureCallback` is the only one with a parameter. This parameter is an object with multiple properties on it. These properties are: 66 | 67 | | Property | Description | 68 | | --- | --- | 69 | | imageData | The imageData object for the captured image | 70 | | score | The evaluated score for the captured image | 71 | | hasMotion | Whether or not the score meets the score threshold for motion | 72 | | 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` | 73 | | motionPixels | An object containg each pixel in the image that changed (indicating motion), only returned if includeMotionPixels is `true` | 74 | | getURL | Convenience function, returns imageData converted to a URL suitable for setting as the src of an image | 75 | | checkMotionPixel | Convenience function, takes in an x and y cooridnate, returns a boolean indicating if the pixel at that location has changed | 76 | 77 | ### Functions 78 | 79 | `DiffCamEngine` exposes the following public functions: 80 | 81 | | Function | Description | 82 | | --- | --- | 83 | | init | Initializes everything and requests permission to access the webcam | 84 | | start | Begin streaming from the webcam | 85 | | stop | Stop streaming from the webcam | 86 | | getPixelDiffThreshold | Get pixelDiffThreshold during execution | 87 | | setPixelDiffThreshold | Set pixelDiffThreshold during execution | 88 | | getScoreThreshold | Get scoreThreshold during execution | 89 | | setScoreThreshold | Set scoreThreshold during execution | 90 | 91 | ### Examples 92 | 93 | 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. 94 | -------------------------------------------------------------------------------- /diff-cam-engine.js: -------------------------------------------------------------------------------- 1 | var DiffCamEngine = (function() { 2 | var stream; // stream obtained from webcam 3 | var video; // shows stream 4 | var captureCanvas; // internal canvas for capturing full images from video 5 | var captureContext; // context for capture canvas 6 | var diffCanvas; // internal canvas for diffing downscaled captures 7 | var diffContext; // context for diff canvas 8 | var motionCanvas; // receives processed diff images 9 | var motionContext; // context for motion canvas 10 | 11 | var initSuccessCallback; // called when init succeeds 12 | var initErrorCallback; // called when init fails 13 | var startCompleteCallback; // called when start is complete 14 | var captureCallback; // called when an image has been captured and diffed 15 | 16 | var captureInterval; // interval for continuous captures 17 | var captureIntervalTime; // time between captures, in ms 18 | var captureWidth; // full captured image width 19 | var captureHeight; // full captured image height 20 | var diffWidth; // downscaled width for diff/motion 21 | var diffHeight; // downscaled height for diff/motion 22 | var isReadyToDiff; // has a previous capture been made to diff against? 23 | var pixelDiffThreshold; // min for a pixel to be considered significant 24 | var scoreThreshold; // min for an image to be considered significant 25 | var includeMotionBox; // flag to calculate and draw motion bounding box 26 | var includeMotionPixels; // flag to create object denoting pixels with motion 27 | 28 | function init(options) { 29 | // sanity check 30 | if (!options) { 31 | throw 'No options object provided'; 32 | } 33 | 34 | // incoming options with defaults 35 | video = options.video || document.createElement('video'); 36 | motionCanvas = options.motionCanvas || document.createElement('canvas'); 37 | captureIntervalTime = options.captureIntervalTime || 100; 38 | captureWidth = options.captureWidth || 640; 39 | captureHeight = options.captureHeight || 480; 40 | diffWidth = options.diffWidth || 64; 41 | diffHeight = options.diffHeight || 48; 42 | pixelDiffThreshold = options.pixelDiffThreshold || 32; 43 | scoreThreshold = options.scoreThreshold || 16; 44 | includeMotionBox = options.includeMotionBox || false; 45 | includeMotionPixels = options.includeMotionPixels || false; 46 | 47 | // callbacks 48 | initSuccessCallback = options.initSuccessCallback || function() {}; 49 | initErrorCallback = options.initErrorCallback || function() {}; 50 | startCompleteCallback = options.startCompleteCallback || function() {}; 51 | captureCallback = options.captureCallback || function() {}; 52 | 53 | // non-configurable 54 | captureCanvas = document.createElement('canvas'); 55 | diffCanvas = document.createElement('canvas'); 56 | isReadyToDiff = false; 57 | 58 | // prep video 59 | video.autoplay = true; 60 | 61 | // prep capture canvas 62 | captureCanvas.width = captureWidth; 63 | captureCanvas.height = captureHeight; 64 | captureContext = captureCanvas.getContext('2d'); 65 | 66 | // prep diff canvas 67 | diffCanvas.width = diffWidth; 68 | diffCanvas.height = diffHeight; 69 | diffContext = diffCanvas.getContext('2d'); 70 | 71 | // prep motion canvas 72 | motionCanvas.width = diffWidth; 73 | motionCanvas.height = diffHeight; 74 | motionContext = motionCanvas.getContext('2d'); 75 | 76 | requestWebcam(); 77 | } 78 | 79 | function requestWebcam() { 80 | var constraints = { 81 | audio: false, 82 | video: { width: captureWidth, height: captureHeight } 83 | }; 84 | 85 | navigator.mediaDevices.getUserMedia(constraints) 86 | .then(initSuccess) 87 | .catch(initError); 88 | } 89 | 90 | function initSuccess(requestedStream) { 91 | stream = requestedStream; 92 | initSuccessCallback(); 93 | } 94 | 95 | function initError(error) { 96 | console.log(error); 97 | initErrorCallback(); 98 | } 99 | 100 | function start() { 101 | if (!stream) { 102 | throw 'Cannot start after init fail'; 103 | } 104 | 105 | // streaming takes a moment to start 106 | video.addEventListener('canplay', startComplete); 107 | video.srcObject = stream; 108 | } 109 | 110 | function startComplete() { 111 | video.removeEventListener('canplay', startComplete); 112 | captureInterval = setInterval(capture, captureIntervalTime); 113 | startCompleteCallback(); 114 | } 115 | 116 | function stop() { 117 | clearInterval(captureInterval); 118 | video.src = ''; 119 | motionContext.clearRect(0, 0, diffWidth, diffHeight); 120 | isReadyToDiff = false; 121 | } 122 | 123 | function capture() { 124 | // save a full-sized copy of capture 125 | captureContext.drawImage(video, 0, 0, captureWidth, captureHeight); 126 | var captureImageData = captureContext.getImageData(0, 0, captureWidth, captureHeight); 127 | 128 | // diff current capture over previous capture, leftover from last time 129 | diffContext.globalCompositeOperation = 'difference'; 130 | diffContext.drawImage(video, 0, 0, diffWidth, diffHeight); 131 | var diffImageData = diffContext.getImageData(0, 0, diffWidth, diffHeight); 132 | 133 | if (isReadyToDiff) { 134 | var diff = processDiff(diffImageData); 135 | 136 | motionContext.putImageData(diffImageData, 0, 0); 137 | if (diff.motionBox) { 138 | motionContext.strokeStyle = '#fff'; 139 | motionContext.strokeRect( 140 | diff.motionBox.x.min + 0.5, 141 | diff.motionBox.y.min + 0.5, 142 | diff.motionBox.x.max - diff.motionBox.x.min, 143 | diff.motionBox.y.max - diff.motionBox.y.min 144 | ); 145 | } 146 | captureCallback({ 147 | imageData: captureImageData, 148 | score: diff.score, 149 | hasMotion: diff.score >= scoreThreshold, 150 | motionBox: diff.motionBox, 151 | motionPixels: diff.motionPixels, 152 | getURL: function() { 153 | return getCaptureUrl(this.imageData); 154 | }, 155 | checkMotionPixel: function(x, y) { 156 | return checkMotionPixel(this.motionPixels, x, y) 157 | } 158 | }); 159 | } 160 | 161 | // draw current capture normally over diff, ready for next time 162 | diffContext.globalCompositeOperation = 'source-over'; 163 | diffContext.drawImage(video, 0, 0, diffWidth, diffHeight); 164 | isReadyToDiff = true; 165 | } 166 | 167 | function processDiff(diffImageData) { 168 | var rgba = diffImageData.data; 169 | 170 | // pixel adjustments are done by reference directly on diffImageData 171 | var score = 0; 172 | var motionPixels = includeMotionPixels ? [] : undefined; 173 | var motionBox = undefined; 174 | for (var i = 0; i < rgba.length; i += 4) { 175 | var pixelDiff = rgba[i] * 0.3 + rgba[i + 1] * 0.6 + rgba[i + 2] * 0.1; 176 | var normalized = Math.min(255, pixelDiff * (255 / pixelDiffThreshold)); 177 | rgba[i] = 0; 178 | rgba[i + 1] = normalized; 179 | rgba[i + 2] = 0; 180 | 181 | if (pixelDiff >= pixelDiffThreshold) { 182 | score++; 183 | coords = calculateCoordinates(i / 4); 184 | 185 | if (includeMotionBox) { 186 | motionBox = calculateMotionBox(motionBox, coords.x, coords.y); 187 | } 188 | 189 | if (includeMotionPixels) { 190 | motionPixels = calculateMotionPixels(motionPixels, coords.x, coords.y, pixelDiff); 191 | } 192 | 193 | } 194 | } 195 | 196 | return { 197 | score: score, 198 | motionBox: score > scoreThreshold ? motionBox : undefined, 199 | motionPixels: motionPixels 200 | }; 201 | } 202 | 203 | function calculateCoordinates(pixelIndex) { 204 | return { 205 | x: pixelIndex % diffWidth, 206 | y: Math.floor(pixelIndex / diffWidth) 207 | }; 208 | } 209 | 210 | function calculateMotionBox(currentMotionBox, x, y) { 211 | // init motion box on demand 212 | var motionBox = currentMotionBox || { 213 | x: { min: coords.x, max: x }, 214 | y: { min: coords.y, max: y } 215 | }; 216 | 217 | motionBox.x.min = Math.min(motionBox.x.min, x); 218 | motionBox.x.max = Math.max(motionBox.x.max, x); 219 | motionBox.y.min = Math.min(motionBox.y.min, y); 220 | motionBox.y.max = Math.max(motionBox.y.max, y); 221 | 222 | return motionBox; 223 | } 224 | 225 | function calculateMotionPixels(motionPixels, x, y, pixelDiff) { 226 | motionPixels[x] = motionPixels[x] || []; 227 | motionPixels[x][y] = true; 228 | 229 | return motionPixels; 230 | } 231 | 232 | function getCaptureUrl(captureImageData) { 233 | // may as well borrow captureCanvas 234 | captureContext.putImageData(captureImageData, 0, 0); 235 | return captureCanvas.toDataURL(); 236 | } 237 | 238 | function checkMotionPixel(motionPixels, x, y) { 239 | return motionPixels && motionPixels[x] && motionPixels[x][y]; 240 | } 241 | 242 | function getPixelDiffThreshold() { 243 | return pixelDiffThreshold; 244 | } 245 | 246 | function setPixelDiffThreshold(val) { 247 | pixelDiffThreshold = val; 248 | } 249 | 250 | function getScoreThreshold() { 251 | return scoreThreshold; 252 | } 253 | 254 | function setScoreThreshold(val) { 255 | scoreThreshold = val; 256 | } 257 | 258 | return { 259 | // public getters/setters 260 | getPixelDiffThreshold: getPixelDiffThreshold, 261 | setPixelDiffThreshold: setPixelDiffThreshold, 262 | getScoreThreshold: getScoreThreshold, 263 | setScoreThreshold: setScoreThreshold, 264 | 265 | // public functions 266 | init: init, 267 | start: start, 268 | stop: stop 269 | }; 270 | })(); 271 | --------------------------------------------------------------------------------