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"}]
btsimonh

Flow Info

created 2 weeks, 6 days ago
updated 2 weeks, 5 days ago

Node Types

Core
  • function (x21)
  • http in (x2)
  • inject (x3)
Other

Tags

  • node-opencv
  • opencv
  • video
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option