|
[{"id":"6c0bc9c1bf2dce87","type":"group","z":"7b0af20c5f713b06","name":"Enrolment","style":{"fill":"#bfdbef","label":true,"label-position":"n","color":"#001f60","fill-opacity":"0.52"},"nodes":["2c0b6732f858c530","14c17278c832c532","884f7041031605f1","cd167b0d7d0b908c","72f98bb780c1075d","be1f2e46d01558b4","8c764e7d197ed066","0aea2e254c8df78f","8e93fc81dea41c59","87e32075e7e4df76","6e7f5d5b3cfe59cf","4e8a298f00527228","36927975024d6a2e","a765530bfca6fbb1","425c5148b378a7df","8d34252dfa991f98","9533185c20972174","76923780257b89d6","4a649315555d7764","d1e2af06a37fc5c8","35238f6392bfea66","66ebc2e78561999d","52f0b3cffe4a68f4","daef8343e14a521b","95f55cdfdc3de3e1"],"x":134,"y":79,"w":1392,"h":402},{"id":"2c0b6732f858c530","type":"function","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Valid face ?","func":"function settimeout() {\n // Clear previous timeout if it exists\n let activeTimeoutId = context.statusTimeoutId\n if (activeTimeoutId) {\n clearTimeout(activeTimeoutId); // Cancel the old timeout\n }\n\n // Set a new timeout to clear the status\n let timeoutId = setTimeout(() => {\n let errors = flow.get(\"errors\") || []; // Ensure errors array exists\n let color = \"red\";\n\n let noFaceCount = errors.filter(item => item.startsWith(\"No face\")).length;\n let multiFaceCount = errors.filter(item => item.startsWith(\"> 1\")).length;\n\n if (noFaceCount + multiFaceCount === 0) {\n color = \"green\";\n }\n\n const errorText = `0 Face=${noFaceCount} | >1 Face=${multiFaceCount}`;\n node.status({ fill: color, shape: \"ring\", text: errorText });\n\n context.statusTimeoutId = null; // Clear the stored timeout ID\n }, 2000);\n\n // Save only the timeout ID in the context\n context.statusTimeoutId = timeoutId;\n}\n\n// Handle msg.payload === 1\nif (msg.payload === 1) {\n settimeout();\n return msg;\n}\n\n// Construct the error message\nlet errMsg = msg.payload === 0\n ? `No face @ ${msg.faceConfig.threshold}: ${msg.filename}`\n : `> 1 face @ ${msg.faceConfig.threshold}: ${msg.filename}`;\n\n// Store the error in the flow context\nlet errors = flow.get(\"errors\") || [];\nerrors.push(errMsg);\nflow.set(\"errors\", errors);\n\n// Create a short error message\nlet shortError = errMsg.split('/')[0].trim();\nconst parts = errMsg.split('/');\nshortError += \" \" + parts.slice(-2).join('/');\n\nsettimeout();\n\n// Immediately update node status with the short error\nnode.status({ fill: \"red\", shape: \"ring\", text: shortError });\n\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":970,"y":280,"wires":[["a765530bfca6fbb1"]],"outputLabels":["1 face "]},{"id":"14c17278c832c532","type":"image","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Extracted Face 1","width":"100","data":"data.face[0]","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":615,"y":280,"wires":[["2c0b6732f858c530"]],"l":false},{"id":"884f7041031605f1","type":"file in","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Image Path","filename":"filename","filenameType":"msg","format":"","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":235,"y":280,"wires":[["95f55cdfdc3de3e1"]],"l":false},{"id":"cd167b0d7d0b908c","type":"link in","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"link in 1","links":["72f98bb780c1075d","daef8343e14a521b"],"x":175,"y":280,"wires":[["884f7041031605f1"]]},{"id":"72f98bb780c1075d","type":"link out","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"link out 2","mode":"link","links":["cd167b0d7d0b908c"],"x":1475,"y":180,"wires":[]},{"id":"be1f2e46d01558b4","type":"function","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Files List","func":"let parts = msg.path.split(\"/\"); // Split the path by \"/\"\nlet name = parts[parts.length - 2]; // The second-to-last part contains the name\nlet people = flow.get(\"people\") || {}\nlet fullPath\n\nfor (let i = 0; i < msg.file.length; i++) {\n fullPath = msg.path + msg.file[i]\n if (people[name]?.[fullPath]) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"file exists skipping\" })\n } else {\n node.status({ fill: \"green\", shape: \"ring\", text: msg.file[i] })\n msg.filename = fullPath\n msg.name = name\n node.send(msg);\n }\n\n // Clear previous timeout if it exists\n let activeTimeoutId = context.statusTimeoutId;\n if (activeTimeoutId) {\n clearTimeout(activeTimeoutId); // Cancel the old timeout\n }\n\n // Set a new timeout to clear the status\n let timeoutId = setTimeout(() => {\n node.status({});\n context.statusTimeoutId= null; // Clear the reference\n }, 10000);\n\n // Save only the timeout ID in the context\n context.statusTimeoutId= timeoutId;\n\n \n}\n\nreturn ","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1180,"y":180,"wires":[["72f98bb780c1075d"]]},{"id":"8c764e7d197ed066","type":"change","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"","rules":[{"t":"delete","p":"errors","pt":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":335,"y":220,"wires":[[]],"l":false},{"id":"0aea2e254c8df78f","type":"fs-ops-dir","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Get Directories","path":"path","pathType":"msg","filter":"*","filterType":"str","dir":"file","dirType":"msg","x":500,"y":180,"wires":[["87e32075e7e4df76"]]},{"id":"8e93fc81dea41c59","type":"inject","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Enrole","props":[{"p":"path","v":"/home/pi/facerec/people/","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":230,"y":180,"wires":[["8c764e7d197ed066","0aea2e254c8df78f"]]},{"id":"87e32075e7e4df76","type":"function","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Send Directories","func":"let path=msg.path\nmsg.file.forEach(entry => {\n msg.path = path + entry + \"/\";\n node.send(msg);\n});\nreturn","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":770,"y":180,"wires":[["6e7f5d5b3cfe59cf"]]},{"id":"6e7f5d5b3cfe59cf","type":"fs-ops-dir","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Get Image Files","path":"path","pathType":"msg","filter":"*","filterType":"str","dir":"file","dirType":"msg","x":1000,"y":180,"wires":[["be1f2e46d01558b4"]]},{"id":"4e8a298f00527228","type":"comment","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"JPEG, PNG, WebP, AVIF, GIF or TIFF image data","info":"JPEG, PNG, WebP, AVIF, GIF or TIFF image data","x":340,"y":120,"wires":[]},{"id":"36927975024d6a2e","type":"comment","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Instructions","info":"\n# Setup Instructions for Face Recognition Flow\n---\n## Initial Setup\nInstall the required nodes\n\n- [node-red-contrib-image-output](https://flows.nodered.org/node/node-red-contrib-image-output)\n- [@smcgann/node-red-annotate-image-plus](https://flows.nodered.org/node/@smcgann/node-red-annotate-image-plus)\n- [@smcgann/node-red-face-detection-plus](https://flows.nodered.org/node/@smcgann/node-red-face-detection-plus)\n- [@smcgann/node-red-face-vectorization-plus](https://flows.nodered.org/node/@smcgann/node-red-face-vectorization-plus)\n- [@smcgann/node-red-cosine-similarity-plus](https://flows.nodered.org/node/@smcgann/node-red-cosine-similarity-plus)\n- [node-red-contrib-fs-ops](https://flows.nodered.org/node/node-red-contrib-fs-ops)\n\nImport the flow into the editor\n\nCreate a base folder for the training images. Example:\n\n```sh\nmkdir -p /home/pi/facerec/people/\n```\n---\n## Creating Person Profiles\n- Within the base folder, create individual folders for each person\n- Name folders as you want the person to be identified (e.g., `/home/pi/facerec/people/Dave`)\n- Add photos of each person to their respective folders\n- **Important:** Each image should contain only one face\n---\n## Image Requirements\n- Images are internaly resized to 640x640 pixels, when detecting faces. So high-resolution images should be cropped to focus on the face (similar to passport photos).\n This ensures that the 640 pixels capture more relevant facial details.\n- Quality is more important than quantity - a few clear photos are likley to work better than many poor ones.\n- It's a good idea to include images from different angles, including from the actual camera in place if posible.\n---\n\n## Operation\n- This flow functions as a dashboard for testing and optimizing face recognition. \n- When you click **Enrole**, it iterates through the person folders and creates `flow.people` which contains names, filenames and corresponding vectors.\nSeveral nodes will display status info, so you can see what is happening during this process.\n- If you add new images to the folders, you can run **Enrole** again, existing records will be skipped.\n- After completion any problems will be logged in `flow.errors` and summarised in the `Valid face ?` status.\n- If not using persistant storeage, you can configure the cosine similarity node to read from a file. Click **Write to File** (_edit filename as required_) \n---\n\n## Troubleshooting Enrollment\nIf you encounter \"No Face\" errors:\n- Check image quality. (e.g. very low resolution, poor contrast etc)\n- Increase the **Confidence Threshold** in the FACE Detection enrollment node.\n- Try using the `YoloV8s-face` model\n\nFor > 1 face :\n- Crop images to show only one face.\n- Decrease the **Confidence Threshold** in the FACE Detection enrollment node.\n\n_Sometimes it's easier to remove problematic images rather than troubleshoot them._\n\nTo attempt to force detection, click **Fix Errors**\n- This will use the error file list and either increase or decrease threshold by 1.\n- After completion if there are still errors, you can click **Fix Errors** again.\n\n- To start fresh, click **Delete ALL** to remove `flow.people`.\n---\n\n## Testing\n- Use the inject file path for testing face identification.\n- Any image can be sent to the recognition flow.\n---\n\n## Reolink Doorbell Setup:\n\nA Specific example using Reolink -\n\n- Add your doorbell's IP address and password to the flow environent variables\n- If using FTP:\n - Configure your server for FTP access.\n - Setup the camera to FTP images on person detection.\n - Set the watch node path (e.g., `/home/pi/FTP/files/face`)\n - New images arriving will trigger the flow.\n- If using Webhook:\n - [See this thread](https://discourse.nodered.org/t/reolink-doorbell-finally-supports-webhooks/93834/73)\n\n\n\n\n\n\n","x":670,"y":120,"wires":[]},{"id":"a765530bfca6fbb1","type":"face-vectorization-plus","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"","data":"data.face","dataType":"msg","inputType":"1","returnType":"0","method":"0","path":"","x":1190,"y":280,"wires":[["425c5148b378a7df"]]},{"id":"425c5148b378a7df","type":"function","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Add vectors","func":"const name = msg.name\nnode.status({ fill: \"green\", shape: \"ring\", text: name });\n\n\n // Clear previous timeout if it exists\n let activeTimeoutId = context.statusTimeoutId;\n if (activeTimeoutId) {\n clearTimeout(activeTimeoutId); // Cancel the old timeout\n }\n\n // Set a new timeout to clear the status\n let timeoutId = setTimeout(() => {\n node.status({});\n context.statusTimeoutId= null; // Clear the reference\n }, 10000);\n\n // Save only the timeout ID in the context\n context.statusTimeoutId= timeoutId;\n\nlet people = flow.get(\"people\") || {}\n\npeople[name] = people[name] || {};\npeople[name][msg.filename] = msg.payload[0];\n\nflow.set(\"people\", people)\n\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1390,"y":280,"wires":[[]],"info":"const name = msg.config.start.split('/').pop()\r\nnode.status({ fill: \"green\", shape: \"ring\", text: name });\r\n\r\nlet images = flow.get(\"images\") || {}\r\n\r\nimages[name] = images[name] || {};\r\nimages[name][msg.filename] = msg.payload[0];\r\n\r\nflow.set(\"images\", images)\r\n\r\nmsg.trigger=true\r\n\r\nreturn msg;\r\n\r\n"},{"id":"8d34252dfa991f98","type":"inject","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Delete ALL","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":980,"y":440,"wires":[["9533185c20972174"]]},{"id":"9533185c20972174","type":"change","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"","rules":[{"t":"delete","p":"people","pt":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":1190,"y":440,"wires":[[]]},{"id":"76923780257b89d6","type":"file","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"","filename":"/home/pi/vectortest.txt","filenameType":"str","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"none","x":1400,"y":380,"wires":[[]]},{"id":"4a649315555d7764","type":"inject","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Write to File","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":990,"y":380,"wires":[["d1e2af06a37fc5c8"]]},{"id":"d1e2af06a37fc5c8","type":"change","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"flow.people","rules":[{"t":"set","p":"payload","pt":"msg","to":"people","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":1170,"y":380,"wires":[["76923780257b89d6"]]},{"id":"35238f6392bfea66","type":"function","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Image size","func":"let buffer = null;\nlet image = msg.data.face[0] // change to suit\n\nfunction isBase64(v) {\n if (v instanceof Boolean || typeof v === 'boolean') { return false }\n var regex = '(?:[A-Za-z0-9+\\\\/]{4})*(?:[A-Za-z0-9+\\\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?'\n return (new RegExp('^' + regex + '$', 'gi')).test(v)\n}\n\nif (Buffer.isBuffer(image)) {\n buffer = image;\n}\nelse {\n if (typeof image === 'string') {\n if (isBase64(image)) {\n buffer = Buffer.from(image, 'base64')\n }\n else {\n buffer = Buffer.from(image);\n }\n }\n}\n\nif (buffer) {\n var imageInfo;\n\n try {\n imageInfo = sizeOf(buffer);\n\n msg.type = imageInfo.type;\n msg.width = imageInfo.width;\n msg.height = imageInfo.height;\n\n var status = imageInfo.type + \"(\" + imageInfo.width + \"x\" + imageInfo.height + \")\";\n node.status({ fill: \"blue\", shape: \"dot\", text: status });\n }\n catch (err) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"unknown format\" });\n }\n}\nelse {\n node.status({ fill: \"red\", shape: \"dot\", text: \"invalid input\" });\n}\n\nnode.send(msg);\n\n\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"sizeOf","module":"image-size"}],"x":485,"y":280,"wires":[["14c17278c832c532"]],"l":false},{"id":"66ebc2e78561999d","type":"function","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Adjust Threshold +/-","func":"const errors = flow.get(\"errors\") || []\nif (errors.length === 0) {\n return\n}\n\nflow.set(\"errors\", undefined) // delete errors\nconst messages = errors.map(item => {\n const match = item.match(/^(.*) @ ([0-9.]+): (\\/.*)$/);\n if (match) {\n const [_, error, threshold, filePath] = match;\n return {\n error,\n threshold: parseFloat(threshold),\n filePath\n };\n }\n return null;\n}).filter(item => item !== null);\n\nfor (let index = 0; index < messages.length; index++) {\n let newThreshold = messages[index].error === \"No face\"\n ? Math.max(0.1, messages[index].threshold - 0.1) // Ensure it doesn't go below 0.1\n : Math.min(1, messages[index].threshold + 0.1); // Ensure it doesn't exceed 1\n\n msg.faceOptions = { \"threshold\": newThreshold };\n msg.filename = messages[index].filePath;\n let parts = msg.filename.split(\"/\");\n msg.name = parts[parts.length - 2]; // The second-to-last part contains the name\n msg.timestamp = Date.now();\n node.send(msg);\n}\n\n\nreturn;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":440,"wires":[["daef8343e14a521b"]]},{"id":"52f0b3cffe4a68f4","type":"inject","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"Fix Errors","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":240,"y":440,"wires":[["66ebc2e78561999d"]]},{"id":"daef8343e14a521b","type":"link out","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"link out 3","mode":"link","links":["cd167b0d7d0b908c"],"x":555,"y":440,"wires":[]},{"id":"95f55cdfdc3de3e1","type":"face-detection-plus","z":"7b0af20c5f713b06","g":"6c0bc9c1bf2dce87","name":"","returnValue":"1","model":"yolov8n-face","threshold":0.5,"absolutePathDir":"","x":360,"y":280,"wires":[["35238f6392bfea66"]]},{"id":"74ae7a292288db38","type":"group","z":"7b0af20c5f713b06","name":"Recognize","style":{"fill":"#7fb7df","label":true,"label-position":"n","color":"#001f60"},"nodes":["7e4aa4c061c5988e","f427ae706fc553b2","11642c07eb1fdc51","43f1e76a633acdc5","93b8eebeae4b4828","152c1fbaa67d6183","3319a6e68992a1de","3aaa7b229e979996","1fa9e16d1482905b","b301f0368e24bb6f","ea34b03304e2c5d9","fc3e2f906befec38","c9742ac7486f6342","0adf41d99faf7f85","359d4952a1efebc0","f1e84d202a01c5a7","b270272c9cf027a5","79f5cbaff48bc9f2","63b031854c72c56c","0b6d8eb834719276","59cfec7fbe8e2cec","55988d64d5e066cf","e5ed2520987fffc6","fe0bc58fd427638d","ae76af275195f2bf","b8d9e2df87249eb1","d736ae121ab0afb5","b54a30cd21f02a9c","db5922f8030e3ec9","20f77136a3dd435e","a135191c9cace57d","c5fe16611e479c04","59ff2a1.fa600d4","54c1e70d.ab3e18","266c286f.d993d8","c3e8aa6527c9a0ab"],"x":134,"y":559,"w":1392,"h":482},{"id":"7e4aa4c061c5988e","type":"file in","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Image Path","filename":"filename","filenameType":"msg","format":"","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":425,"y":680,"wires":[["d736ae121ab0afb5"]],"l":false},{"id":"f427ae706fc553b2","type":"image","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Extracted Face 1","width":"100","data":"data.face[0]","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":1035,"y":600,"wires":[["93b8eebeae4b4828"]],"l":false},{"id":"11642c07eb1fdc51","type":"image","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Extracted Face 3","width":"100","data":"data.face[2]","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":1415,"y":600,"wires":[["152c1fbaa67d6183"]],"l":false},{"id":"43f1e76a633acdc5","type":"image","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Extracted Face 5","width":"100","data":"data.face[4]","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":1225,"y":780,"wires":[["3319a6e68992a1de"]],"l":false},{"id":"93b8eebeae4b4828","type":"image","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Extracted Face 2","width":"100","data":"data.face[1]","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":1225,"y":600,"wires":[["11642c07eb1fdc51"]],"l":false},{"id":"152c1fbaa67d6183","type":"image","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Extracted Face 4","width":"100","data":"data.face[3]","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":1035,"y":780,"wires":[["43f1e76a633acdc5"]],"l":false},{"id":"3319a6e68992a1de","type":"image","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Extracted Face 6","width":"100","data":"data.face[5]","dataType":"msg","thumbnail":false,"active":true,"pass":true,"outputs":1,"x":1415,"y":780,"wires":[["79f5cbaff48bc9f2"]],"l":false},{"id":"3aaa7b229e979996","type":"watch","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","files":"/home/pi/FTP/files/face","recursive":"","x":260,"y":600,"wires":[["1fa9e16d1482905b"]]},{"id":"1fa9e16d1482905b","type":"switch","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","property":"event","propertyType":"msg","rules":[{"t":"eq","v":"update","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":430,"y":600,"wires":[["b301f0368e24bb6f"]]},{"id":"b301f0368e24bb6f","type":"delay","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"allowrate":false,"outputs":1,"x":570,"y":600,"wires":[["ea34b03304e2c5d9"]]},{"id":"ea34b03304e2c5d9","type":"trigger","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","op1":"","op2":"","op1type":"nul","op2type":"pay","duration":"250","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":740,"y":600,"wires":[["0b6d8eb834719276"]]},{"id":"fc3e2f906befec38","type":"function","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"add annotations","func":"var the_rects;\n\n// Get the bounding boxes (assumed to be an array with properties x, y, w, h)\nlet boxes = msg.data.boxes;\nlet payload = msg.payload;\n\n// Transform the payload to extract person name and match value\nconst namesArray = payload.map((item, index) => {\n // If the item is empty, return defaults\n if (Object.keys(item).length === 0) {\n return {\n name: \"Unknown\",\n match: 0,\n boxidx: index\n };\n }\n // Otherwise, get the person's name (first key)\n let personName = Object.keys(item)[0];\n let innerObj = item[personName];\n // If the inner object is missing or empty, use defaults\n if (!innerObj || Object.keys(innerObj).length === 0) {\n return {\n name: personName || \"Unknown\",\n match: 0,\n boxidx: index\n };\n }\n // Get the match value from the first (and only) property of innerObj\n let matchKey = Object.keys(innerObj)[0];\n let matchValue = innerObj[matchKey];\n return {\n name: personName,\n match: matchValue,\n boxidx: index // assign index as an identifier\n };\n});\n\n// Merge the namesArray with the corresponding bounding box data.\nconst mergedArray = namesArray.map((obj, index) => ({\n ...obj,\n ...boxes[index],\n boxidx: index // ensure box index is maintained\n}));\n\n// Create the rectangle annotations.\nthe_rects = mergedArray.map(x => {\n return {\n type: \"rect\",\n x: x.x || 0,\n y: x.y || 0,\n w: x.w || 0,\n h: x.h || 0,\n // Format the label with box index, person name, and match value as a percentage.\n label: `${x.boxidx} ${x.name} ${(x.match * 100).toFixed(1)}%`\n };\n});\n\nmsg.annotations = the_rects;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":920,"wires":[["f1e84d202a01c5a7"]],"info":"var the_rects;\r\n\r\nlet boxes = msg.data.boxes\r\nlet names = msg.payload\r\n\r\nconst mergedArray = names.map((obj, index) => ({\r\n ...obj,\r\n ...boxes[index],\r\n boxidx: index\r\n}));\r\n\r\nthe_rects = mergedArray.map(x => {\r\n\r\n var result = {\r\n type: \"rect\",\r\n x: x.x || 0,\r\n y: x.y || 0,\r\n w: x.w || 0,\r\n h: x.h || 0,\r\n label: x.name + \" \" + x.match +\"%\",\r\n }\r\n return result;\r\n});\r\n\r\nmsg.annotations = the_rects;\r\n\r\n\r\nreturn msg;\r\n"},{"id":"c9742ac7486f6342","type":"image","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","width":"600","data":"payload","dataType":"msg","thumbnail":false,"active":true,"pass":false,"outputs":0,"x":740,"y":1000,"wires":[]},{"id":"0adf41d99faf7f85","type":"http request","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"get image use &width=640&height=480 to get from sub stream","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":495,"y":740,"wires":[["d736ae121ab0afb5"]],"l":false},{"id":"359d4952a1efebc0","type":"inject","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Grab lo-res","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":240,"y":720,"wires":[["fe0bc58fd427638d"]]},{"id":"f1e84d202a01c5a7","type":"annotate-image-plus","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","lineWidth":"","fontSize":"","minFontSize":"10","stroke":"#ffC000","stroke-opacity":1,"fontColor":"#ff0000","fontColor-opacity":1,"textBackground":"#ffffff","textBackground-opacity":1,"data":"originImg","dataType":"msg","x":740,"y":960,"wires":[["c9742ac7486f6342"]]},{"id":"b270272c9cf027a5","type":"link in","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"link in 2","links":["79f5cbaff48bc9f2"],"x":175,"y":920,"wires":[["55988d64d5e066cf"]]},{"id":"79f5cbaff48bc9f2","type":"link out","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"link out 4","mode":"link","links":["b270272c9cf027a5"],"x":1485,"y":780,"wires":[]},{"id":"63b031854c72c56c","type":"inject","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"inject a file path","props":[{"p":"filename","v":"/home/pi/something.png","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":260,"y":680,"wires":[["7e4aa4c061c5988e"]]},{"id":"0b6d8eb834719276","type":"link out","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"link out 5","mode":"link","links":["59cfec7fbe8e2cec"],"x":845,"y":600,"wires":[]},{"id":"59cfec7fbe8e2cec","type":"link in","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"link in 3","links":["0b6d8eb834719276"],"x":325,"y":640,"wires":[["7e4aa4c061c5988e"]]},{"id":"55988d64d5e066cf","type":"face-vectorization-plus","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Face Vectorization","data":"data.face","dataType":"msg","inputType":"1","returnType":"0","method":"0","path":"","x":290,"y":920,"wires":[["b8d9e2df87249eb1","e5ed2520987fffc6"]]},{"id":"e5ed2520987fffc6","type":"cosine-similarity-plus","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Cosine Similarity","threshold":"0.5","fileType":"flow","file":"people","x":510,"y":920,"wires":[["ae76af275195f2bf","fc3e2f906befec38"]]},{"id":"fe0bc58fd427638d","type":"change","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","rules":[{"t":"set","p":"url","pt":"msg","to":"\"http://\" & $env(\"IP\") & \"/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=wuuPhkmUCeI9WG7C&user=\" & $env(\"USER\") & \"&password=\" & $env(\"PASSWORD\") &\"&width=640&height=480\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":425,"y":720,"wires":[["0adf41d99faf7f85"]],"l":false},{"id":"ae76af275195f2bf","type":"debug","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"cosine","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":470,"y":960,"wires":[]},{"id":"b8d9e2df87249eb1","type":"debug","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"vectorize","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":260,"y":960,"wires":[]},{"id":"d736ae121ab0afb5","type":"function","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Image size","func":"let buffer = null;\nlet image = msg.payload // change to suit\n\nfunction isBase64(v) {\n if (v instanceof Boolean || typeof v === 'boolean') { return false }\n var regex = '(?:[A-Za-z0-9+\\\\/]{4})*(?:[A-Za-z0-9+\\\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?'\n return (new RegExp('^' + regex + '$', 'gi')).test(v)\n}\n\nif (Buffer.isBuffer(image)) {\n buffer = image;\n}\nelse {\n if (typeof image === 'string') {\n if (isBase64(image)) {\n buffer = Buffer.from(image, 'base64')\n }\n else {\n buffer = Buffer.from(image);\n }\n }\n}\n\nif (buffer) {\n var imageInfo;\n\n try {\n imageInfo = sizeOf(buffer);\n\n var status = imageInfo.type + \"(\" + imageInfo.width + \"x\" + imageInfo.height + \")\";\n node.status({ fill: \"blue\", shape: \"dot\", text: status });\n }\n catch (err) {\n node.error(\"Unknown image format: \" + err);\n node.status({ fill: \"red\", shape: \"dot\", text: \"unknown format\" });\n }\n}\nelse {\n node.error(\"Invalid input type\");\n node.status({ fill: \"red\", shape: \"dot\", text: \"invalid input\" });\n}\n\nnode.send(msg);\n\n\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"sizeOf","module":"image-size"}],"x":565,"y":680,"wires":[["c5fe16611e479c04"]],"l":false},{"id":"b54a30cd21f02a9c","type":"inject","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Grab hi-res","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":240,"y":760,"wires":[["db5922f8030e3ec9"]]},{"id":"db5922f8030e3ec9","type":"change","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","rules":[{"t":"set","p":"url","pt":"msg","to":"\"http://\" & $env(\"IP\") & \"/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=wuuPhkmUCeI9WG7C&user=\" & $env(\"USER\") & \"&password=\" & $env(\"PASSWORD\") ","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":425,"y":760,"wires":[["0adf41d99faf7f85"]],"l":false},{"id":"20f77136a3dd435e","type":"function","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"Image size","func":"let buffer = null;\nlet image = msg.data.face[0] // change to suit\n\nfunction isBase64(v) {\n if (v instanceof Boolean || typeof v === 'boolean') { return false }\n var regex = '(?:[A-Za-z0-9+\\\\/]{4})*(?:[A-Za-z0-9+\\\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?'\n return (new RegExp('^' + regex + '$', 'gi')).test(v)\n}\n\nif (Buffer.isBuffer(image)) {\n buffer = image;\n}\nelse {\n if (typeof image === 'string') {\n if (isBase64(image)) {\n buffer = Buffer.from(image, 'base64')\n }\n else {\n buffer = Buffer.from(image);\n }\n }\n}\n\nif (buffer) {\n var imageInfo;\n\n try {\n imageInfo = sizeOf(buffer);\n\n var status = imageInfo.type + \"(\" + imageInfo.width + \"x\" + imageInfo.height + \")\";\n node.status({ fill: \"blue\", shape: \"dot\", text: status });\n }\n catch (err) {\n node.error(\"Unknown image format: \" + err);\n node.status({ fill: \"red\", shape: \"dot\", text: \"unknown format\" });\n }\n}\nelse {\n node.error(\"Invalid input type\");\n node.status({ fill: \"red\", shape: \"dot\", text: \"invalid input\" });\n}\n\nnode.send(msg);\n\n\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"sizeOf","module":"image-size"}],"x":915,"y":600,"wires":[["f427ae706fc553b2"]],"l":false},{"id":"a135191c9cace57d","type":"debug","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"face detec","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":640,"wires":[]},{"id":"c5fe16611e479c04","type":"face-detection-plus","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","returnValue":"1","model":"yolov8n-face","threshold":"0.4","absolutePathDir":"","x":740,"y":680,"wires":[["a135191c9cace57d","20f77136a3dd435e"]]},{"id":"59ff2a1.fa600d4","type":"http in","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","url":"/reolink","method":"post","upload":false,"swaggerDoc":"","x":230,"y":820,"wires":[["54c1e70d.ab3e18","c3e8aa6527c9a0ab","db5922f8030e3ec9"]]},{"id":"54c1e70d.ab3e18","type":"template","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n <head></head>\n <body>\n <h1>Hello Doorbell</h1>\n </body>\n</html>","x":410,"y":820,"wires":[["266c286f.d993d8"]]},{"id":"266c286f.d993d8","type":"http response","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"","statusCode":"","headers":{},"x":530,"y":820,"wires":[]},{"id":"c3e8aa6527c9a0ab","type":"debug","z":"7b0af20c5f713b06","g":"74ae7a292288db38","name":"webhook","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload.alarm.type","targetType":"msg","statusVal":"payload.alarm.type","statusType":"auto","x":420,"y":860,"wires":[]}] |