btsimonh's node-opencv motion detection (2017-11-02)

This flow shows use of node-opencv (from https://github.com/btsimonh/node-opencv-nr/tree/btsimonh-dev) used for motion detection on RPi3 at 5fps at 640x480. At 5fps, it uses about 25% of the RPi.

You will need to install opencv 3.3.0 (see https://btsimonh.wordpress.com/node-opencv-with-node-red/).

You will need https://flows.nodered.org/node/node-red-contrib-multipart-stream-encoder

You will need 'require' in globals.

Notes:

Press start to start the flow.

Press stop to stop the flow. make sure you stop before re-deploying, else you may find you can't restart (unable to open video).

Point a browser at /test2 for a mjpeg stream of the input video with boxes drawn for motion detected.

Point a browser at /test4 for a mjpeg stream of the output of the opencv background extractor.

FPS is hardcoded in GetImage. Motion detection frame size is set using 'width' in 'Variables'. It will work at higher framerates with smaller image sizes, and because some of the processing is Async, you can occupy more than one core of the RPi. If you push it too hard, you may start to get out-of-order images at output!.

Updated to allow global or NR local node-opencv install.

[{"id":"20c06da9.6a0b32","type":"inject","z":"7feb4cfd.5832e4","name":"Start","topic":"","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"x":150,"y":60,"wires":[["63e4c177.13bce"]]},{"id":"35689967.a95c06","type":"function","z":"7feb4cfd.5832e4","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}\n\nvar cvdesc = Object.keys(cv);\nnode.send([null, {payload:cvdesc}]);\nflow.set('cv', cv);\n\nnode.send({payload:1});\nnode.send({payload:'next'});\n","outputs":"2","noerr":0,"x":440,"y":60,"wires":[["baafd7b1.4c3be8"],[]]},{"id":"baafd7b1.4c3be8","type":"function","z":"7feb4cfd.5832e4","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":340,"y":160,"wires":[["1ce63b80.b22cd5"],["60e9ae10.2146d"],["7588699e.3e4008"]]},{"id":"4e09100a.4fddf","type":"inject","z":"7feb4cfd.5832e4","name":"Stop","topic":"","payload":"0","payloadType":"num","repeat":"","crontab":"","once":false,"x":150,"y":100,"wires":[["baafd7b1.4c3be8"]]},{"id":"a3046016.19071","type":"function","z":"7feb4cfd.5832e4","name":"BackgroundSubtraction","func":"var cv = flow.get('cv');\n\ntry {\n var bg = flow.get('bg');\n if (!bg){\n bg = cv.BackgroundSubtractor.createMOG2();\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":730,"y":120,"wires":[["48b95320.d04c6c"],["7588699e.3e4008"]]},{"id":"60e9ae10.2146d","type":"function","z":"7feb4cfd.5832e4","name":"GetNextFrame","func":"setTimeout(function(){\n node.send({payload:'next'});\n}, 10);\nreturn;","outputs":1,"noerr":0,"x":340,"y":240,"wires":[["baafd7b1.4c3be8"]]},{"id":"ac15881e.357f98","type":"multipart-encoder","z":"7feb4cfd.5832e4","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":940,"y":540,"wires":[["73d31c26.ca7114"]]},{"id":"e9b4b33f.080ab","type":"http in","z":"7feb4cfd.5832e4","name":"","url":"/test2","method":"get","upload":false,"swaggerDoc":"","x":740,"y":540,"wires":[["ac15881e.357f98"]]},{"id":"73d31c26.ca7114","type":"function","z":"7feb4cfd.5832e4","name":"","func":"flow.set('enableavg', msg.payload);\nreturn msg;","outputs":1,"noerr":0,"x":1090,"y":540,"wires":[[]]},{"id":"2aa2ff3f.a9f2d","type":"function","z":"7feb4cfd.5832e4","name":"encodetojpg","func":"var enable = flow.get('enableavg');\n\nif (1){\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":810,"y":480,"wires":[["ac15881e.357f98"]]},{"id":"48b95320.d04c6c","type":"function","z":"7feb4cfd.5832e4","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":910,"y":240,"wires":[["922d4265.3ce3b"],["7588699e.3e4008"]]},{"id":"922d4265.3ce3b","type":"function","z":"7feb4cfd.5832e4","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, {payload:0, topic:'movement'}]);\n        //node.send([null, {payload:100, topic:'movement'}]);\n        node.warn(\"movement\");\n    }\n} else {\n    if (prevmv){\n        //node.send([null, {payload:100, topic:'movement'}]);\n        //node.send([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":"3","noerr":0,"x":910,"y":300,"wires":[["fab7c15b.458"],["7588699e.3e4008"],["7588699e.3e4008"]],"outputLabels":["","movement",""]},{"id":"b70c2764.68f528","type":"ui_chart","z":"7feb4cfd.5832e4","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":490,"y":440,"wires":[[],[]]},{"id":"7588699e.3e4008","type":"function","z":"7feb4cfd.5832e4","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":320,"y":460,"wires":[["b70c2764.68f528"],["9f9e74b.eba0a88"]]},{"id":"84bd3791.1019b8","type":"inject","z":"7feb4cfd.5832e4","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":170,"y":460,"wires":[["7588699e.3e4008"]]},{"id":"63e4c177.13bce","type":"function","z":"7feb4cfd.5832e4","name":"Variables","func":"\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":300,"y":60,"wires":[["35689967.a95c06"]]},{"id":"1ce63b80.b22cd5","type":"function","z":"7feb4cfd.5832e4","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":510,"y":140,"wires":[["a3046016.19071"],["7588699e.3e4008"]]},{"id":"56bbff3c.d1b34","type":"function","z":"7feb4cfd.5832e4","name":"split","func":"node.send([msg, null]);\nnode.send([null, msg]);\n","outputs":"2","noerr":0,"x":750,"y":420,"wires":[["2aa2ff3f.a9f2d"],["61fcaeed.c043e"]]},{"id":"9255a76a.b06e18","type":"multipart-encoder","z":"7feb4cfd.5832e4","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":940,"y":640,"wires":[["b430a548.8cf558"]]},{"id":"dbbc70b5.46db8","type":"http in","z":"7feb4cfd.5832e4","name":"","url":"/test4","method":"get","upload":false,"swaggerDoc":"","x":740,"y":640,"wires":[["9255a76a.b06e18"]]},{"id":"b430a548.8cf558","type":"function","z":"7feb4cfd.5832e4","name":"","func":"flow.set('enableavg', msg.payload);\nreturn msg;","outputs":1,"noerr":0,"x":1090,"y":640,"wires":[[]]},{"id":"61fcaeed.c043e","type":"function","z":"7feb4cfd.5832e4","name":"encodetojpg","func":"var enable = flow.get('enableavg');\n\nif (1){\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":810,"y":580,"wires":[["9255a76a.b06e18"]]},{"id":"9f9e74b.eba0a88","type":"ui_chart","z":"7feb4cfd.5832e4","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":490,"y":480,"wires":[[],[]]},{"id":"fab7c15b.458","type":"function","z":"7feb4cfd.5832e4","name":"split","func":"node.send([msg, null]);\nnode.send([null, msg]);\n","outputs":"2","noerr":0,"x":750,"y":380,"wires":[["fe2e7219.4884a"],["56bbff3c.d1b34"]]},{"id":"fe2e7219.4884a","type":"function","z":"7feb4cfd.5832e4","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":950,"y":400,"wires":[["7588699e.3e4008"]]},{"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 3 weeks, 1 day ago
updated 2 weeks, 5 days ago

Node Types

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

Tags

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