node-red-contrib-face-recognition
Version:
A wrapper node for the epic face-api.js library
486 lines (417 loc) • 20.9 kB
HTML
<!-- Input Node -->
<script type="text/javascript">
RED.nodes.registerType('face-api-input', {
category: 'Face Recognition',
color: '#a6bbcf',
inputs: 1,
outputs: 1,
icon: "file.png",
defaults: {
name: { value: "face-api-input", required: true },
model: { value: "SSD", required: true },
confidence: { value: 50 },
input_size: { value: 416 },
landmarks: { value: false, required: true },
expressions: { value: false, required: true },
age_gender: { value: false, required: true },
descriptors: { value: false, required: true },
match_metric: { value: "Mean Squared Error", required: true },
match_confidence: { value: 0, required: true },
recognise_nodes: { value: [] },
recognise_node_editor: { value: null, type: "face-api-recognise", required: false },
},
label: function () {
return this.name || "face-api-input"
},
oneditprepare: async function () {
// Set the node as this for context
let node = this
// Add recognise nodes as an array if not there
node.recognise_nodes = (node.recognise_nodes || [])
// Create an ajax function to get the nodes info
let get_face_api_nodes = function () {
return new Promise((resolve, reject) => {
$.ajax({
url: 'flows',
type: 'get',
dataType: 'json',
async: false,
success: async function (data) {
let recognise_node_list = data.flows.filter(item => {
return item.type === "face-api-recognise"
})
recognise_node_list = recognise_node_list.map(item => {
return { 'id': item.id, 'name': item.name }
})
resolve(recognise_node_list)
},
error: function () {
reject([])
}
});
})
}
// Create a function to update the display of the side bar
let display_side_bar_html = function (recognise_node_list) {
$("#node-input-rule-container").css('min-height', '215px').css('min-width', '300px').editableList({
addItem: function (container, i, opt) {
// Setup the list dropdown
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
var row = $('<div/>').appendTo(container);
var recognise_node_dropdown = $('<select/>', { style: "width:95%; margin-left: 5px; text-align: center;" }).appendTo(row);
var recognise_node_dropdown_group = $('<optgroup/>', { label: "recognise nodes" }).appendTo(recognise_node_dropdown);
// Add the please select option to the dropdown
recognise_node_dropdown_group.append(
$("<option></option>")
.val("null")
.text("Please select...")
);
// Add each recognise node to other dropdown list
recognise_node_list.forEach(recognise_node => {
// Add the value to the drop down list
recognise_node_dropdown_group.append(
$("<option></option>")
.val(recognise_node.id)
.text(recognise_node.name)
);
// If the opt value is passed and valid change the value immediatley
if ((typeof opt === 'string' || opt instanceof String) && opt === recognise_node.id) {
recognise_node_dropdown.val(opt).change();
}
});
},
removeItem: function (opt) {
},
sortable: false,
removable: true
});
}
// Create a function to show all previously added recognise nodes
let display_previous_recognise_nodes = function () {
node.recognise_nodes.forEach((recognise_node_id) => {
$("#node-input-rule-container").editableList('addItem', recognise_node_id);
});
}
// Run the functions in sync
get_face_api_nodes()
.then(display_side_bar_html)
.then(display_previous_recognise_nodes)
.catch();
// Set the handler for model visibility
$("#node-input-model").on("change", function () {
($("#node-input-model").val() === "Tiny Yolo") ? $(".yoloOptions").show() : $(".yoloOptions").hide();
});
},
oneditsave: function () {
// Get the node from this for later use
let node = this
// Get the list of saved recognise nodes from the DOM element
let recognise_nodes = $("#node-input-rule-container").editableList('items');
// Clear the current list of recognise nodes for the input
node.recognise_nodes = [];
// Push each of the added nodes to the input node array
recognise_nodes.each(function (i) {
// Get the value of the drop down list to store
let value = $(this).find("select").val();
// Check if value is not null
if (value === "null") return;
// Check the node has not already been added
for (var i = 0; i < node.recognise_nodes.length; i++) {
if (node.recognise_nodes[i] === value) return;
}
// Push value to array if valid
node.recognise_nodes.push(value);
});
}
});
</script>
<script type="text/x-red" data-template-name="face-api-input">
<!-- General Settings -->
<h4>General Settings</h4>
<hr>
</hr>
<!-- Input Name -->
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<!-- Detection Settings -->
<h4>Detection Settings</h4>
<hr>
</hr>
<!-- Model -->
<div class="form-row">
<label for="node-input-model" style="vertical-align: middle;"><i class="icon-tag"></i> Detection Type </label>
<select type="text" id="node-input-model" style="position: relative; width: 70%;">
<option selected="selected">SSD</option>
<option>Tiny Yolo</option>
</select>
</div>
<!-- Confidence Level -->
<div class="form-row">
<label for="node-input-confidence" style="vertical-align: middle;"><i class="icon-bookmark"></i> Detection Confidence</label>
<input type="number" max="100" min="0" id="node-input-confidence" placeholder=50>
</div>
<!-- Input Size -->
<div class="form-row" hidden>
<label for="node-input-input_size" style="vertical-align: middle;"><i class="icon-bookmark"></i> Input Size</label>
<select type="number" id="node-input-input_size" style="position: relative; width: 70%;">
<option>128</option>
<option>160</option>
<option>128</option>
<option>224</option>
<option>320</option>
<option selected="selected">416</option>
<option>512</option>
<option>608</option>
</select>
</div>
<!-- Landmarks Boolean -->
<div class="form-row">
<label> </label>
<input type="checkbox" id="node-input-landmarks" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-landmarks" style="width: 70%;"><span> Add <b>Facial Landmarks</b> to each face</span></label>
</div>
<!-- Expressions Boolean -->
<div class="form-row">
<label> </label>
<input type="checkbox" id="node-input-expressions" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-expressions" style="width: 70%;"><span>Predict <b>Facial Expressions</b> for each face</span></label>
</div>
<!-- Age and Gender Boolean -->
<div class="form-row">
<label> </label>
<input type="checkbox" id="node-input-age_gender" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-age_gender" style="width: 70%;"><span>Predict <b>Age and Gender</b> for each face</span></label>
</div>
<!-- Descriptors -->
<div class="form-row">
<label> </label>
<input type="checkbox" id="node-input-descriptors" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-descriptors" style="width: 70%;"><span>Add <b>Descriptors</b> to the output</span></label>
</div>
<!-- Recognition Settings -->
<h4>Recognition Settings</h4>
<hr>
</hr>
<!-- Recognition Metric Field -->
<div class="form-row">
<label for="node-input-match_metric" style="vertical-align: middle;"><i class="icon-bookmark"></i> Recognition Metric</label>
<select type="text" id="node-input-match_metric" style="position: relative; width: 70%;">
<option>Euclidean</option>
<option>Manhattan</option>
<option>Chebyshev</option>
<option selected="selected">Mean Squared Error</option>
</select>
</div>
<!-- Recognition confidence Field -->
<div class="form-row">
<label for="node-input-match_confidence" style="vertical-align: middle;"><i class="icon-bookmark"></i> Matched Confidence</label>
<input type="number" min="0" id="node-input-match_confidence" placeholder="Enter a number between 0 and 10,000">
</div>
<!-- Recognise Node List -->
<div class="form-row node-input-rule-container-row">
<ol id="node-input-rule-container"></ol>
</div>
<!-- Recognise Node Editor -->
<div class="form-row">
<label for="node-input-recognise_node_editor" style="vertical-align: middle;">Add/Edit</label>
<input id="node-input-recognise_node_editor">
</div>
<!-- Padding Bottom -->
<hr style="padding-bottom: 18px">
</hr>
</script>
<script type="text/x-red" data-help-name="face-api-input">
<h3>Overview</h3>
<p>
Use this input node to inference on an image. Add recognise nodes to recognise faces.
</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload <span class="property-type">Buffer</span> </dt>
<dd> The payload must be set to a binary buffer of an image to be accepted</dd>
</dl>
<h3>Options</h3>
<dl class="message-properties">
<dt>Name <span class="property-type">String</span> </dt>
<dd> The name of the node instance, this will also be the label for recognition </dd>
<dt>Detection Type <span class="property-type">Dropdown</span> </dt>
<dd> The type of algorithm to use (SSD or Yolo) </dd>
<dt class=>Detection Confidence <span class="property-type">Number</span></dt>
<dd> The minimum confidence value to accept as a face. Defaults to 50%</dd>
<dt class="optional">Input Size <span class="property-type">Dropdown</span></dt>
<dd> The input size of the Yolo algorithm. Defaults to 416</dd>
<dt class="optional">Facial Landmarks <span class="property-type">Check Box</span></dt>
<dd> Check this to add facial landmarks to the calculation of each face</dd>
<dt class="optional">Facial Expressions <span class="property-type">Check Box</span></dt>
<dd> Check this to add facial expressions to the calculation of each face</dd>
<dt class="optional">Age and Gender <span class="property-type">Check Box</span></dt>
<dd> Check this to calculate the age and gender of each face</dd>
<dt class="optional">Descriptors <span class="property-type">Check Box</span></dt>
<dd> Check this to add the facial descriptors to the output</dd>
<dt class="optional">Recognition Metric <span class="property-type">Dropdown</span></dt>
<dd> The type of metric to compare faces for recognition. Defaults to Mean Squared Error</dd>
<dt class="optional">Matched Confidence <span class="property-type">Numeric</span></dt>
<dd> The cutoff value to decide if a face is recognised or not. Defaults to 0</dd>
<dt class="optional">Recognise Nodes List <span class="property-type">Check Box</span></dt>
<dd> Add recognise nodes here to match detected faces to known faces</dd>
<dt class="optional">Add/Edit <span class="property-type">Check Box</span></dt>
<dd> Add or edit recognise nodes to add to the list</dd>
</dl>
<h3>Details</h3>
<p>
This node is a central inference node for running facial detection and recognition on. Use <code>recognise nodes</code>.
to compare detected faces to known ones.
</p>
<h3>References</h3>
<ul>
<li><a href="https://github.com/justadudewhohacks/face-api.js">face-api.js</a> - Visit for more info on face-api.js</li>
<li><a href="">GitHub</a> - the nodes github repository</li>
</ul>
</script>
<!-- Recognise Node -->
<script type="text/javascript">
RED.nodes.registerType('face-api-recognise', {
category: 'config',
icon: "file.png",
defaults: {
name: { value: "Unknown", required: true },
file: { value: "" }
},
label: function () {
return this.name || "face-api-recognise"
},
oneditprepare: function () {
// Get the Id of the node for functions to use
const thisId = this.id
// Set up the button callbacks for when clicked
$("#node-node-config-file").on("change", function () {
if ($("#node-node-config-file").prop("files").length) {
$("#recognitionStatus").html("<b>Files added.</b> Click \"Update\" to upload. This make take a while so please be patient")
}
})
// Setup file button to allow uploads of images to the webpage
$("#fileButton").on("click", function () {
$("#recognitionStatus").html("<b>Please select one or more valid images</b>")
$("#node-node-config-file").trigger("click");
})
// Setup the delete button to remove descriptors if wanted
$("#deleteButton").on("click", function () {
$.ajax({
url: "faceapi/" + thisId + "/delete",
type: 'GET',
cache: false,
processData: false,
contentType: false,
success: function (data, textStatus) {
$("#recognitionStatus").html("<b>Deleted Node Descriptors</b>")
RED.notify("Successfully deleted descriptor");
},
error: function (jqXHR, textStatus, errorThrown) {
if (errorThrown === "Not Found") {
RED.notify("File not found for this node", "error");
}
else {
RED.notify("Node Red Internal Error: " + errorThrown, "error");
}
}
})
})
// Get the status of how many descriptors are stored
$.get("faceapi/" + thisId + "/check", function (data) {
if (data > 0) {
$("#recognitionStatus").html("<b>" + data.toString() + "</b>" + ((data > 1) ? " descriptors exist for this node" : " descriptor exists for this node"))
}
else if (data == 0) {
$("#recognitionStatus").html("<b>Descriptor does not exist</b>")
}
else {
$("#recognitionStatus").html("<b>Unknown state of descriptor</b>")
}
});
},
oneditsave: function () {
// Get the files to upload from the DOM element
const filesToUpload = $("#node-node-config-file").prop("files");
// If there are 1 or more files uplaod them to the backend
if (filesToUpload.length) {
// Notify the user
RED.notify("Attempting to create descriptors from images, please wait...", "info", false, 5000);
// Get the current ID of the node and create a URL
const uri = "faceapi/" + this.id + "/create";
// Create the data
let data = new FormData();
$.each(filesToUpload, function (key, value) {
data.append(key, value);
});
// Send data using ajax to backend url
$.ajax({
url: uri,
type: 'POST',
data: data,
cache: false,
processData: false,
contentType: false,
success: function (data, textStatus) {
RED.notify("Successfully created descriptors from images", "info", false, 5000);
},
error: function (jqXHR, textStatus, errorThrown) {
RED.notify("Error Creating Descriptors: " + textStatus, "error", false, 5000);
}
})
}
// Set the file field to null on exit
$("#node-node-config-file").val('');
}
});
</script>
<script type="text/x-red" data-template-name="face-api-recognise">
<!-- Name Field -->
<div class="form-row">
<label for="node-config-input-name" style="vertical-align: middle;"><i class="icon-bookmark"></i> Name </label>
<input type="text" id="node-config-input-name" placeholder="Name of the face used in this node">
</div>
<!-- File uploads -->
<div class="form-row">
<label for="node-node-config-file" style="vertical-align: middle;"><i class="icon-bookmark"></i> Add Descriptor</label>
<input type="file" multiple accept="image/*" id="node-node-config-file" style="position:absolute; top:-100px">
<div style="display: inline-block; position: relative; width: 70%; height: 20px;">
<div class="btn-group">
<a id="fileButton" class="editor-button"><b>Add Images</b></a>
<a id="deleteButton" class="editor-button">Remove Descriptors</a>
</div>
</div>
</div>
<!-- Descriptor status -->
<div class="form-row">
<label for="recognitionStatus"></label>
<label id="recognitionStatus" style="width: 70%;"></label>
</div>
</script>
<script type="text/x-red" data-help-name="face-api-recognise">
<h3>Overview</h3>
<p>
Recognises the desired face based on previous known detections.
</p>
<h3>Options</h3>
<dl class="message-properties">
<dt>Name <span class="property-type">String</span> </dt>
<dd> The name of the node instance, this will also be the label for recognition </dd>
</dl>
<h3>Details</h3>
<p>
To use the facial recognition, please select images using the <code>Add Image</code> button after
checking the recognise box. After selection the image will be processed into a descriptor
for this node. If an image has not been selected, the node will not compare the input for
recognition and throw a warning. The label for the descriptor will be the name of the node.
</p>
<h3>References</h3>
<ul>
<li><a href="https://github.com/justadudewhohacks/face-api.js">face-api.js</a> - Visit for more info on face-api.js</li>
<li><a href="https://github.com/thebigpotatoe/node-red-contrib-face-api">GitHub</a> - the nodes github repository</li>
</ul>
</script>