Face Recognition - using @smcgann/node-red-face-detection-plus

Setup Instructions for Face Recognition Flow


Initial Setup

Install the required nodes

Import the flow into the editor

Create a base folder for the training images. Example:

mkdir -p /home/pi/facerec/people/

Creating Person Profiles

  • Within the base folder, create individual folders for each person
  • Name folders as you want the person to be identified (e.g., /home/pi/facerec/people/Dave)
  • Add photos of each person to their respective folders
  • Important: Each image should contain only one face

Image Requirements

  • 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). This ensures that the 640 pixels capture more relevant facial details.
  • Quality is more important than quantity - a few clear photos are likley to work better than many poor ones.
  • It's a good idea to include images from different angles, including from the actual camera in place if posible.

Operation

  • This flow functions as a dashboard for testing and optimizing face recognition.
  • When you click Enrole, it iterates through the person folders and creates flow.people which contains names, filenames and corresponding vectors. Several nodes will display status info, so you can see what is happening during this process.
  • If you add new images to the folders, you can run Enrole again, existing records will be skipped.
  • After completion any problems will be logged in flow.errors and summarised in the Valid face ? status.
  • 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)

Troubleshooting Enrollment

If you encounter "No Face" errors:

  • Check image quality. (e.g. very low resolution, poor contrast etc)
  • Increase the Confidence Threshold in the FACE Detection enrollment node.
  • Try using the YoloV8s-face model

For > 1 face :

  • Crop images to show only one face.
  • Decrease the Confidence Threshold in the FACE Detection enrollment node.

Sometimes it's easier to remove problematic images rather than troubleshoot them.

To attempt to force detection, click Fix Errors

  • This will use the error file list and either increase or decrease threshold by 1.

  • After completion if there are still errors, you can click Fix Errors again.

  • To start fresh, click Delete ALL to remove flow.people.


Testing

  • Use the inject file path for testing face identification.
  • Any image can be sent to the recognition flow.

Reolink Doorbell Setup:

A Specific example using Reolink -

  • Add your doorbell's IP address and password to the flow environent variables
  • If using FTP:
    • Configure your server for FTP access.
    • Setup the camera to FTP images on person detection.
    • Set the watch node path (e.g., /home/pi/FTP/files/face)
    • New images arriving will trigger the flow.
  • If using Webhook:
[{"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":[]}]

Collection Info

prev

Flow Info

Created 6 days ago
Rating: not yet rated

Owner

Actions

Rate:

Node Types

Core
  • change (x5)
  • comment (x2)
  • debug (x4)
  • delay (x1)
  • file (x1)
  • file in (x2)
  • function (x9)
  • http in (x1)
  • http request (x1)
  • http response (x1)
  • inject (x7)
  • link in (x3)
  • link out (x4)
  • switch (x1)
  • template (x1)
  • trigger (x1)
  • watch (x1)
Other

Tags

  • face
  • recognition
  • ai
  • vector
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option