├── motiondetector.js ├── node_helper.js └── diff-cam-engine.js /motiondetector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Module.register('motiondetector',{ 4 | 5 | defaults: { 6 | powerSaving: true, 7 | timeout: 120000 // 5 mins 8 | }, 9 | 10 | lastTimeMotionDetected: null, 11 | 12 | poweredOff: false, 13 | 14 | getScripts: function() { 15 | return ["diff-cam-engine.js"]; 16 | }, 17 | 18 | // Override socket notification handler. 19 | socketNotificationReceived: function(notification, payload) { 20 | if (notification === "USER_PRESENCE"){ 21 | this.sendNotification(notification, payload) 22 | } 23 | }, 24 | 25 | start: function() { 26 | Log.info('Starting module: ' + this.name); 27 | 28 | this.lastTimeMotionDetected = new Date(); 29 | 30 | var _this = this; 31 | 32 | // make sure that the monitor is on when starting 33 | _this.sendSocketNotification('MOTION_DETECTED', _this.config); 34 | 35 | 36 | var video = document.createElement('video'); 37 | var cameraPreview = document.createElement("div"); 38 | cameraPreview.id = "cameraPreview"; 39 | cameraPreview.style = "visibility:hidden;" 40 | cameraPreview.appendChild(video); 41 | 42 | var canvas = document.createElement('canvas'); 43 | 44 | DiffCamEngine.init({ 45 | video: video, 46 | motionCanvas: canvas, 47 | initSuccessCallback: function () { 48 | DiffCamEngine.start(); 49 | }, 50 | initErrorCallback: function () { 51 | console.log('error init cam engine'); 52 | }, 53 | captureCallback: function(payload){ 54 | var score = payload.score; 55 | if (score > 20) { 56 | _this.lastTimeMotionDetected = new Date(); 57 | if (_this.poweredOff) { 58 | _this.poweredOff = false; 59 | _this.sendSocketNotification('MOTION_DETECTED', _this.config); 60 | } 61 | } 62 | else { 63 | var currentDate = new Date(); 64 | var time = currentDate.getTime() - _this.lastTimeMotionDetected; 65 | if ((time > _this.config.timeout) && (!_this.poweredOff)) { 66 | _this.sendSocketNotification('DEACTIVATE_MONITOR', _this.config); 67 | _this.sendNotification('DEACTIVATE_MONITOR', _this.config); 68 | _this.poweredOff = true; 69 | } 70 | } 71 | console.log('score:' + score); 72 | } 73 | }); 74 | 75 | }, 76 | 77 | 78 | }); -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Magic Mirror 4 | * Module: MMM-PIR-Sensor 5 | * 6 | * By Paul-Vincent Roll http://paulvincentroll.com 7 | * MIT Licensed. 8 | */ 9 | 10 | const NodeHelper = require('node_helper'); 11 | const exec = require('child_process').exec; 12 | 13 | module.exports = NodeHelper.create({ 14 | start: function () { 15 | this.started = false 16 | 17 | this.isMonitorOn(function(result){ 18 | console.log("monitor on: " + result); 19 | }); 20 | }, 21 | 22 | activateMonitor: function () { 23 | var _this = this; 24 | this.isMonitorOn(function(result){ 25 | if (!result){ 26 | exec("/opt/vc/bin/tvservice --preferred && sudo chvt 6 && sudo chvt 7", null); 27 | console.log('monitor has been activated'); 28 | } 29 | 30 | }); 31 | this.started = false; 32 | }, 33 | 34 | deactivateMonitor: function () { 35 | this.isMonitorOn(function(result){ 36 | if (result){ 37 | exec("/opt/vc/bin/tvservice -o", null); 38 | console.log('monitor has been deactivated'); 39 | } 40 | 41 | }); 42 | 43 | this.started = false; 44 | }, 45 | 46 | isMonitorOn: function(resultCallback){ 47 | //exec("/opt/vc/bin/tvservice -o", null); 48 | 49 | exec('/opt/vc/bin/tvservice -s', function(err, out, code) { 50 | var e = err; 51 | var o = out; 52 | var c = code; 53 | 54 | if (out.indexOf('0x120002') > 0) { 55 | resultCallback(false); 56 | } 57 | else { 58 | resultCallback(true); 59 | } 60 | 61 | console.log("monitor :" + out); 62 | 63 | }); 64 | }, 65 | // Subclass socketNotificationReceived received. 66 | socketNotificationReceived: function (notification, payload) { 67 | const self = this; 68 | if (notification === 'MOTION_DETECTED' && this.started == false) { 69 | const self = this; 70 | this.started = true; 71 | this.activateMonitor(); 72 | } 73 | 74 | if (notification === 'DEACTIVATE_MONITOR' && this.started == false) { 75 | const self = this; 76 | this.started = true; 77 | this.deactivateMonitor(); 78 | } 79 | } 80 | 81 | }); -------------------------------------------------------------------------------- /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 self = this; 81 | (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia).call( 82 | navigator, 83 | {video: true}, 84 | function(localMediaStream) { 85 | if(video) { 86 | var vendorURL = window.URL || window.webkitURL; 87 | 88 | if (navigator.mozGetUserMedia) { 89 | video.mozSrcObject = localMediaStream; 90 | video.play(); 91 | } else { 92 | video.src = vendorURL.createObjectURL(localMediaStream); 93 | } 94 | initSuccess(localMediaStream); 95 | } 96 | }, 97 | console.error 98 | ); 99 | } 100 | 101 | // function requestWebcam() { 102 | // var constraints = { 103 | // audio: false, 104 | // video: { width: captureWidth, height: captureHeight } 105 | // }; 106 | 107 | // navigator.getUserMedia(constraints) 108 | // .then(initSuccess) 109 | // .catch(initError); 110 | // } 111 | 112 | function initSuccess(requestedStream) { 113 | stream = requestedStream; 114 | initSuccessCallback(); 115 | } 116 | 117 | function initError(error) { 118 | console.log(error); 119 | initErrorCallback(); 120 | } 121 | 122 | function start() { 123 | if (!stream) { 124 | throw 'Cannot start after init fail'; 125 | } 126 | 127 | // streaming takes a moment to start 128 | video.addEventListener('canplay', startComplete); 129 | video.srcObject = stream; 130 | } 131 | 132 | function startComplete() { 133 | video.removeEventListener('canplay', startComplete); 134 | captureInterval = setInterval(capture, captureIntervalTime); 135 | startCompleteCallback(); 136 | } 137 | 138 | function stop() { 139 | clearInterval(captureInterval); 140 | video.src = ''; 141 | motionContext.clearRect(0, 0, diffWidth, diffHeight); 142 | isReadyToDiff = false; 143 | } 144 | 145 | function capture() { 146 | // save a full-sized copy of capture 147 | captureContext.drawImage(video, 0, 0, captureWidth, captureHeight); 148 | var captureImageData = captureContext.getImageData(0, 0, captureWidth, captureHeight); 149 | 150 | // diff current capture over previous capture, leftover from last time 151 | diffContext.globalCompositeOperation = 'difference'; 152 | diffContext.drawImage(video, 0, 0, diffWidth, diffHeight); 153 | var diffImageData = diffContext.getImageData(0, 0, diffWidth, diffHeight); 154 | 155 | if (isReadyToDiff) { 156 | var diff = processDiff(diffImageData); 157 | 158 | motionContext.putImageData(diffImageData, 0, 0); 159 | if (diff.motionBox) { 160 | motionContext.strokeStyle = '#fff'; 161 | motionContext.strokeRect( 162 | diff.motionBox.x.min + 0.5, 163 | diff.motionBox.y.min + 0.5, 164 | diff.motionBox.x.max - diff.motionBox.x.min, 165 | diff.motionBox.y.max - diff.motionBox.y.min 166 | ); 167 | } 168 | captureCallback({ 169 | imageData: captureImageData, 170 | score: diff.score, 171 | hasMotion: diff.score >= scoreThreshold, 172 | motionBox: diff.motionBox, 173 | motionPixels: diff.motionPixels, 174 | getURL: function() { 175 | return getCaptureUrl(this.imageData); 176 | }, 177 | checkMotionPixel: function(x, y) { 178 | return checkMotionPixel(this.motionPixels, x, y) 179 | } 180 | }); 181 | } 182 | 183 | // draw current capture normally over diff, ready for next time 184 | diffContext.globalCompositeOperation = 'source-over'; 185 | diffContext.drawImage(video, 0, 0, diffWidth, diffHeight); 186 | isReadyToDiff = true; 187 | } 188 | 189 | function processDiff(diffImageData) { 190 | var rgba = diffImageData.data; 191 | 192 | // pixel adjustments are done by reference directly on diffImageData 193 | var score = 0; 194 | var motionPixels = includeMotionPixels ? [] : undefined; 195 | var motionBox = undefined; 196 | for (var i = 0; i < rgba.length; i += 4) { 197 | var pixelDiff = rgba[i] * 0.3 + rgba[i + 1] * 0.6 + rgba[i + 2] * 0.1; 198 | var normalized = Math.min(255, pixelDiff * (255 / pixelDiffThreshold)); 199 | rgba[i] = 0; 200 | rgba[i + 1] = normalized; 201 | rgba[i + 2] = 0; 202 | 203 | if (pixelDiff >= pixelDiffThreshold) { 204 | score++; 205 | coords = calculateCoordinates(i / 4); 206 | 207 | if (includeMotionBox) { 208 | motionBox = calculateMotionBox(motionBox, coords.x, coords.y); 209 | } 210 | 211 | if (includeMotionPixels) { 212 | motionPixels = calculateMotionPixels(motionPixels, coords.x, coords.y, pixelDiff); 213 | } 214 | 215 | } 216 | } 217 | 218 | return { 219 | score: score, 220 | motionBox: score > scoreThreshold ? motionBox : undefined, 221 | motionPixels: motionPixels 222 | }; 223 | } 224 | 225 | function calculateCoordinates(pixelIndex) { 226 | return { 227 | x: pixelIndex % diffWidth, 228 | y: Math.floor(pixelIndex / diffWidth) 229 | }; 230 | } 231 | 232 | function calculateMotionBox(currentMotionBox, x, y) { 233 | // init motion box on demand 234 | var motionBox = currentMotionBox || { 235 | x: { min: coords.x, max: x }, 236 | y: { min: coords.y, max: y } 237 | }; 238 | 239 | motionBox.x.min = Math.min(motionBox.x.min, x); 240 | motionBox.x.max = Math.max(motionBox.x.max, x); 241 | motionBox.y.min = Math.min(motionBox.y.min, y); 242 | motionBox.y.max = Math.max(motionBox.y.max, y); 243 | 244 | return motionBox; 245 | } 246 | 247 | function calculateMotionPixels(motionPixels, x, y, pixelDiff) { 248 | motionPixels[x] = motionPixels[x] || []; 249 | motionPixels[x][y] = true; 250 | 251 | return motionPixels; 252 | } 253 | 254 | function getCaptureUrl(captureImageData) { 255 | // may as well borrow captureCanvas 256 | captureContext.putImageData(captureImageData, 0, 0); 257 | return captureCanvas.toDataURL(); 258 | } 259 | 260 | function checkMotionPixel(motionPixels, x, y) { 261 | return motionPixels && motionPixels[x] && motionPixels[x][y]; 262 | } 263 | 264 | function getPixelDiffThreshold() { 265 | return pixelDiffThreshold; 266 | } 267 | 268 | function setPixelDiffThreshold(val) { 269 | pixelDiffThreshold = val; 270 | } 271 | 272 | function getScoreThreshold() { 273 | return scoreThreshold; 274 | } 275 | 276 | function setScoreThreshold(val) { 277 | scoreThreshold = val; 278 | } 279 | 280 | return { 281 | // public getters/setters 282 | getPixelDiffThreshold: getPixelDiffThreshold, 283 | setPixelDiffThreshold: setPixelDiffThreshold, 284 | getScoreThreshold: getScoreThreshold, 285 | setScoreThreshold: setScoreThreshold, 286 | 287 | // public functions 288 | init: init, 289 | start: start, 290 | stop: stop 291 | }; 292 | })(); --------------------------------------------------------------------------------