Skip to content

Instantly share code, notes, and snippets.

@btsimonh
Last active November 5, 2017 09:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save btsimonh/c172899be094e2cf37a92f32b7c47635 to your computer and use it in GitHub Desktop.
Save btsimonh/c172899be094e2cf37a92f32b7c47635 to your computer and use it in GitHub Desktop.
node-opencv motion to video

This flow demonstrates use of node-opencv to detect motion using BackgroundSubtractor, and record an AVI file containing MP4 video of each motion event on an RPi3.

Not for everyone; follow https://btsimonh.wordpress.com/node-opencv-with-node-red/ for installation instructions of what is required; plus you'll need global require and node-red-contrib-multipart-stream-encoder.

Videos are written to /home/pi as AVI.

This WILL burn your SD card if left active!!!!

example output: https://youtu.be/Z5zKLVfHx50

2017-11-05 Updated to allow for global or NR local node-opencv installation

[{"id":"db974bc.6a054b8","type":"inject","z":"3a03b882.acc878","name":"Start","topic":"","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"x":130,"y":60,"wires":[["8e1708c7.b4bf68"]]},{"id":"9b7e5227.94d31","type":"function","z":"3a03b882.acc878","name":"LoadCV","func":"var require = global.get('require');\nif (!require){\n node.error(\"require not found in globals - see https://btsimonh.wordpress.com/node-opencv-with-node-red/ for installation notes\")\n return;\n}\n\n// look for globally installed opencv\nvar cv = require.main.require('opencv');\nif (!cv){\n // look for locally installed opencv\n cv = require('opencv');\n}\nif (!cv){\n node.error(\"opencv not found - see https://btsimonh.wordpress.com/node-opencv-with-node-red/ for installation notes\")\n return;\n}\nvar cvdesc = Object.keys(cv);\nnode.send([null, {payload:cvdesc}]);\nflow.set('cv', cv);\n\n var bg = flow.get('bg');\n if (bg){\n delete bg;\n bg = null;\n }\n \n bg = cv.BackgroundSubtractor.createMOG();\n node.warn(\"made bg \"+util.inspect(bg));\n flow.set('bg', bg);\n \n\nnode.send({payload:1});\nnode.send({payload:'next'});\n","outputs":"2","noerr":0,"x":420,"y":60,"wires":[["aaeee634.93dc28"],[]]},{"id":"aaeee634.93dc28","type":"function","z":"3a03b882.acc878","name":"GetImage","func":"\n\nif (msg.payload === 0){\n flow.set('state', 'stop');\n \n // in case we never hit if(stop), delete in 200ms\n // else can't open camera ever again. \n setTimeout(function(){\n var vid = flow.get('cvvid');\n if (vid){\n // stopping, so delete vid whilst we're not using it.\n node.warn(util.inspect(vid));\n vid.release();\n flow.set('cvvid', null);\n delete vid;\n }\n }, 200);\n}\n\n\nif (msg.payload === 1){\n try{\n flow.set('state', 'run');\n flow.set('start', null);\n flow.set('count', null);\n flow.set('last_s', null);\n \n var cv = flow.get('cv');\n var timings = flow.get('timings') || {};\n timings.startup = {};\n timings.startup.start = Date.now();\n var vid = new cv.VideoCapture(0);\n var fps = vid.setFPS(5);\n node.warn(fps);\n timings.startup.end = Date.now();\n timings.startup.diff = timings.startup.end - timings.startup.start;\n \n node.warn(vid);\n flow.set('cvvid', vid);\n } catch (e){\n node.warn(e);\n }\n}\n\nif (msg.payload === 'ack'){\n var timings = flow.get('timings');\n timings.imagecidiff = context.imageci - msg.imageci;\n return;\n}\n\n\nif (msg.payload === 'next'){\n var vid = flow.get('cvvid');\n \n if (vid){\n try{\n //node.warn(\"grabbed \" + util.inspect(err) + \" \"+util.inspect(im));\n vid.read(function(err, im){\n try{\n var state = flow.get(\"state\");\n switch(state){\n case 'stop':\n // stopping, so delete vid whilst we're not using it.\n node.warn(util.inspect(vid));\n vid.release();\n flow.set('cvvid', null);\n delete vid;\n return;\n break;\n default:\n break\n }\n \n if (err) {\n node.warn(\"read \" + util.inspect(err) + \" \"+util.inspect(im));\n return;\n }\n \n if ((im.size()[0] === 0) && (im.size()[1] === 0)){\n node.warn(\"image has zero width or height\");\n return; \n }\n \n msg.camera = im;\n msg.flowstarttime = new Date();\n\n context.lasttime = context.lasttime || 0;\n\n node.send([null, {payload:0}, null]);\n\n var frametime = Date.now();\n if (context.lasttime){\n node.send([null, null, {payload:frametime - context.lasttime, topic:\"inputframeinterval\"}]);\n }\n context.lasttime = frametime;\n \n node.send(msg);\n } catch(e){\n node.warn(e);\n }\n });\n } catch (e){\n node.warn(e);\n }\n }\n}\n","outputs":"3","noerr":0,"x":320,"y":160,"wires":[["71b1ba0c.62ede4"],["aa19908a.955b9"],["c850c0a.56d754"]]},{"id":"311d94ab.4095ac","type":"inject","z":"3a03b882.acc878","name":"Stop","topic":"","payload":"0","payloadType":"num","repeat":"","crontab":"","once":false,"x":130,"y":100,"wires":[["aaeee634.93dc28"]]},{"id":"740e93da.4de76c","type":"function","z":"3a03b882.acc878","name":"BackgroundSubtraction","func":"var cv = flow.get('cv');\n\ntry {\n var bg = flow.get('bg');\n if (!bg){\n bg = cv.BackgroundSubtractor.createMOG();\n node.warn(\"made bg \"+util.inspect(bg));\n flow.set('bg', bg);\n }\n \n if (bg){\n var bgstart = (new Date()).valueOf();\n bg.apply(msg.img, function(err, mat){\n //node.warn(\"did mog \"+err+\" \"+util.inspect(mat));\n try{\n var bgend = (new Date()).valueOf();\n var bgtime = bgend - bgstart;\n node.send([null, {payload:bgtime, topic:\"bgtime\"}]);\n \n msg.out = mat;\n //node.warn(\"did mog:\"+util.inspect(mat));\n \n node.send(msg);\n //msg.avg.release(); \n return;\n } catch(e){\n node.warn(\"exception\"+e);\n }\n });\n }\n \n} catch(e){\n node.warn(e);\n}\n\nreturn;\n\nreturn msg;","outputs":"2","noerr":0,"x":910,"y":120,"wires":[["736e4f1f.510d9"],["c850c0a.56d754"]]},{"id":"aa19908a.955b9","type":"function","z":"3a03b882.acc878","name":"GetNextFrame","func":"setTimeout(function(){\n node.send({payload:'next'});\n}, 10);\nreturn;","outputs":1,"noerr":0,"x":320,"y":240,"wires":[["aaeee634.93dc28"]]},{"id":"c3397585.78a1e8","type":"multipart-encoder","z":"3a03b882.acc878","name":"","statusCode":"","ignoreMessages":true,"outputOneNew":true,"outputOneClosed":true,"outputAllClosed":true,"globalHeaders":{"Content-Type":"multipart/x-mixed-replace;boundary=myboundary","Connection":"keep-alive","Expires":"Fri, 01 Jan 1990 00:00:00 GMT","Cache-Control":"no-cache, no-store, max-age=0, must-revalidate","Pragma":"no-cache"},"partHeaders":{"Content-Type":"image/jpeg"},"destination":"all","x":920,"y":540,"wires":[["506805c5.d0779c"]]},{"id":"f32c1d53.5172a","type":"http in","z":"3a03b882.acc878","name":"","url":"/test2","method":"get","upload":false,"swaggerDoc":"","x":720,"y":540,"wires":[["c3397585.78a1e8"]]},{"id":"506805c5.d0779c","type":"function","z":"3a03b882.acc878","name":"","func":"flow.set('enableavg', msg.payload);\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":540,"wires":[[]]},{"id":"7131f628.0561f8","type":"function","z":"3a03b882.acc878","name":"encodetojpg","func":"var enable = flow.get('enableavg');\n\nif (enable){\n var av = msg.camera;\n //node.warn(util.inspect(av));\n var d = av.toBuffer();\n var newmsg = {\n payload: d\n };\nnode.send(newmsg);\n}\n","outputs":1,"noerr":0,"x":790,"y":480,"wires":[["c3397585.78a1e8"]]},{"id":"736e4f1f.510d9","type":"function","z":"3a03b882.acc878","name":"GetContours","func":"var cv = flow.get('cv');\n\nvar starttime = new Date();\nvar cnts = msg.out.findContours(cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);\nmsg.cnts = cnts;\n\nnode.send([null, {payload:(new Date()) - starttime, topic:\"findcontours\"}]);\n\nreturn msg;","outputs":"2","noerr":0,"x":890,"y":240,"wires":[["e091cc42.4b426"],["c850c0a.56d754"]]},{"id":"e091cc42.4b426","type":"function","z":"3a03b882.acc878","name":"Draw Boxes","func":"\nvar numcnts = msg.cnts.size();\n//var area = flow.get(\"area\");\n\nvar movementfound = false;\nvar area = flow.get('area');\n\nvar starttime = new Date();\nif (numcnts){\n var scalex = msg.camera.width()/msg.out.width();\n var scaley = msg.camera.height()/msg.out.height();;\n for (var c = 0; c < numcnts; c++){\n if (msg.cnts.area(c) > area){\n var bounding = msg.cnts.boundingRect(c);\n //node.error(bounding);\n \n // draw onto original camera image\n msg.camera.rectangle([bounding.x*scalex, bounding.y*scaley], [bounding.width*scalex, bounding.height*scaley], [0, 0, 255], 2);\n movementfound = true;\n }\n }\n}\nnode.send([null, null, {payload:(new Date()) - starttime, topic:\"drawboxes\"}]);\n\n\nmsg.movement = movementfound;\n\nvar prevmv = flow.get('prevmv');\nif(movementfound){\n node.send([null, {payload:100, topic:'movement'}]);\n if (!prevmv){\n node.send([null, null, null, {payload:100, topic:'movement'}]);\n node.warn(\"movement\");\n }\n} else {\n if (prevmv){\n node.send([null, null, null, {payload:0, topic:'movement'}]);\n node.warn(\"no movement\");\n }\n node.send([null, {payload:0, topic:'movement'}]);\n}\n\nflow.set('prevmv', movementfound);\n\nreturn msg;\n","outputs":"4","noerr":0,"x":890,"y":300,"wires":[["3507a39c.cdb64c"],["c850c0a.56d754"],["c850c0a.56d754"],["aa5b9536.236bf8"]],"outputLabels":["","movement",""]},{"id":"e983df18.e605","type":"ui_chart","z":"3a03b882.acc878","name":"","group":"85a18620.7d76d8","order":0,"width":"6","height":"11","label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"200","removeOlder":"2","removeOlderPoints":"100","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"x":470,"y":440,"wires":[[],[]]},{"id":"c850c0a.56d754","type":"function","z":"3a03b882.acc878","name":"","func":"context.values = context.values || {};\ncontext.avgs = context.avgs || {};\nif (msg.topic){\n context.values = context.values || {};\n context.values[msg.topic] = msg.payload;\n context.avgs = context.avgs || {};\n if (!context.avgs[msg.topic+'avg']){\n context.avgs[msg.topic+'avg'] = msg.payload;\n } else {\n context.avgs[msg.topic+'avg'] *= 0.95;\n context.avgs[msg.topic+'avg'] += msg.payload*0.05;\n }\n return;\n}\n\nvar keys = Object.keys(context.values);\nfor (var i=0; i < keys.length; i++){\n var newmsg = {\n payload:context.values[keys[i]],\n topic:keys[i]\n };\n node.send([newmsg, null]);\n \n}\n\nvar keys = Object.keys(context.avgs);\nfor (var i=0; i < keys.length; i++){\n var newmsg = {\n payload:context.avgs[keys[i]],\n topic:keys[i]\n };\n node.send([null, newmsg]);\n \n}","outputs":"2","noerr":0,"x":300,"y":460,"wires":[["e983df18.e605"],["a83cedd5.25dff"]]},{"id":"3816a3fd.2f44cc","type":"inject","z":"3a03b882.acc878","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":150,"y":460,"wires":[["c850c0a.56d754"]]},{"id":"8e1708c7.b4bf68","type":"function","z":"3a03b882.acc878","name":"Variables","func":"\n\nflow.set(\"writerframes\", 200);\nflow.set(\"writerframe\", -1);\n\nflow.set(\"area\", 200);\nflow.set(\"width\", 640);\nflow.set(\"height\", ((flow.get('width')*3/4/2)>>0)*2);\n\nvar timings = {};\nflow.set('timings', timings);\n\nreturn msg;","outputs":1,"noerr":0,"x":280,"y":60,"wires":[["9b7e5227.94d31"]]},{"id":"71b1ba0c.62ede4","type":"function","z":"3a03b882.acc878","name":"resize","func":"\nvar starttime = new Date();\nvar AfterResize = function(err, img){\n try{\n var resizetime = (new Date()) - starttime;\n node.send([null, {payload:resizetime, topic:\"resizetime\"}]);\n msg.img = img;\n node.send([msg, null]);\n } catch (e) {\n node.warn(e);\n }\n};\n\ntry{\nvar Async = true;\nif (Async){\n // note - generates a new image\n msg.camera.resize(flow.get('width'), flow.get('height'), AfterResize);\n} else {\n // sync - note - modifies the input image\n msg.camera.resize(flow.get('width'), flow.get('height'));\n AfterResize(null, msg.camera);\n}\n} catch (e) {\n node.warn(e);\n}\n\n","outputs":"2","noerr":0,"x":490,"y":140,"wires":[["740e93da.4de76c"],["c850c0a.56d754"]]},{"id":"66e276e4.adb168","type":"function","z":"3a03b882.acc878","name":"split","func":"node.send([msg, null]);\nnode.send([null, msg]);\n","outputs":"2","noerr":0,"x":730,"y":420,"wires":[["7131f628.0561f8"],["cc44a7b7.97d9f8"]]},{"id":"8ab0bd20.e8e58","type":"multipart-encoder","z":"3a03b882.acc878","name":"","statusCode":"","ignoreMessages":true,"outputOneNew":true,"outputOneClosed":true,"outputAllClosed":true,"globalHeaders":{"Content-Type":"multipart/x-mixed-replace;boundary=myboundary","Connection":"keep-alive","Expires":"Fri, 01 Jan 1990 00:00:00 GMT","Cache-Control":"no-cache, no-store, max-age=0, must-revalidate","Pragma":"no-cache"},"partHeaders":{"Content-Type":"image/jpeg"},"destination":"all","x":920,"y":640,"wires":[["4866c6d9.f3eda8"]]},{"id":"65123976.17d278","type":"http in","z":"3a03b882.acc878","name":"","url":"/mvmt","method":"get","upload":false,"swaggerDoc":"","x":720,"y":640,"wires":[["8ab0bd20.e8e58"]]},{"id":"4866c6d9.f3eda8","type":"function","z":"3a03b882.acc878","name":"","func":"flow.set('enablepic', msg.payload);\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":640,"wires":[[]]},{"id":"5f4454e.b3a51ac","type":"function","z":"3a03b882.acc878","name":"encodetojpg","func":"var enable = flow.get('enablepic');\n\nif (enable){\n //var av = flow.get('mask1');//msg.out;\n var av = msg.out;\n //node.warn(util.inspect(av));\n var d = av.toBuffer();\n var newmsg = {\n payload: d\n };\nnode.send(newmsg);\n}\n","outputs":1,"noerr":0,"x":790,"y":580,"wires":[["8ab0bd20.e8e58"]]},{"id":"a83cedd5.25dff","type":"ui_chart","z":"3a03b882.acc878","name":"","group":"96dbe569.b5e008","order":0,"width":"6","height":"11","label":"avgs","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"200","removeOlder":"2","removeOlderPoints":"100","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"x":470,"y":480,"wires":[[],[]]},{"id":"3507a39c.cdb64c","type":"function","z":"3a03b882.acc878","name":"split","func":"node.send([msg, null]);\nnode.send([null, msg]);\n","outputs":"2","noerr":0,"x":730,"y":380,"wires":[["5281b427.b385dc"],["66e276e4.adb168"]]},{"id":"5281b427.b385dc","type":"function","z":"3a03b882.acc878","name":"endtime","func":"var endtime = new Date();\n\nvar newmsg = {\n payload:endtime - msg.flowstarttime,\n topic:'flowtime'\n}\n\nreturn newmsg;","outputs":1,"noerr":0,"x":930,"y":400,"wires":[["c850c0a.56d754"]]},{"id":"b000771b.4218b8","type":"function","z":"3a03b882.acc878","name":"GetMask","func":"var cv = flow.get('cv');\n\ncv.readImage(\"/home/pi/mask.png\", function(err, img){\n try {\n if (err){\n node.warn(err);\n return;\n }\n \n img.resize(flow.get('width'),flow.get('height'));\n\n node.warn(util.inspect(img));\n node.warn('before not')\n\n var lower = [0,0,0];\n var upper = [255,255,255];\n\n //img.inRange(lower, upper);\n\n var mask = img.copy();\n \n \n img.bitwiseNot(img);\n node.warn('notted')\n \n node.warn('resized');\n \n var oldmask = flow.get('mask');\n if (oldmask) oldmask.release();\n flow.set('mask1', mask);\n flow.set('mask', null);\n var notmask = flow.get('notmask');\n if (notmask) notmask.release();\n flow.set('notmask', img);\n \n node.send(msg); \n node.warn(\"sent mask\");\n } catch (e) {\n node.warn(e);\n }\n});\n\n","outputs":1,"noerr":0,"x":360,"y":100,"wires":[["aaeee634.93dc28"]]},{"id":"5369f5cf.84386c","type":"function","z":"3a03b882.acc878","name":"MaskImage","func":"\nvar cv = flow.get('cv');\nvar notmask = flow.get('notmask');\nvar mask = flow.get('mask');\n\n// just set the area of the mask too the mask value\n// this will then be interpreted s part of the background\nif (mask){\n msg.img.setWithMask([127,127,127], mask);\n node.warn(\"set mask\");\n}\nreturn msg;\n","outputs":1,"noerr":0,"x":650,"y":80,"wires":[["740e93da.4de76c"]]},{"id":"14c1abc.99efa54","type":"function","z":"3a03b882.acc878","name":"VideoWriter","func":"\nvar writerframe = flow.get('writerframe');\nvar writerframes = flow.get('writerframes');\n\nif (writerframe >= 0){\n //node.warn(\"writerframe \"+ writerframe);\n \n var writer = flow.get('writer');\n //node.warn(writerframe);\n \n if (writerframe === 0){\n // start writer\n var cv = flow.get('cv');\n var filename = '/home/pi/output-'+new Date().getTime()+'.avi';\n var FPS = 5;\n writer = new cv.VideoWriter(filename, 'MP42' /*'DIVX'*/, FPS, msg.camera.size(), true); \n flow.set('writer', writer);\n node.warn(\"started writer\"+util.inspect(writer));\n }\n\n if (writer){\n writerframe++;\n flow.set('writerframe', writerframe);\n writer.writeSync(msg.camera);\n }\n\n if (writerframe >= writerframes){\n // stop writer\n if (writer){\n writer.release();\n flow.set('writer', null);\n node.warn(\"stopped writer\");\n }\n flow.set('writerframe', -1);\n }\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":1030,"y":460,"wires":[[]]},{"id":"cc44a7b7.97d9f8","type":"function","z":"3a03b882.acc878","name":"split","func":"node.send([msg, null]);\nnode.send([null, msg]);\n","outputs":"2","noerr":0,"x":850,"y":440,"wires":[["5f4454e.b3a51ac"],["14c1abc.99efa54"]]},{"id":"aa5b9536.236bf8","type":"function","z":"3a03b882.acc878","name":"start-stop-VideoRecord","func":"if (msg.payload){\n var writerframe = flow.get('writerframe');\n node.warn(\"movement started \"+writerframe);\n if (writerframe < 0) {\n // start writer\n flow.set('writerframe', 0);\n writerframe = flow.get('writerframe');\n node.warn(\"set writerframe to \"+writerframe);\n }\n} else {\n var writerframe = flow.get('writerframe');\n node.warn(\"movement stopped \"+writerframe);\n if (writerframe >= 0) {\n // start writer\n var writerframes = flow.get('writerframes');\n flow.set('writerframe', writerframes);\n writerframe = flow.get('writerframe');\n node.warn(\"set writerframe to \"+writerframe);\n }\n \n}\nreturn msg;","outputs":1,"noerr":0,"x":1140,"y":340,"wires":[[]]},{"id":"85a18620.7d76d8","type":"ui_group","z":"","name":"Default","tab":"82abaa61.322da8","disp":true,"width":"6"},{"id":"96dbe569.b5e008","type":"ui_group","z":"","name":"avgs","tab":"82abaa61.322da8","disp":true,"width":"6"},{"id":"82abaa61.322da8","type":"ui_tab","z":"","name":"newstuff","icon":"dashboard"}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment