Object Detection using node-red-contrib-tfjs-coco-ssd

I'd be happy to add it as an example to the node as well.

Yes, very interesting. To start, could you just share the code you have in "draw Rects"?

Very nice, working fine! This topic is so interesting and challenging!

I made a small change to your first ui-template node so now I can feed it with images directly from the example of @SuperNinja via a base64 node. Just changed one line

image.src = "data:image/jpg;base64,"+myimag;

Secondly I used your grabbing setup that I now trigger automatically after a short delay

Playing a bit further, using your flow examples, I made an add-on example "hijacking" images and boxes. Resulting flow is below

[{"id":"bdfde2a1.a7e7d","type":"ui_template","z":"2d489fd9.1eedd","group":"7b6a751b.c5eb9c","name":"","order":4,"width":"1","height":"1","format":"    <script>\n    (function(scope) {\n        scope.$watch('msg', function(msg) {\n            if (msg) {\n                // Do something when msg arrives\n                let dataURL = document.querySelector(\"#canvasImage\").toDataURL('image/jpeg');\n                scope.send({payload: dataURL});\n            }\n        });\n    })(scope);\n    </script>\n","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":270,"y":1300,"wires":[["7643a96c.565538"]]},{"id":"a6b62e54.11837","type":"ui_template","z":"2d489fd9.1eedd","group":"7b6a751b.c5eb9c","name":"","order":2,"width":"7","height":"7","format":"","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":270,"y":1480,"wires":[["1431313f.5c0fbf"]]},{"id":"d9cc2a9c.01e6a8","type":"debug","z":"2d489fd9.1eedd","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":270,"y":1600,"wires":[]},{"id":"cbce6b69.9cd1e8","type":"template","z":"2d489fd9.1eedd","name":"","field":"template","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<img width=\"320\" height=\"240\" alt=\"storepic\" src={{{payload}}} />","output":"str","x":270,"y":1420,"wires":[["a6b62e54.11837"]]},{"id":"7643a96c.565538","type":"switch","z":"2d489fd9.1eedd","name":"data:image","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"data:image","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":270,"y":1360,"wires":[["cbce6b69.9cd1e8"]]},{"id":"d4e37092.e9a51","type":"link in","z":"2d489fd9.1eedd","name":"","links":["7a793272.3245fc"],"x":105,"y":1120,"wires":[["97bf55b7.ec4198"]]},{"id":"5fea2441.2f779c","type":"image","z":"2d489fd9.1eedd","name":"","width":"240","data":"payload","dataType":"msg","thumbnail":false,"active":true,"x":660,"y":1390,"wires":[]},{"id":"1431313f.5c0fbf","type":"function","z":"2d489fd9.1eedd","name":"","func":"let p = msg.payload.split(',')[1];\nmsg.payload = p;\nreturn msg;","outputs":1,"noerr":0,"x":270,"y":1540,"wires":[["5fea2441.2f779c","d9cc2a9c.01e6a8"]]},{"id":"c2aab595.729488","type":"image","z":"2d489fd9.1eedd","name":"","width":"240","data":"image","dataType":"msg","thumbnail":false,"active":true,"x":660,"y":1120,"wires":[]},{"id":"b81d2ada.cd80e8","type":"trigger","z":"2d489fd9.1eedd","op1":"","op2":"true","op1type":"nul","op2type":"bool","duration":"0.1","extend":false,"units":"s","reset":"","bytopic":"all","name":"","x":270,"y":1240,"wires":[["bdfde2a1.a7e7d"]]},{"id":"c06dcfae.24d1b","type":"ui_template","z":"2d489fd9.1eedd","group":"7b6a751b.c5eb9c","name":"","order":2,"width":"7","height":"6","format":"<canvas id=\"canvasImage\" width=\"320\" height=\"240\" style = \"border: 3px solid yellow\">\n    Your browser does not support the canvas element.\n</canvas>\n\n<script>\n    (function(scope) {\n        scope.$watch('msg', function (msg) {\n            if (msg) {\n                // Do something when msg arrives\n                function squares(canvasid, squarelist, myimag) {\n                    let canvas = document.querySelector(canvasid);\n                    let ctx = canvas.getContext('2d');\n                    ctx.scale(1, 1);\n                    let image = new Image;\n                    image.src = \"data:image/jpg;base64,\"+myimag;\n                    image.onload = function () {\n                        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);\n                        //ctx.drawImage(image, 0, 0, 1280, 720);\n                        for (const square of squarelist) {\n                            ctx.lineWidth = 3;\n                            ctx.strokeStyle = 'red';\n                            ctx.strokeRect(square.bbox[0], square.bbox[1], square.bbox[2], square.bbox[3]);\n                            //ctx.fillText(square.class,square.bbox[0],square.bbox[1]);\n                            ctx.font = \"italic 12px Calibri\";\n                            ctx.fillStyle = 'yellow';\n                            let width = ctx.measureText(square.class).width;\n                            ctx.fillRect(square.bbox[0], square.bbox[1] - 12, width, 12);\n                            ctx.fillStyle = '#000';\n                            ctx.fillText(square.class, square.bbox[0], square.bbox[1]);\n                        }\n                    };\n                }\n\n                let canvasid1 = \"#canvasImage\";\n                let detect = msg.detect;\n                let imag1  = msg.image;\n                squares(canvasid1, detect, imag1);\n            }\n        });\n})(scope);\n</script>","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":270,"y":1180,"wires":[["b81d2ada.cd80e8"]]},{"id":"97bf55b7.ec4198","type":"base64","z":"2d489fd9.1eedd","name":"","action":"str","property":"image","x":270,"y":1120,"wires":[["c06dcfae.24d1b","c2aab595.729488"]]},{"id":"7b6a751b.c5eb9c","type":"ui_group","z":"","name":"NR Image tests","tab":"a0ac7d1b.a925d","disp":true,"width":"10","collapse":false},{"id":"a0ac7d1b.a925d","type":"ui_tab","z":"","name":"XXX","icon":"dashboard","order":1,"disabled":false,"hidden":false}]
1 Like


Another little effort, you have to adapt the boxes to the size of the incomming image (at the top my Flow for reference) :wink:

@krambriw, @dceejay,

Here is the server side version (tidied up) as requested...

Demo...

Flow...

[{"id":"e57010c0.e2bd8","type":"inject","z":"55a254fe.14f63c","name":"test","topic":"loremflickr","payload":"0","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":150,"y":60,"wires":[["ddb5c73b.aaadc8"]]},{"id":"ddb5c73b.aaadc8","type":"http request","z":"55a254fe.14f63c","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://loremflickr.com/320/240/person","tls":"","persist":false,"proxy":"","authType":"","x":330,"y":60,"wires":[["c5737ffb.a7f52"]]},{"id":"c5737ffb.a7f52","type":"tensorflowCoco","z":"55a254fe.14f63c","name":"x","model":"","scoreThreshold":"","passthru":true,"x":510,"y":60,"wires":[["e1c5f9f.2f1f208"]]},{"id":"e1c5f9f.2f1f208","type":"change","z":"55a254fe.14f63c","name":"pay to detect","rules":[{"t":"move","p":"payload","pt":"msg","to":"detect","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":690,"y":60,"wires":[["fd25ec9a.7245d"]]},{"id":"fd25ec9a.7245d","type":"jimp-image","z":"55a254fe.14f63c","name":"","data":"image","dataType":"msg","ret":"img","parameter1":"200","parameter1Type":"num","parameter2":"","parameter2Type":"auto","parameter3":"RESIZE_NEAREST_NEIGHBOR","parameter3Type":"resizeMode","parameter4":"","parameter4Type":"msg","parameter5":"","parameter5Type":"msg","parameter6":"","parameter6Type":"msg","parameter7":"","parameter7Type":"msg","parameter8":"","parameter8Type":"msg","parameterCount":0,"jimpFunction":"none","selectedJimpFunction":{"name":"none","fn":"none","description":"Just loads the image.","parameters":[]},"x":150,"y":120,"wires":[["bd9fa409.372298","b723098.853eaf8","e3da3334.1278c"]]},{"id":"bd9fa409.372298","type":"image viewer","z":"55a254fe.14f63c","name":"","width":"400","data":"payload","dataType":"msg","x":270,"y":200,"wires":[[]]},{"id":"b723098.853eaf8","type":"function","z":"55a254fe.14f63c","name":"draw Rects","func":"const LINE_WIDTH = 2;\nconst LINE_COLOR = 0xED143DFF;\n\nvar jimpImage = msg.payload;\nlet imgW = jimpImage.bitmap.width;\nlet imgH = jimpImage.bitmap.height;\n\n//create clamping functions to avoid printing outside of image\nlet clampX = (val) => clamp(val,0,imgW);\nlet clampY = (val) => clamp(val,0,imgH);\n\n//add imgBatchOps to msg for drawing text in next node \nmsg.imgBatchOps = [];\n\n\nif (msg.detect.length>0) {\n    drawBoxes(jimpImage, msg.detect);\n}\n\nfunction drawBox(img, box) {\n    //get box ccords\n    let x = box.bbox[0];\n    let y = box.bbox[1];\n    let w = box.bbox[2];\n    let h = box.bbox[3];\n    \n    scanHLine(img, x, y+h-14, w, 14, 0xfa758e22); //draw box for text\n    scanRectangle(img, x, y, w, h, LINE_WIDTH, LINE_COLOR);//draw outer rect\n    \n    //built batch operations for next image node to draw text\n    msg.imgBatchOps.push(  \n        {\n            \"name\": \"print\",\n            \"parameters\": [\n                \"FONT_SANS_10_BLACK\",\n                clampX(x+2),\n                clampY(y+h-16),\n                box.class\n            ]\n        }\n        \n    );  \n}\n\nfunction drawBoxes(img, detections) {\n    detections.forEach(element => drawBox(img, element))\n}\n\n\nfunction makeColorIterator(color) {\n  return function (x, y, offset) {\n    this.bitmap.data.writeUInt32BE(color, offset, true);\n  }\n}\n\nfunction scanRectangle(image,x,y,w,h,linePX,color){\n    let iterator = makeColorIterator(color);\n   \n    let xw, yh;\n    x = clampX(x-(linePX/2));\n    y = clampY(y-(linePX/2));\n    w = clampX(w);\n    h = clampY(h);\n    xw = clampX(x+w);\n    yh = clampY(y+h);\n\n    \n    if(y+linePX <= imgH){\n        image.scan(x, y, w, linePX,  iterator);// scan linePX height line - TOP\n    }\n    if(yh+linePX <= imgH){\n        image.scan(x, yh, w, linePX, iterator);// scan linePX height line - BOTTOM\n    }\n    if(x+linePX <= imgW){\n        image.scan(x, y, linePX, h,  iterator);// scan linePX width line - LEFT\n    }\n    if(xw+linePX <= imgW){\n        image.scan(xw, y, linePX, h, iterator);// scan linePX width line - RIGHT\n    }\n    \n}\n\n\nfunction scanHLine(image,x,y,length,linePX,color){\n    let iterator = makeColorIterator(color);\n    x = clampX(x);\n    y = clampY(y);\n    length = clampX(length);\n    if(y+linePX <= imgH){\n        image.scan(x, y, length, linePX,  iterator);// \n    }\n}\n\nfunction scanVLine(image,x,y,height,linePX,color){\n    let iterator = makeColorIterator(color);\n    x = clampX(x);\n    y = clampY(y);\n    height = clampY(y+height);\n    if(x+linePX <= imgW){\n        image.scan(x, y, linePX, height,  iterator);// \n    }\n}\n\n\n//scanCircle(jimpImage, 92, 170, 43, 0x000000FF);\n//scanCircle(jimpImage, 92, 170, 42, 0xFFCC00FF);\n\nfunction clamp(value, min, max) {\n    return Math.min(Math.max(value, min), max);\n}\n\nfunction scanCircle(image, x, y, radius, color) {\n    let iterator = makeColorIterator(color);\n    return image.scan(x - radius, y - radius, radius*2, radius*2, function (pixX, pixY, idx) {\n        if (Math.pow(pixX - x, 2) + Math.pow(pixY - y, 2) < radius*radius) {\n            iterator.call(this, pixX, pixY, idx);\n        }\n    });\n}\n\n \n\nreturn msg;","outputs":1,"noerr":0,"x":410,"y":160,"wires":[["58bb5b3e.f919f4"]]},{"id":"8be04a7a.8f2ad8","type":"image viewer","z":"55a254fe.14f63c","name":"","width":"400","data":"payload","dataType":"msg","x":710,"y":200,"wires":[[]]},{"id":"58bb5b3e.f919f4","type":"jimp-image","z":"55a254fe.14f63c","name":"add text","data":"payload","dataType":"msg","ret":"img","parameter1":"imgBatchOps","parameter1Type":"msg","parameter2":"","parameter2Type":"msg","parameter3":"","parameter3Type":"msg","parameter4":"","parameter4Type":"msg","parameter5":"","parameter5Type":"msg","parameter6":"","parameter6Type":"msg","parameter7":"","parameter7Type":"msg","parameter8":"","parameter8Type":"msg","parameterCount":1,"jimpFunction":"batch","selectedJimpFunction":{"name":"batch","fn":"batch","description":"apply one or more functions","parameters":[{"name":"options","type":"json","required":true,"hint":"an object or an array of objects containing {\"name\" : \"function_name\", \"parameters\" : [x,y,z]}.  Refer to info on side panel}"}]},"x":560,"y":160,"wires":[["8be04a7a.8f2ad8","c06c130b.3795a"]]},{"id":"c06c130b.3795a","type":"jimp-image","z":"55a254fe.14f63c","name":"Save as image_with_annotations.jpg","data":"payload","dataType":"msg","ret":"img","parameter1":"image_with_annotations.jpg","parameter1Type":"str","parameter2":"","parameter2Type":"msg","parameter3":"","parameter3Type":"msg","parameter4":"","parameter4Type":"msg","parameter5":"","parameter5Type":"msg","parameter6":"","parameter6Type":"msg","parameter7":"","parameter7Type":"msg","parameter8":"","parameter8Type":"msg","parameterCount":1,"jimpFunction":"write","selectedJimpFunction":{"name":"write","fn":"write","description":"Write to file. NOTE: You can specify an alternative file extension type to change the type. Currently support types are jpg, png, bmp.","parameters":[{"name":"filename","type":"str","required":true,"hint":"Name of the file","defaultType":"str"}]},"x":810,"y":160,"wires":[[]]},{"id":"e3da3334.1278c","type":"jimp-image","z":"55a254fe.14f63c","name":"Save as image_without_annotations.jpg","data":"payload","dataType":"msg","ret":"img","parameter1":"image_without_annotations.jpg","parameter1Type":"str","parameter2":"","parameter2Type":"msg","parameter3":"","parameter3Type":"msg","parameter4":"","parameter4Type":"msg","parameter5":"","parameter5Type":"msg","parameter6":"","parameter6Type":"msg","parameter7":"","parameter7Type":"msg","parameter8":"","parameter8Type":"msg","parameterCount":1,"jimpFunction":"write","selectedJimpFunction":{"name":"write","fn":"write","description":"Write to file. NOTE: You can specify an alternative file extension type to change the type. Currently support types are jpg, png, bmp.","parameters":[{"name":"filename","type":"str","required":true,"hint":"Name of the file","defaultType":"str"}]},"x":820,"y":120,"wires":[[]]}]

NOTES

  • Uses node-red-contrib-image-tools for drawing on image
  • For anyone wondering why I seemingly duplicated efforts - I did it as a POC about 4 days - I needed to get annotated images purely server-side (i.e. headless / no browser required). Essentially I am detecting people outside the house & send annotated image to Telegram/Pushbullet) - so i needed it to run headless. This is a cut down demo.
5 Likes

Great, that is nice to have as well, many options around.

You are correct, the one previously is client-sided, I think it only works the whole way if I have the browser with the views in focus

Your server-sided solved it nicely, drawing on top of the image. I never had a clue how to do it in javascript so I desperately installed opencv and opencv4nodejs. It did work but the process of installing was very time consuming. Only advantage maybe, you could do it with less code lines :wink:

And my oldest solution doing the same is still written in Python and also using opencv

Yes, you are right, it looks awful if the picture has any other size than 320x240

1 Like

Intermediate score: :slot_machine:
Steve-Mcl is at the head of a point
Andrei follows behind and will bring us something good ...
krambriw will he go back up or throw in the towel?
:rofl:

2 Likes

Instead of cancelled formula one's and, sadly, cancelled Wimbledon....most likely cancelled French Open
Very much OT, yes

(Yes, and Roland Garros postponed to the end of September)

Hi Steve, I find that your approach for the use case and contrib-node are great. I plan to improve my home surveillance system in a near future and will certainly take the same approach (run headless and send notifications via Telegram). I just installed the contrib node to start playing with the nodes but strangely the viewer node is not appearing in the palette. I will check later today. Thank you for bringing jimp.js to Node-RED with your contrib.

Hi Andrei, you are most welcome. I've gained so much from node-red, seems only fair to contribute right?

Could you tell me what browser you're using please

Also, could you do a quick test - hold ctrl and click somewhere inside node-red (get the quick add node pop-up list) and type viewer. See if its there.

Lastly, what OS and Node Version please.

1 Like

Not a big concern about image viewer as I will be mainly using the jimp-image.

I am using Chrome (latest version).

Runtime environment:

Node-RED version: v1.0.3
Node.js version: v8.11.1
Windows_NT 10.0.18363 x64 LE
Dashboard version 2.19.2 started at /ui

Viewer is not showing up in the quick mode. I tried also searching by "image"

im-03

This is what manage palette shows me.

im-01

and this is the log after the install.

I restart node-red after installation (just in case).

Hmmm

Could be node v8 thing.

3 questions,

  • Is there any errors or warnings in browser devtools?
  • are you able to update to node V10 or 12?
  • Is the other separate image preview node installed? (Might conflict)

Hi Steve, you nailed it !

I started by uninstalling node-red-contrib-image-output and voila ! The viewer node is now available.

Good catch. Probably some conflict.

im-04

So here all together; Steve's, Andrei's and SuperNinja's contributions "hacked" and put together, showing the same image, works fine!!!

Sending the image via Telegram requires images to be binary buffers. (for Steve''s solution, the image needs to be converted as such, I suspect it is a jimp formatted image???)

EDIT: Easy enough, thanks to Steve's image node, coverting back to buffer from jimp, so easy. Now immages from Steve's solution is sent quickly via Telegram

2 Likes

4 posts were split to a new topic: How to manage the image-output / image-tools node conflict (split topic)

In case you need it, I just thought I should share how I configured the things so that images from Steve's solution can be sent via Telegram

  1. Install nodes that supports Telegram, I used node-red-contrib-telegrambot

  2. Configure your Telegram bot with your token

  3. Add some additional nodes to your flow, example in picture below

  • Add an image node that captures the analyzed image data (as shown in the preview)
  • Since the image is in jimp format, we configure the node to give us base64 back
  • Next we can decide to show the image also on a webpage, not necessary, you may skip the ui_template node
  • A split and switch node is then used so that just the base64 image data is sent further down the line
  • Telegram wants binary buffer for images so this is taken care of by the base64 node
  • Finally, the function node is composing the msg that is sent to the Telegram sender node

Personally, I decided to add the required chatId in the msg. So my code in the function node looks like this (my chatId is of course something different):

var m = { content:'', type:'', chatId:''};
m.content = msg.payload;
m.type = 'photo';
m.chatId = 123456789;
msg.payload = m;
return msg;

Happy messaging!

1 Like

Now scaling etc is working the same for both variants, also for larger images :innocent:

Happy Easter!

2 Likes

super improvement ! can you share your flow ? :slightly_smiling_face: