From 300624f61fc03d92aa1c8978d2c595d45a10dc31 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Wed, 7 Dec 2022 23:26:16 +0200 Subject: [PATCH 01/11] Face manager is full working --- languages/en_CA.json | 12 +- libs/faceManager.js | 325 ++++++++++++++++ libs/webServerAdminPaths.js | 2 + web/assets/js/super.faceManager.js | 508 ++++++++++++++++++++++++++ web/pages/blocks/superFaceManager.ejs | 30 ++ web/pages/super.ejs | 6 + 6 files changed, 882 insertions(+), 1 deletion(-) create mode 100644 libs/faceManager.js create mode 100644 web/assets/js/super.faceManager.js create mode 100644 web/pages/blocks/superFaceManager.ejs diff --git a/languages/en_CA.json b/languages/en_CA.json index 592e12dc..b5208d8c 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -1731,5 +1731,15 @@ "Last Updated": "Last Updated", "Z-Wave Manager": "Z-Wave Manager", "Z-Wave": "Z-Wave", - "Primary":"Primary" + "Primary":"Primary", + "Upload Images": "Upload Images", + "Images Sent": "Images Sent", + "Click to Upload Images": "Click to Upload Images", + "Face Name": "Face Name", + "faceManager": "Face Manager", + "Face Manager": "Face Manager", + "deleteFace": "Delete Face", + "deleteFaceText": "Are you sure you want to delete ALL the images for this face {face}? they will not be recoverable.", + "deleteImage": "Delete Image", + "deleteImageText": "Are you sure you want to delete this image {image} of {face}? it will not be recoverable." } diff --git a/libs/faceManager.js b/libs/faceManager.js new file mode 100644 index 00000000..eb8ec46d --- /dev/null +++ b/libs/faceManager.js @@ -0,0 +1,325 @@ +const fs = require('fs'); +const fsExtra = require("fs-extra"); +const fileUpload = require('express-fileupload'); +const { execPath } = require('process'); + +const MODULE_NAME = "face"; + +module.exports = (s, config, lang, app, io) => { + const data = { + faces: {} + }; + + const sendDataToConnectedSuperUsers = (message) => { + return s.tx(message, '$'); + }; + + const reloadFacesData = (notify = false) => { + data.faces = {}; + + fs.readdir(config.facesFolder, (err, folders) => { + folders.forEach((name) => { + const faceDirectory = `${config.facesFolder}${name}`; + const stats = fs.statSync(faceDirectory); + + if(stats.isDirectory()) { + try { + data.faces[name] = fs.readdirSync(faceDirectory); + + } catch (ex) { + console.error(`Failed to load faces and images, Error: ${ex}`); + } + } + }); + + const message = { + f: 'recompileFaceDescriptors', + faces: data.faces, + path: config.facesFolder + }; + + sendDataToConnectedSuperUsers(message); + + s.sendToAllDetectors(message); + }); + }; + + const notifyImageUploaded = (name, image, authUser) => { + const endpoint = `${name}/image/${image}`; + const fileLink = getUrl(endpoint, authUser); + + sendDataToConnectedSuperUsers({ + f:`${MODULE_NAME}ImageUploaded`, + faceName: name, + fileName: image, + url: fileLink + }); + }; + + const notifyContentDeleted = (name, image = null) => { + const isImageDeleted = image !== undefined && image !== null; + const deleteDescription = isImageDeleted ? "Image" : "Folder"; + + const message = { + f: `${MODULE_NAME}${deleteDescription}Deleted`, + faceName: name + } + + if(isImageDeleted) { + message["fileName"] = image + } + + sendDataToConnectedSuperUsers(message); + }; + + const getUrl = (endpoint, authUser = ":auth") => { + const url = `${config.webPaths.superApiPrefix}${authUser}/${MODULE_NAME}${endpoint}`; + + return url; + }; + + const getImagePath = (name, image) => { + const path = `${getFacePath(name)}/${image}`; + + return path; + }; + + const getFacePath = (name) => { + const path = `${config.facesFolder}${name}`; + + return path; + }; + + const checkFile = (file, name, authUser) => { + const fileName = file.name; + const fileParts = fileName.split("."); + const fileExt = fileParts[fileParts.length]; + const allowedExtensions = ["jpeg", "jpg"]; + const canUpload = allowedExtensions.includes(fileExt); + const result = canUpload ? fileName : null; + + if(canUpload) { + const facePath = getFacePath(req.params.name); + const imagePath = getImagePath(req.params.name, fileName); + + if(!fs.existsSync(facePath)){ + fs.mkdirSync(facePath); + } + + file.mv(imagePath, function(err) { + if(err) { + console.error(`Failed to store image in ${imagePath}`); + } else { + notifyImageUploaded(name, fileName, authUser); + } + }); + } + + return result; + }; + + const registerDeleteEndpoint = (endpoint, handler) => { + const url = getUrl(endpoint); + + app.delete(url, (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + }; + + const registerGetEndpoint = (endpoint, handler) => { + const url = getUrl(endpoint); + + app.get(url, (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + }; + + const registerPostEndpoint = (endpoint, handler, isFileUpload = false) => { + const url = getUrl(endpoint); + + if(isFileUpload) { + app.post(url, fileUpload(), (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + } else { + app.post(url, (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + } + }; + + const handleGetFaces = (req, res, superAuthResponse) => { + res.json({ + ok: true, + faces: data.faces + }); + }; + + const handleGetImage = (req, res, superAuthResponse) => { + const imagePath = getImagePath(req.params.name, req.params.image); + + if(fs.existsSync(imagePath)) { + res.setHeader('Content-Type', 'image/jpeg'); + fs.createReadStream(imagePath).pipe(res); + + } else { + res.json({ + ok: false, + msg: lang['File Not Found'] + }); + } + }; + + const handleDeleteImage = (req, res, superAuthResponse) => { + const name = req.params.name; + const image = req.params.image; + + const imagePath = getImagePath(name, image); + + if(fs.existsSync(imagePath)) { + fs.rm(imagePath,() => { + reloadFacesData(true); + + console.info(`Delete image '${image}' of face '${name}' completed successfuly`); + + notifyContentDeleted(name, image); + + res.json({ + ok: true, + }); + }); + + } else { + res.json({ + ok: false, + }); + } + }; + + const handleDeleteFace = (req, res, superAuthResponse) => { + const name = req.params.name; + const facePath = getFacePath(name); + + if(fs.existsSync(facePath)) { + fsExtra.emptyDirSync(facePath); + + fs.rmdir(facePath, () => { + reloadFacesData(true); + + console.info(`Delete face '${name}' completed successfuly`); + + notifyContentDeleted(name); + + res.json({ + ok: true, + }); + }); + + } else { + res.json({ + ok: false, + }); + } + }; + + const handleMoveImage = (req, res, superAuthResponse) => { + const oldImagePath = getImagePath(req.params.name, req.params.image); + const newImagePath = getImagePath(req.params.newName, req.params.image); + const fileExists = fs.existsSync(oldImagePath); + + if(fileExists) { + fs.rename(oldImagePath, newImagePath, (err, content) => { + notifyContentDeleted(req.params.name, req.params.image); + + notifyImageUploaded(req.params.newName, req.params.newImage, req.params.auth); + + if(err) { + console.error(`Failed to move file from ${oldImagePath} to ${newImagePath}, Error: ${err}`); + } else { + reloadFacesData(true); + + console.info(`Handle image move completed successfuly, From: ${oldImagePath}, To: ${newImagePath}`); + } + }); + } else { + console.error(`Handle image move failed file '${oldImagePath}' does not exists`); + } + + res.json({ + ok: fileExists, + }); + }; + + const handleImageUpload = (req, res, superAuthResponse) => { + const fileKeys = Object.keys(req.files || {}); + + if(fileKeys.length == 0){ + return res.status(400).send('No files were uploaded.'); + } + + fileKeys.forEach(key => { + const file = req.files[key]; + + try { + const files = file instanceof Array ? [...file] : [file]; + + const uploaded = files.map(f => checkFile(f, req.params.name, req.params.auth)); + + reloadFacesData(true); + + const responseData = { + ok: true, + filesUploaded: uploaded + }; + + console.info(`Handle image uploading completed successfuly, Data: ${responseData}`); + + res.json(responseData); + } catch(err) { + console.error(`Failed to upload file ${file}, Error: ${err}`); + + res.json({ + ok: false + }); + } + }); + + + } + + const createDirectory = () => { + if(!config.facesFolder) { + config.facesFolder = `${s.mainDirectory}/faces/`; + } + + config.facesFolder = s.checkCorrectPathEnding(config.facesFolder); + + if(!fs.existsSync(config.facesFolder)){ + fs.mkdirSync(config.facesFolder) + } + }; + + const initialize = () => { + createDirectory(); + reloadFacesData(); + + registerGetEndpoint('s', handleGetFaces); + registerGetEndpoint('/:name/image/:image', handleGetImage); + + registerDeleteEndpoint('/:name', handleDeleteFace); + registerDeleteEndpoint('/:name/image/:image', handleDeleteImage); + + registerPostEndpoint('/:name', handleImageUpload, () => fileUpload()); + registerPostEndpoint('/:name/image/:image/move/:newName', handleMoveImage); + }; + + initialize(); +} diff --git a/libs/webServerAdminPaths.js b/libs/webServerAdminPaths.js index 6612ea9e..8205c7a8 100644 --- a/libs/webServerAdminPaths.js +++ b/libs/webServerAdminPaths.js @@ -668,4 +668,6 @@ module.exports = function(s,config,lang,app){ } },res,req) }) + + require('./faceManager.js')(s,config,lang,app,null); } diff --git a/web/assets/js/super.faceManager.js b/web/assets/js/super.faceManager.js new file mode 100644 index 00000000..c697acd0 --- /dev/null +++ b/web/assets/js/super.faceManager.js @@ -0,0 +1,508 @@ +$(document).ready(() => { + //const faceManagerModal = $("#faceManager"); + const faceImageList = $("#faceImageList"); + const faceNvigationPanel = $("#faceNvigationPanel"); + + const faceManagerForm = $("#faceManagerUploadForm"); + const faceNameField = $("#faceNameField"); + const fileinput = $("#fileinput"); + const faceManagerUploadStatus = $("#faceManagerUploadStatus"); + + const getUrl = (endpoint) => { + const url = `${moduleData.baseUrl}${endpoint}`; + + return url; + }; + + const getFaceImages = () => { + const url = getUrl("s"); + + $.get(url, response => { + moduleData.faces = response.faces || {}; + + onFaceImagesReterived(); + }); + }; + + const sendAJAXRequest = (requestData) => { + try { + requestData.success = result => { + console.info(`Succesfully sent ${requestData.type} request to ${requestData.url}, Response: ${JSON.stringify(result)}`); + }; + + requestData.error = result => { + console.error(`Failed to ${requestData.type} request to ${requestData.url}, Response: ${JSON.stringify(result)}`); + }; + + $.ajax(requestData); + } catch (error) { + console.error(`Failed to ${requestData.type} request to ${requestData.url}, Error: ${error}`); + } + }; + + const deleteFaceImage = (name, image) => { + try { + const url = getUrl(`/${name}/image/${image}`); + + const requestData = { + url: url, + type: "DELETE" + }; + + sendAJAXRequest(requestData); + } catch (error) { + console.error(`Failed to delete image ${image} of face ${face}, Error: ${error}`); + } + }; + + const deleteFaceFolder = (name) => { + try { + const url = getUrl(`/${name}`); + + const requestData = { + url: url, + type: "DELETE" + }; + + sendAJAXRequest(requestData); + } catch (error) { + console.error(`Failed to delete face ${face}, Error: ${error}`); + } + }; + + const moveFaceImage = (name, image, newFaceName) => { + try { + const url = getUrl(`/${name}/image/${image}/move/${newFaceName}`); + + const requestData = { + url: url, + type: "POST", + data: new FormData(faceManagerForm[0]), + cache: false, + contentType: false, + processData: false, + }; + + sendAJAXRequest(requestData); + } catch (error) { + console.error(`Failed to move image ${image} of face ${face} to ${newFaceName}, Error: ${error}`); + } + }; + + const onFormSubmitted = () => { + try { + const name = faceNameField.val(); + const url = getUrl(`/${name}`); + + const requestData = { + url: url, + type: "POST", + data: new FormData(faceManagerForm[0]), + cache: false, + contentType: false, + processData: false + }; + + faceManagerUploadStatus.show(); + + sendAJAXRequest(requestData); + + faceNameField.val(""); + fileinput.val(""); + + } catch (error) { + console.error(`Failed to handle form submit event, Error: ${error}`); + } + }; + + const getFaceNavigationItem = (name) => { + const images = moduleData.faces[name]; + const imageCount = images.length; + + const navigationItem = ``; + + return navigationItem; + }; + + const getFaceDelete = (name) => { + const navigationItem = `
+ ${lang.deleteFace} + ${lang['Click to Upload Images']} +
`; + + return navigationItem; + }; + + const getImageListItem = (name, image) => { + const imageUrl = getUrl(`/${name}/image/${image}`); + const imageItem = `
+ +
`; + + return imageItem; + }; + + const onSelectedFaceChanged = () => { + try { + $(`#faceNvigationPanel li[face="${moduleData.selectedFace}"] a`).tab('show'); + + faceImageList.empty(); + const images = moduleData.faces[moduleData.selectedFace]; + const items = images.map(image => getImageListItem(moduleData.selectedFace, image)); + + const deleteFace = getFaceDelete(moduleData.selectedFace); + items.push(deleteFace); + + const html = items.join(""); + faceImageList.html(html); + + activateDraggableImages(); + prettySizeFaceImages(); + } catch (error) { + console.error(`Failed to handle face [${moduleData.selectedFace}] selection changed event, Error: ${error}`); + } + }; + + const onFaceImagesReterived = () => { + try { + faceNvigationPanel.empty(); + faceImageList.empty(); + faceManagerUploadStatus.hide(); + + const faceKeys = Object.keys(moduleData.faces); + + if(faceKeys.length === 0) { + console.info("No face found"); + + } else { + if(moduleData.selectedFace === null) { + moduleData.selectedFace = faceKeys[0]; + } + + const items = faceKeys.map(name => getFaceNavigationItem(name)); + + const html = items.join(""); + + faceNvigationPanel.html(html); + + faceKeys.forEach(name => activateDroppableContainer(name)); + + onSelectedFaceChanged(); + } + + } catch (error) { + console.error(`Failed to handle images reterived event, Error: ${error}`); + } + }; + + const drawFaceImages = (selectedFace = null) => { + try { + moduleData.selectedFace = selectedFace; + + onFormDataChanged(); + + getFaceImages(); + } catch (error) { + console.error(`Failed to draw face images, Error: ${error}`); + } + }; + + const prettySizeFaceImages = () => { + try { + const faceImagesRendered = faceImageList.find(".face-image"); + const faceHeight = faceImagesRendered.first().width(); + faceImagesRendered.css("height", faceHeight); + } catch (error) { + console.error(`Failed to resize images, Error: ${error}`); + } + }; + + const activateDroppableContainer = (name) => { + const navigationItem = faceNvigationPanel.find(`.nav-item[face="${name}"]`); + + try { + navigationItem.droppable("destroy"); + } catch (err) {} + + try { + navigationItem.droppable({ + tolerance: "intersect", + accept: ".face-image", + activeClass: "ui-state-default", + hoverClass: "ui-state-hover", + drop: (eventData, ui) => { + const newFace = getEventItemAttribute(eventData, "face", "target"); + const oldFace = getEventItemAttribute(eventData.originalEvent, "face", "target"); + const fileName = getEventItemAttribute(eventData.originalEvent, "image", "target"); + const faceImageElement = $(ui.draggable); + + if (oldFace !== newFace) { + moveFaceImage(oldFace, fileName, newFace); + } else { + faceImageElement.css({ + top: 0, + left: 0, + }); + } + }, + }); + } catch (error) { + console.error(`Failed to activate draggable images, Face: ${name}, Error: ${error}`); + } + }; + + const activateDraggableImages = (name) => { + const imageEls = faceImageList.find(`.face-image`); + try { + imageEls.draggable("destroy"); + } catch (err) {} + + try { + imageEls.draggable({ + appendTo: "body", + cursor: "move", + revert: "invalid", + }); + } catch (error) { + console.error(`Failed to activate draggable images, Face: ${name}, Error: ${error}`); + } + }; + + $(window).resize(() => { + try { + prettySizeFaceImages(); + } catch (error) { + console.error(`Failed to resize event, Error: ${error}`); + } + + }); + + const getEventItemAttribute = (eventData, key, targetKey = null) => { + if(targetKey === null) { + targetKey = "currentTarget"; + } + + window.eventItems = { + eventData: eventData, + targetKey: targetKey, + key: key, + targetElement: eventData[targetKey] + }; + + const targetElement = eventData[targetKey]; + + const value = targetElement.attributes[key].value; + + return value; + }; + + const getFixedText = (text, replacements = {}) => { + const replacementKeys = Object.keys(replacements); + replacementKeys.forEach(r => { + text = text.replace(`{${r}}`, `${replacements[r]}`); + }); + + return text; + }; + + const onDeleteImageRequest = (e) => { + try { + e.preventDefault(); + const faceName = getEventItemAttribute(e, "face"); + const faceImage = getEventItemAttribute(e, "image"); + + const imageUrl = getUrl(`/${faceName}/image/${faceImage}`); + const textReplacements = { + "face": faceName, + "image": faceImage + }; + + const text = getFixedText(lang.deleteImageText, textReplacements); + + const data = { + title: lang.deleteImage, + body: `${text}
`, + clickOptions: { + class: "btn-danger", + title: lang.Delete, + }, + clickCallback: () => deleteFaceImage(faceName, faceImage) + }; + + $.confirm.create(data); + } catch (error) { + console.error(`Failed to handle delete image request event, Error: ${error}`); + } + + return false; + }; + + const onUploadImageRequest = (e) => { + try { + e.preventDefault(); + const faceName = getEventItemAttribute(e, "face"); + + faceNameField.val(faceName); + fileinput.trigger('click'); + + const image = fileinput.val(); + + if(image !== undefined && image !== null && image !== "") { + setTimeout(() => onFormSubmitted(), 2000); + } else { + faceNameField.val(""); + fileinput.val(""); + } + + } catch (error) { + console.error(`Failed to handle delete image request event, Error: ${error}`); + } + + return false; + }; + + const onDeleteFaceRequest = (e) => { + try { + e.preventDefault(); + + const faceName = getEventItemAttribute(e, "face"); + + const textReplacements = { + "face": faceName + }; + + const text = getFixedText(lang.deleteFaceText, textReplacements); + + const data = { + title: lang.deleteFace, + body: text, + clickOptions: { + class: "btn-danger", + title: lang.Delete, + }, + clickCallback: () => deleteFaceFolder(faceName) + }; + + $.confirm.create(data); + } catch (error) { + console.error(`Failed to handle delete face request event, Error: ${error}`); + } + + return false; + }; + + const onFormDataChanged = () => { + try { + const name = faceNameField.val(); + const image = fileinput.val(); + + const enableSubmit = name !== undefined && name !== null && name.length > 0 && image !== undefined && image !== null && image.length > 0; + + $("#submitUpload").prop('disabled', !enableSubmit); + } catch (error) { + console.error(`Failed to handle image browsing, Error: ${error}`); + } + }; + + const onInitSuccess = (d) => { + try { + moduleData.baseUrl = `${superApiPrefix}${d.superSessionKey}/face`; + + drawFaceImages(); + } catch (error) { + console.error(`Failed to handle init success event, Error: ${error}`); + } + }; + + const onRecompileFaceDescriptors = (d) => { + moduleData.faces = d.faces; + + onFaceImagesReterived(moduleData.selectedFace); + }; + + const onFaceFolderDeleted = (d) => { + drawFaceImages(); + }; + + const onFaceImageDeleted = (d) => { + drawFaceImages(moduleData.selectedFace); + }; + + const onFaceImageUploaded = (d) => { + drawFaceImages(d.faceName); + }; + + const moduleData = { + baseUrl: null, + faces: {}, + selectedFace: null, + eventMapper: { + "init_success": onInitSuccess, + "recompileFaceDescriptors": onRecompileFaceDescriptors, + "faceFolderDeleted": onFaceFolderDeleted, + "faceImageDeleted": onFaceImageDeleted, + "faceImageUploaded": onFaceImageUploaded + } + }; + + window.moduleData = moduleData; + + $.ccio.ws.on("f", (d) => { + const handler = moduleData.eventMapper[d.f]; + + if (handler === undefined) { + console.info(`No handler found, Data: ${JSON.stringify(d)}`); + } else { + try { + setTimeout(() => { + handler(d); + }, 100); + } catch (error) { + console.error(`Failed to handle event ${d.f}, Error: ${error}`); + } + + + } + }); + + // Upload image + faceManagerForm.submit(() => { + onFormSubmitted(); + + return false; + }); + + $("div").on("click", ".deleteFolder", e => { + onDeleteFaceRequest(e); + }); + + faceImageList.on("click", ".deleteImage", e => { + onDeleteImageRequest(e); + }); + + faceImageList.on("click", ".uploadImage", e => { + onUploadImageRequest(e); + }); + + faceNameField.change(() => { + onFormDataChanged(); + }); + + fileinput.change(() => { + onFormDataChanged(); + }); + + faceNvigationPanel.on('click', ".faceNavigation", e => { + const faceName = getEventItemAttribute(e, "face"); + + if(faceName !== moduleData.selectedFace) { + moduleData.selectedFace = faceName; + + onSelectedFaceChanged(); + } + + + }); +}); diff --git a/web/pages/blocks/superFaceManager.ejs b/web/pages/blocks/superFaceManager.ejs new file mode 100644 index 00000000..edb2a7b0 --- /dev/null +++ b/web/pages/blocks/superFaceManager.ejs @@ -0,0 +1,30 @@ +
+
+
+
+ <%- lang['Face Name'] %> +
+
+ +
+
+ +
+
+ +
+
+ <%- lang['Images Sent'] %> +
+
+
+
+ + +
+
+
+
+
+ + diff --git a/web/pages/super.ejs b/web/pages/super.ejs index 75d1201e..076e9df1 100644 --- a/web/pages/super.ejs +++ b/web/pages/super.ejs @@ -90,6 +90,9 @@ +
@@ -126,6 +129,9 @@
<% include blocks/superPluginManager.ejs %>
+
+ <% include blocks/superFaceManager.ejs %> +
From 837bae92cc37145c62f87c67aa7b3aed1131d9d0 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Wed, 7 Dec 2022 23:28:54 +0200 Subject: [PATCH 02/11] removed unused parameter --- web/assets/js/super.faceManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/assets/js/super.faceManager.js b/web/assets/js/super.faceManager.js index c697acd0..0b4f4850 100644 --- a/web/assets/js/super.faceManager.js +++ b/web/assets/js/super.faceManager.js @@ -419,7 +419,7 @@ $(document).ready(() => { const onRecompileFaceDescriptors = (d) => { moduleData.faces = d.faces; - onFaceImagesReterived(moduleData.selectedFace); + onFaceImagesReterived(); }; const onFaceFolderDeleted = (d) => { From f2fa2c1832e3e52fe47dcbe1ec3f488060a4d1d5 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Thu, 8 Dec 2022 14:02:56 +0200 Subject: [PATCH 03/11] Add support for DeepStack / CodeProject AI to work with the face manager UI --- libs/faceManager.js | 13 +- plugins/deepstack-face/conf.sample.json | 1 + .../deepstack-face/shinobi-deepstack-face.js | 265 +++++++++++++----- .../shinobi-deepstack-object.js | 265 +++++++++++++----- web/assets/js/super.faceManager.js | 2 - 5 files changed, 399 insertions(+), 147 deletions(-) diff --git a/libs/faceManager.js b/libs/faceManager.js index eb8ec46d..d91d5e2e 100644 --- a/libs/faceManager.js +++ b/libs/faceManager.js @@ -290,9 +290,7 @@ module.exports = (s, config, lang, app, io) => { ok: false }); } - }); - - + }); } const createDirectory = () => { @@ -307,10 +305,13 @@ module.exports = (s, config, lang, app, io) => { } }; + const onProcessReady = (d) => { + reloadFacesData(); + }; + const initialize = () => { createDirectory(); - reloadFacesData(); - + registerGetEndpoint('s', handleGetFaces); registerGetEndpoint('/:name/image/:image', handleGetImage); @@ -319,6 +320,8 @@ module.exports = (s, config, lang, app, io) => { registerPostEndpoint('/:name', handleImageUpload, () => fileUpload()); registerPostEndpoint('/:name/image/:image/move/:newName', handleMoveImage); + + s.onProcessReadyExtensions.push(onProcessReady); }; initialize(); diff --git a/plugins/deepstack-face/conf.sample.json b/plugins/deepstack-face/conf.sample.json index 14338e0b..a78241fe 100644 --- a/plugins/deepstack-face/conf.sample.json +++ b/plugins/deepstack-face/conf.sample.json @@ -7,6 +7,7 @@ "key": "DeepStack-Face", "mode": "client", "type": "detector", + "fullyControlledByFaceManager": false, "deepStack": { "host": "HOSTNAME OR IP", "port": 5000, diff --git a/plugins/deepstack-face/shinobi-deepstack-face.js b/plugins/deepstack-face/shinobi-deepstack-face.js index cd634988..4de09449 100644 --- a/plugins/deepstack-face/shinobi-deepstack-face.js +++ b/plugins/deepstack-face/shinobi-deepstack-face.js @@ -51,6 +51,8 @@ if(s === null) { let detectorSettings = null; +const DELIMITER = "___"; + const DETECTOR_TYPE_FACE = 'face'; const DETECTOR_TYPE_OBJECT = 'object'; @@ -61,10 +63,13 @@ const DETECTOR_CONFIGUTATION = { face: { detectEndpoint: '/vision/face/recognize', startupEndpoint: '/vision/face/list', + registerEndpoint: '/vision/face/register', + deleteEndpoint: '/vision/face/delete', key: 'userid' }, object: { detectEndpoint: '/vision/detection', + startupEndpoint: '/vision/detection', key: 'label' } } @@ -103,7 +108,7 @@ const postMessage = (data) => { const initialize = () => { const deepStackProtocol = PROTOCOLS[config.deepStack.isSSL]; - baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; + const baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; const detectionType = config.plug.split("-")[1].toLowerCase(); const detectorConfig = DETECTOR_CONFIGUTATION[detectionType]; @@ -113,67 +118,162 @@ const initialize = () => { type: detectionType, active: false, baseUrl: baseUrl, - apiKey: config.deepStack.apiKey + apiKey: config.deepStack.apiKey, + faces: { + shinobi: null, + server: null, + legacy: null + }, + eventMapping: { + "recompileFaceDescriptors": onRecompileFaceDescriptors + } }; - - if(detectionType === DETECTOR_TYPE_FACE) { - detectorSettings["registeredPersons"] = config.persons === undefined ? [] : config.persons; - } - + detectorConfigKeys.forEach(k => detectorSettings[k] = detectorConfig[k]); - const testRequestData = getFormData(detectorSettings.detectEndpoint); + const startupRequestData = getFormData(detectorSettings.startupEndpoint); - request.post(testRequestData, (err, res, body) => { - try { - if(err) { - throw err; - } - + request.post(startupRequestData, handleStartupResponse); +}; + +const handleStartupResponse = (err, res, body) => { + try { + if(err) { + logError(`Failed to initialize ${config.plug} plugin, Error: ${err}`); + + } else { const response = JSON.parse(body); - + if(response.error) { detectorSettings.active = !response.error.endsWith('endpoint not activated'); } else { detectorSettings.active = response.success; } - const detectorSettingsKeys = Object.keys(detectorSettings); - - const pluginMessageHeader = []; - pluginMessageHeader.push(`${config.plug} loaded`); - - const configMessage = detectorSettingsKeys.map(k => `${k}: ${detectorSettings[k]}`); - - const fullPluginMessage = pluginMessageHeader.concat(configMessage); - - const pluginMessage = fullPluginMessage.join(", "); - - logInfo(pluginMessage); + logInfo(`${config.plug} loaded, Configuration: ${JSON.stringify(detectorSettings)}`); if (detectorSettings.active) { s.detectObject = detectObject; - if(detectionType === DETECTOR_TYPE_FACE) { - const requestData = getFormData(detectorSettings.startupEndpoint); - const requestTime = getCurrentTimestamp(); - - request.post(requestData, (errStartup, resStartup, bodyStartup) => { - if (!!resStartup) { - resStartup.duration = getDuration(requestTime); - } - - onFaceListResult(errStartup, resStartup, bodyStartup); - }); + if(detectorSettings.type === DETECTOR_TYPE_FACE) { + onFaceListResult(err, res, body); } - } - } catch(ex) { - logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`) + } } - }); + + + } catch(ex) { + logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`); + } }; -const processImage = (imageB64, d, tx, frameLocation, callback) => { +const registerFace = (serverFileName) => { + const shinobiFileParts = serverFileName.split(DELIMITER); + const faceName = shinobiFileParts[0]; + const image = shinobiFileParts[1]; + + const frameLocation = `${config.facesFolder}${faceName}/${image}`; + + const imageStream = fs.createReadStream(frameLocation); + + const form = { + image: imageStream, + userId: serverFileName + }; + + const requestData = getFormData(detectorSettings.registerEndpoint, form); + + request.post(requestData, (err, res, body) => { + if (err) { + logError(`Failed to register face, Face: ${faceName}, Image: ${image}`); + } else { + logInfo(`Register face, Face: ${faceName}, Image: ${image}`); + } + }); +}; + +const unregisterFace = (serverFileName) => { + const form = { + userId: serverFileName + }; + + const requestData = getFormData(detectorSettings.deleteEndpoint, form); + + request.post(requestData, (err, res, body) => { + if (err) { + logError(`Failed to delete face, UserID: ${serverFileName}`); + } else { + logInfo(`Deleted face, UserID: ${serverFileName}`); + } + }); +}; + +const getServerFileNameByShinobi = (name, image) => { + const fileName = `${name}${DELIMITER}${image}`; + + return fileName; +} + +const compareShinobiVSServer = () => { + const allFaces = detectorSettings.faces; + const shinobiFaces = allFaces.shinobi; + const serverFaces = allFaces.server; + const compareShinobiVSServerDelayID = detectorSettings.compareShinobiVSServerDelayID || null; + + if (compareShinobiVSServerDelayID !== null) { + clearTimeout(compareShinobiVSServerDelayID) + } + + if(serverFaces === null || shinobiFaces === null) { + detectorSettings.compareShinobiVSServerDelayID = setTimeout(compareShinobiVSServer, 5000); + logWarn("AI Server not ready yet, will retry in 5 seconds"); + + return; + } + + const shinobiFaceKeys = Object.keys(shinobiFaces); + + const shinobiFiles = shinobiFaceKeys.length === 0 ? + [] : + shinobiFaceKeys + .map(faceName => { + const value = shinobiFaces[faceName].map(image => getServerFileNameByShinobi(faceName, image)); + + return value; + }) + .reduce((acc, item) => { + console.log("acc: ", acc); + console.log("item: ", item); + + const result = [...acc, ...item]; + + return result; + }); + + const facesToRegister = shinobiFiles.filter(f => !serverFaces.includes(f)); + const facesToUnregister = serverFaces.filter(f => !shinobiFiles.includes(f)); + + if(facesToRegister.length > 0) { + logInfo(`Registering the following faces: ${facesToRegister}`); + facesToRegister.forEach(f => registerFace(f)); + } + + if(facesToUnregister.length > 0) { + const allowUnregister = detectorSettings.fullyControlledByFaceManager || false; + + if(allowUnregister) { + logInfo(`Unregister the following faces: ${facesToUnregister}`); + + facesToUnregister.forEach(f => unregisterFace(f)); + } else { + logInfo(`Skip unregistering the following faces: ${facesToUnregister}`); + + detectorSettings.faces.legacy = facesToUnregister; + } + } +}; + +const processImage = (frameBuffer, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; } @@ -195,11 +295,11 @@ const processImage = (imageB64, d, tx, frameLocation, callback) => { res.duration = getDuration(requestTime); } - onImageProcessed(d, tx, err, res, body, imageB64); + onImageProcessed(d, tx, err, res, body, frameBuffer); fs.unlinkSync(frameLocation); }); - }catch(ex){ + } catch(ex) { logError(`Failed to process image, Error: ${ex}`); if(fs.existsSync(frameLocation)) { @@ -221,21 +321,19 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { d.dir = `${s.dir.streams}${d.ke}/${d.id}/`; - frameLocation = `${d.dir}${s.gid(5)}.jpg`; + const path = `${d.dir}${s.gid(5)}.jpg`; if(!fs.existsSync(d.dir)) { fs.mkdirSync(d.dir, dirCreationOptions); } - fs.writeFile(frameLocation, frameBuffer, function(err) { + fs.writeFile(path, frameBuffer, function(err) { if(err) { return s.systemLog(err); } try { - const imageB64 = frameBuffer.toString('base64'); - - processImage(imageB64, d, tx, frameLocation, callback); + processImage(frameBuffer, d, tx, path, callback); } catch(ex) { logError(`Detector failed to parse frame, Error: ${ex}`); @@ -260,8 +358,6 @@ const getDuration = (requestTime) => { }; const onFaceListResult = (err, res, body) => { - const duration = !!res ? res.duration : 0; - try { const response = JSON.parse(body); @@ -269,20 +365,22 @@ const onFaceListResult = (err, res, body) => { const facesArr = response.faces; const faceStr = facesArr.join(","); + detectorSettings.faces.server = facesArr; + if(success) { - logInfo(`DeepStack loaded with the following faces: ${faceStr}, Response time: ${duration} ms`); + logInfo(`DeepStack loaded with the following faces: ${faceStr}`); } else { - logWarn(`Failed to connect to DeepStack server, Error: ${err}, Response time: ${duration} ms`); + logWarn(`Failed to connect to DeepStack server, Error: ${err}`); } } catch(ex) { - logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}, Response time: ${duration} ms`); + logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}`); } }; -const onImageProcessed = (d, tx, err, res, body, imageStream) => { +const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { const duration = !!res ? res.duration : 0; - let objects = []; + const result = []; try { if(err) { @@ -297,11 +395,13 @@ const onImageProcessed = (d, tx, err, res, body, imageStream) => { const predictions = response.predictions; if(predictions !== null && predictions.length > 0) { - objects = predictions.map(p => getDeepStackObject(p)).filter(p => !!p); + const predictionDescriptons = predictions.map(p => getPredictionDescripton(p)).filter(p => !!p); - if(objects.length > 0) { - const identified = objects.filter(p => p.tag !== FACE_UNKNOWN); - const unknownCount = objects.length - identified.length; + result.push(...predictionDescriptons); + + if(predictionDescriptons.length > 0) { + const identified = predictionDescriptons.filter(p => p.tag !== FACE_UNKNOWN); + const unknownCount = predictionDescriptons.length - identified.length; if(unknownCount > 0) { logInfo(`${d.id} detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); @@ -338,18 +438,18 @@ const onImageProcessed = (d, tx, err, res, body, imageStream) => { imgWidth: height, time: duration }, - frame: imageStream + frame: frameBuffer }; tx(eventData); } } - } + } } catch(ex) { logError(`Error while processing image, Error: ${ex} | ${err},, Response time: ${duration} ms, Body: ${body}`); } - return objects + return result }; const getFormData = (endpoint, additionalParameters) => { @@ -374,7 +474,7 @@ const getFormData = (endpoint, additionalParameters) => { return requestData; }; -const getDeepStackObject = (prediction) => { +const getPredictionDescripton = (prediction) => { if(prediction === undefined) { return null; } @@ -399,14 +499,39 @@ const getDeepStackObject = (prediction) => { }; if (detectorSettings.type === DETECTOR_TYPE_FACE) { - const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) - const person = matchingPersons.length > 0 ? matchingPersons[0] : null; + const legacyFaces = detectorSettings.faces.legacy || []; - obj["person"] = person; - } - + if (legacyFaces.includes(tag)) { + const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) + obj.person = matchingPersons.length > 0 ? matchingPersons[0] : null; + + } else { + const shinobiFileParts = tag.split(DELIMITER); + obj.person = shinobiFileParts[0]; + } + } return obj; }; +const onRecompileFaceDescriptors = (d) => { + if(detectorSettings.faces.shinobi !== d.faces) { + detectorSettings.faces.shinobi = d.faces; + + compareShinobiVSServer(); + } +}; + +s.MainEventController = (d,cn,tx) => { + const handler = detectorSettings.eventMapping[d.f]; + + if (handler !== undefined) { + try { + handler(d); + } catch (error) { + logError(`Failed to handle event ${d.f}, Error: ${error}`); + } + } +} + initialize(); diff --git a/plugins/deepstack-object/shinobi-deepstack-object.js b/plugins/deepstack-object/shinobi-deepstack-object.js index 54acd144..4de09449 100644 --- a/plugins/deepstack-object/shinobi-deepstack-object.js +++ b/plugins/deepstack-object/shinobi-deepstack-object.js @@ -51,6 +51,8 @@ if(s === null) { let detectorSettings = null; +const DELIMITER = "___"; + const DETECTOR_TYPE_FACE = 'face'; const DETECTOR_TYPE_OBJECT = 'object'; @@ -61,10 +63,13 @@ const DETECTOR_CONFIGUTATION = { face: { detectEndpoint: '/vision/face/recognize', startupEndpoint: '/vision/face/list', + registerEndpoint: '/vision/face/register', + deleteEndpoint: '/vision/face/delete', key: 'userid' }, object: { detectEndpoint: '/vision/detection', + startupEndpoint: '/vision/detection', key: 'label' } } @@ -103,7 +108,7 @@ const postMessage = (data) => { const initialize = () => { const deepStackProtocol = PROTOCOLS[config.deepStack.isSSL]; - baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; + const baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; const detectionType = config.plug.split("-")[1].toLowerCase(); const detectorConfig = DETECTOR_CONFIGUTATION[detectionType]; @@ -113,67 +118,162 @@ const initialize = () => { type: detectionType, active: false, baseUrl: baseUrl, - apiKey: config.deepStack.apiKey + apiKey: config.deepStack.apiKey, + faces: { + shinobi: null, + server: null, + legacy: null + }, + eventMapping: { + "recompileFaceDescriptors": onRecompileFaceDescriptors + } }; - - if(detectionType === DETECTOR_TYPE_FACE) { - detectorSettings["registeredPersons"] = config.persons === undefined ? [] : config.persons; - } - + detectorConfigKeys.forEach(k => detectorSettings[k] = detectorConfig[k]); - const testRequestData = getFormData(detectorSettings.detectEndpoint); + const startupRequestData = getFormData(detectorSettings.startupEndpoint); - request.post(testRequestData, (err, res, body) => { - try { - if(err) { - throw err; - } - + request.post(startupRequestData, handleStartupResponse); +}; + +const handleStartupResponse = (err, res, body) => { + try { + if(err) { + logError(`Failed to initialize ${config.plug} plugin, Error: ${err}`); + + } else { const response = JSON.parse(body); - + if(response.error) { detectorSettings.active = !response.error.endsWith('endpoint not activated'); } else { detectorSettings.active = response.success; } - const detectorSettingsKeys = Object.keys(detectorSettings); - - const pluginMessageHeader = []; - pluginMessageHeader.push(`${config.plug} loaded`); - - const configMessage = detectorSettingsKeys.map(k => `${k}: ${detectorSettings[k]}`); - - const fullPluginMessage = pluginMessageHeader.concat(configMessage); - - const pluginMessage = fullPluginMessage.join(", "); - - logInfo(pluginMessage); + logInfo(`${config.plug} loaded, Configuration: ${JSON.stringify(detectorSettings)}`); if (detectorSettings.active) { s.detectObject = detectObject; - if(detectionType === DETECTOR_TYPE_FACE) { - const requestData = getFormData(detectorSettings.startupEndpoint); - const requestTime = getCurrentTimestamp(); - - request.post(requestData, (errStartup, resStartup, bodyStartup) => { - if (!!resStartup) { - resStartup.duration = getDuration(requestTime); - } - - onFaceListResult(errStartup, resStartup, bodyStartup); - }); + if(detectorSettings.type === DETECTOR_TYPE_FACE) { + onFaceListResult(err, res, body); } - } - } catch(ex) { - logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`) + } } - }); + + + } catch(ex) { + logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`); + } }; -const processImage = (imageB64, d, tx, frameLocation, callback) => { +const registerFace = (serverFileName) => { + const shinobiFileParts = serverFileName.split(DELIMITER); + const faceName = shinobiFileParts[0]; + const image = shinobiFileParts[1]; + + const frameLocation = `${config.facesFolder}${faceName}/${image}`; + + const imageStream = fs.createReadStream(frameLocation); + + const form = { + image: imageStream, + userId: serverFileName + }; + + const requestData = getFormData(detectorSettings.registerEndpoint, form); + + request.post(requestData, (err, res, body) => { + if (err) { + logError(`Failed to register face, Face: ${faceName}, Image: ${image}`); + } else { + logInfo(`Register face, Face: ${faceName}, Image: ${image}`); + } + }); +}; + +const unregisterFace = (serverFileName) => { + const form = { + userId: serverFileName + }; + + const requestData = getFormData(detectorSettings.deleteEndpoint, form); + + request.post(requestData, (err, res, body) => { + if (err) { + logError(`Failed to delete face, UserID: ${serverFileName}`); + } else { + logInfo(`Deleted face, UserID: ${serverFileName}`); + } + }); +}; + +const getServerFileNameByShinobi = (name, image) => { + const fileName = `${name}${DELIMITER}${image}`; + + return fileName; +} + +const compareShinobiVSServer = () => { + const allFaces = detectorSettings.faces; + const shinobiFaces = allFaces.shinobi; + const serverFaces = allFaces.server; + const compareShinobiVSServerDelayID = detectorSettings.compareShinobiVSServerDelayID || null; + + if (compareShinobiVSServerDelayID !== null) { + clearTimeout(compareShinobiVSServerDelayID) + } + + if(serverFaces === null || shinobiFaces === null) { + detectorSettings.compareShinobiVSServerDelayID = setTimeout(compareShinobiVSServer, 5000); + logWarn("AI Server not ready yet, will retry in 5 seconds"); + + return; + } + + const shinobiFaceKeys = Object.keys(shinobiFaces); + + const shinobiFiles = shinobiFaceKeys.length === 0 ? + [] : + shinobiFaceKeys + .map(faceName => { + const value = shinobiFaces[faceName].map(image => getServerFileNameByShinobi(faceName, image)); + + return value; + }) + .reduce((acc, item) => { + console.log("acc: ", acc); + console.log("item: ", item); + + const result = [...acc, ...item]; + + return result; + }); + + const facesToRegister = shinobiFiles.filter(f => !serverFaces.includes(f)); + const facesToUnregister = serverFaces.filter(f => !shinobiFiles.includes(f)); + + if(facesToRegister.length > 0) { + logInfo(`Registering the following faces: ${facesToRegister}`); + facesToRegister.forEach(f => registerFace(f)); + } + + if(facesToUnregister.length > 0) { + const allowUnregister = detectorSettings.fullyControlledByFaceManager || false; + + if(allowUnregister) { + logInfo(`Unregister the following faces: ${facesToUnregister}`); + + facesToUnregister.forEach(f => unregisterFace(f)); + } else { + logInfo(`Skip unregistering the following faces: ${facesToUnregister}`); + + detectorSettings.faces.legacy = facesToUnregister; + } + } +}; + +const processImage = (frameBuffer, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; } @@ -195,11 +295,11 @@ const processImage = (imageB64, d, tx, frameLocation, callback) => { res.duration = getDuration(requestTime); } - onImageProcessed(d, tx, err, res, body, imageB64); + onImageProcessed(d, tx, err, res, body, frameBuffer); fs.unlinkSync(frameLocation); }); - }catch(ex){ + } catch(ex) { logError(`Failed to process image, Error: ${ex}`); if(fs.existsSync(frameLocation)) { @@ -221,21 +321,19 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { d.dir = `${s.dir.streams}${d.ke}/${d.id}/`; - frameLocation = `${d.dir}${s.gid(5)}.jpg`; + const path = `${d.dir}${s.gid(5)}.jpg`; if(!fs.existsSync(d.dir)) { fs.mkdirSync(d.dir, dirCreationOptions); } - fs.writeFile(frameLocation, frameBuffer, function(err) { + fs.writeFile(path, frameBuffer, function(err) { if(err) { return s.systemLog(err); } try { - const imageB64 = frameBuffer.toString('base64'); - - processImage(imageB64, d, tx, frameLocation, callback); + processImage(frameBuffer, d, tx, path, callback); } catch(ex) { logError(`Detector failed to parse frame, Error: ${ex}`); @@ -260,8 +358,6 @@ const getDuration = (requestTime) => { }; const onFaceListResult = (err, res, body) => { - const duration = !!res ? res.duration : 0; - try { const response = JSON.parse(body); @@ -269,20 +365,22 @@ const onFaceListResult = (err, res, body) => { const facesArr = response.faces; const faceStr = facesArr.join(","); + detectorSettings.faces.server = facesArr; + if(success) { - logInfo(`DeepStack loaded with the following faces: ${faceStr}, Response time: ${duration} ms`); + logInfo(`DeepStack loaded with the following faces: ${faceStr}`); } else { - logWarn(`Failed to connect to DeepStack server, Error: ${err}, Response time: ${duration} ms`); + logWarn(`Failed to connect to DeepStack server, Error: ${err}`); } } catch(ex) { - logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}, Response time: ${duration} ms`); + logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}`); } }; -const onImageProcessed = (d, tx, err, res, body, imageStream) => { +const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { const duration = !!res ? res.duration : 0; - let objects = []; + const result = []; try { if(err) { @@ -297,11 +395,13 @@ const onImageProcessed = (d, tx, err, res, body, imageStream) => { const predictions = response.predictions; if(predictions !== null && predictions.length > 0) { - objects = predictions.map(p => getDeepStackObject(p)).filter(p => !!p); + const predictionDescriptons = predictions.map(p => getPredictionDescripton(p)).filter(p => !!p); - if(objects.length > 0) { - const identified = objects.filter(p => p.tag !== FACE_UNKNOWN); - const unknownCount = objects.length - identified.length; + result.push(...predictionDescriptons); + + if(predictionDescriptons.length > 0) { + const identified = predictionDescriptons.filter(p => p.tag !== FACE_UNKNOWN); + const unknownCount = predictionDescriptons.length - identified.length; if(unknownCount > 0) { logInfo(`${d.id} detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); @@ -338,18 +438,18 @@ const onImageProcessed = (d, tx, err, res, body, imageStream) => { imgWidth: height, time: duration }, - frame: imageStream + frame: frameBuffer }; tx(eventData); } } - } + } } catch(ex) { logError(`Error while processing image, Error: ${ex} | ${err},, Response time: ${duration} ms, Body: ${body}`); } - return objects + return result }; const getFormData = (endpoint, additionalParameters) => { @@ -374,7 +474,7 @@ const getFormData = (endpoint, additionalParameters) => { return requestData; }; -const getDeepStackObject = (prediction) => { +const getPredictionDescripton = (prediction) => { if(prediction === undefined) { return null; } @@ -399,14 +499,39 @@ const getDeepStackObject = (prediction) => { }; if (detectorSettings.type === DETECTOR_TYPE_FACE) { - const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) - const person = matchingPersons.length > 0 ? matchingPersons[0] : null; + const legacyFaces = detectorSettings.faces.legacy || []; - obj["person"] = person; - } - + if (legacyFaces.includes(tag)) { + const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) + obj.person = matchingPersons.length > 0 ? matchingPersons[0] : null; + + } else { + const shinobiFileParts = tag.split(DELIMITER); + obj.person = shinobiFileParts[0]; + } + } return obj; }; +const onRecompileFaceDescriptors = (d) => { + if(detectorSettings.faces.shinobi !== d.faces) { + detectorSettings.faces.shinobi = d.faces; + + compareShinobiVSServer(); + } +}; + +s.MainEventController = (d,cn,tx) => { + const handler = detectorSettings.eventMapping[d.f]; + + if (handler !== undefined) { + try { + handler(d); + } catch (error) { + logError(`Failed to handle event ${d.f}, Error: ${error}`); + } + } +} + initialize(); diff --git a/web/assets/js/super.faceManager.js b/web/assets/js/super.faceManager.js index 0b4f4850..065b441a 100644 --- a/web/assets/js/super.faceManager.js +++ b/web/assets/js/super.faceManager.js @@ -447,8 +447,6 @@ $(document).ready(() => { } }; - window.moduleData = moduleData; - $.ccio.ws.on("f", (d) => { const handler = moduleData.eventMapper[d.f]; From 38a6b47f1130c4ac85dfda6f8d9a42d3f8d96ec4 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 9 Dec 2022 12:44:42 +0200 Subject: [PATCH 04/11] Add enableFaceManager configuration parameter, fix missing fields in configuration editor, removed those are available in configuration but without definition in schema --- libs/config.js | 1 + libs/faceManager.js | 4 + web/assets/js/super.configEditor.js | 645 +++++++++++++++------------- web/assets/js/super.faceManager.js | 9 +- web/pages/super.ejs | 16 +- 5 files changed, 359 insertions(+), 316 deletions(-) diff --git a/libs/config.js b/libs/config.js index adc0fdd2..23238875 100644 --- a/libs/config.js +++ b/libs/config.js @@ -36,6 +36,7 @@ module.exports = function(s){ if(config.cron.interval === undefined)config.cron.interval=1; if(config.databaseType === undefined){config.databaseType='mysql'} if(config.pluginKeys === undefined)config.pluginKeys={}; + if(config.enableFaceManager === undefined)config.enableFaceManager=false; if(config.databaseLogs === undefined){config.databaseLogs=false} if(config.useUTC === undefined){config.useUTC=false} if(config.iconURL === undefined){config.iconURL = "https://shinobi.video/libs/assets/icon/apple-touch-icon-152x152.png"} diff --git a/libs/faceManager.js b/libs/faceManager.js index d91d5e2e..6ed2b8a1 100644 --- a/libs/faceManager.js +++ b/libs/faceManager.js @@ -310,6 +310,10 @@ module.exports = (s, config, lang, app, io) => { }; const initialize = () => { + if(!config.enableFaceManager) { + return; + } + createDirectory(); registerGetEndpoint('s', handleGetFaces); diff --git a/web/assets/js/super.configEditor.js b/web/assets/js/super.configEditor.js index f63bc7b1..5a06d082 100644 --- a/web/assets/js/super.configEditor.js +++ b/web/assets/js/super.configEditor.js @@ -1,321 +1,360 @@ $(document).ready(function(){ - var schema = { - "title": "Main Configuration", - "type": "object", - "properties": { - "debugLog": { - "type": "boolean", - "default": false - }, - "subscriptionId": { - "type": "string", - }, - "port": { - "type": "integer", - "default": 8080 - }, - "passwordType": { - "type": "string", - "enum": [ - "sha256", - "sha512", - "md5" - ], - "default": "sha256" - }, - "addStorage": { - "type": "array", - "format": "table", - "title": "Additional Storage", - "description": "Separate storage locations that can be set for different monitors.", - "uniqueItems": true, - "items": { - "type": "object", - "title": "Storage Array", - "properties": { - "name": { - "type": "string", - }, - "path": { - "type": "string", - "default": "__DIR__/videos2" - } - } - }, - "default": [ - { - "name": "second", - "path": "__DIR__/videos2" - } - ] - }, - "plugins": { - "type": "array", - "format": "table", - "title": "Plugins", - "descripton": "Elaborate Plugin connection settings.", - "uniqueItems": true, - "items": { - "type": "object", - "title": "Plugin", - "properties": { - "plug": { - "type": "string", - "default": "pluginName" - }, - "key": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "host", - "client" - ], - "default": "client" - }, - "https": { - "type": "boolean", - "descripton": "Only for Host mode.", - "default": false - }, - "host": { - "type": "string", - "descripton": "Only for Host mode.", - "default": "localhost" - }, - "port": { - "type": "integer", - "descripton": "Only for Host mode.", - "default": 8082 - }, - "type": { - "type": "string", - "default": "detector" - } - } - }, - "default": [ - { - "name": "second", - "path": "__DIR__/videos2" - } - ] - }, - "pluginKeys": { + var schema = { + "title": "Main Configuration", + "type": "object", + "properties": { + "debugLog": { + "type": "boolean", + "default": false + }, + "subscriptionId": { + "type": "string" + }, + "port": { + "type": "integer", + "default": 8080 + }, + "passwordType": { + "type": "string", + "enum": [ + "sha256", + "sha512", + "md5" + ], + "default": "sha256" + }, + "addStorage": { + "type": "array", + "format": "table", + "title": "Additional Storage", + "description": "Separate storage locations that can be set for different monitors.", + "uniqueItems": true, + "items": { "type": "object", - "format": "table", - "title": "Plugin Keys", - "description": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.", - "uniqueItems": true, - "items": { - "type": "object", - "title": "Plugin Key", - "properties": {} + "title": "Storage Array", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string", + "default": "__DIR__/videos2" + } } }, - "db": { - "type": "object", - "format": "table", - "title": "Database Options", - "description": "Credentials to connect to where detailed information is stored.", - "properties": { - "host": { - "type": "string", - "default": "127.0.0.1" - }, - "user": { - "type": "string", - "default": "majesticflame" - }, - "password": { - "type": "string", - "default": "" - }, - "database": { - "type": "string", - "default": "ccio" - }, - "port": { - "type": "integer", - "default": 3306 - } + "default": [ + { + "name": "second", + "path": "__DIR__/videos2" + } + ] + }, + "plugins": { + "type": "array", + "format": "table", + "title": "Plugins", + "descripton": "Elaborate Plugin connection settings.", + "uniqueItems": true, + "items": { + "type": "object", + "title": "Plugin", + "properties": { + "plug": { + "type": "string", + "default": "pluginName" }, - "default": { - "host": "127.0.0.1", - "user": "majesticflame", - "password": "", - "database": "ccio", - "port":3306 + "key": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "host", + "client" + ], + "default": "client" + }, + "https": { + "type": "boolean", + "descripton": "Only for Host mode.", + "default": false + }, + "host": { + "type": "string", + "descripton": "Only for Host mode.", + "default": "localhost" + }, + "port": { + "type": "integer", + "descripton": "Only for Host mode.", + "default": 8082 + }, + "type": { + "type": "string", + "default": "detector" } + } }, - "cron": { - "type": "object", - "format": "table", - "title": "CRON Options", - "properties": { - "key": { - "type": "string", - }, - "deleteOld": { - "type": "boolean", - "description": "cron will delete videos older than Max Number of Days per account.", - "default": true - }, - "deleteNoVideo": { - "type": "boolean", - "description": "cron will delete SQL rows that it thinks have no video files.", - "default": true - }, - "deleteOverMax": { - "type": "boolean", - "description": "cron will delete files that are over the set maximum storage per account.", - "default": true - }, - } + "default": null + }, + "pluginKeys": { + "type": "object", + "format": "table", + "title": "Plugin Keys", + "description": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.", + "uniqueItems": true, + "items": { + "type": "object", + "title": "Plugin Key", + "properties": {} + } + }, + "enableFaceManager": { + "type": "boolean", + "title": "Enable Face Manager", + "description": "Super dashboard and API to register and unregister faces with images for Face Recognition plugins", + "default": false + }, + "db": { + "type": "object", + "format": "table", + "title": "Database Options", + "description": "Credentials to connect to where detailed information is stored.", + "properties": { + "host": { + "type": "string", + "default": "127.0.0.1" + }, + "user": { + "type": "string", + "default": "majesticflame" + }, + "password": { + "type": "string", + "default": "" + }, + "database": { + "type": "string", + "default": "ccio" + }, + "port": { + "type": "integer", + "default": 3306 + } }, - "mail": { + "default": { + "host": "127.0.0.1", + "user": "majesticflame", + "password": "", + "database": "ccio", + "port": 3306 + } + }, + "cron": { + "type": "object", + "format": "table", + "title": "CRON Options", + "properties": { + "key": { + "type": "string" + }, + "deleteOld": { + "type": "boolean", + "description": "cron will delete videos older than Max Number of Days per account.", + "default": true + }, + "deleteNoVideo": { + "type": "boolean", + "description": "cron will delete SQL rows that it thinks have no video files.", + "default": true + }, + "deleteOverMax": { + "type": "boolean", + "description": "cron will delete files that are over the set maximum storage per account.", + "default": true + } + } + }, + "mail": { + "type": "object", + "format": "table", + "title": "Email Options", + "properties": { + "service": { + "type": "string" + }, + "host": { + "type": "string" + }, + "auth": { "type": "object", - "format": "table", - "title": "Email Options", "properties": { - "service": { - "type": "string", + "user": { + "type": "string" }, - "host": { - "type": "string", - }, - "auth": { - "type": "object", - "properties": { - "user": { - "type": "string", - }, - "pass": { - "type": "string", - }, - }, - }, - "secure": { - "type": "boolean", - "default": false - }, - "ignoreTLS": { - "type": "boolean", - }, - "requireTLS": { - "type": "boolean", - }, - "port": { - "type": "integer", + "pass": { + "type": "string" } } - }, - "detectorMergePamRegionTriggers": { - "type": "boolean", - "default": true - }, - "doSnapshot": { - "type": "boolean", - "default": true - }, - "discordBot": { - "type": "boolean", - "default": false - }, - "dropInEventServer": { - "type": "boolean", - "default": false - }, - "ftpServer": { - "type": "boolean", - "default": false - }, - "oldPowerVideo": { - "type": "boolean", - "default": false - }, - "wallClockTimestampAsDefault": { - "type": "boolean", - "default": true - }, - "defaultMjpeg": { - "type": "string", - }, - "streamDir": { - "type": "string", - }, - "videosDir": { - "type": "string", - }, - "windowsTempDir": { - "type": "string", + }, + "secure": { + "type": "boolean", + "default": false, + "format": "checkbox" + }, + "ignoreTLS": { + "type": "boolean" + }, + "requireTLS": { + "type": "boolean" + }, + "port": { + "type": "integer" + } } + }, + "detectorMergePamRegionTriggers": { + "type": "boolean", + "default": true + }, + "doSnapshot": { + "type": "boolean", + "default": true + }, + "discordBot": { + "type": "boolean", + "default": false + }, + "dropInEventServer": { + "type": "boolean", + "default": false + }, + "ftpServer": { + "type": "boolean", + "default": false + }, + "oldPowerVideo": { + "type": "boolean", + "default": false + }, + "wallClockTimestampAsDefault": { + "type": "boolean", + "default": true + }, + "defaultMjpeg": { + "type": "string" + }, + "streamDir": { + "type": "string" + }, + "videosDir": { + "type": "string" + }, + "windowsTempDir": { + "type": "string" } - }; - - var configurationTab = $('#config') - var configurationForm = configurationTab.find('form') - - // Set default options - JSONEditor.defaults.options.theme = 'bootstrap3'; - JSONEditor.defaults.options.iconlib = 'fontawesome4'; - - // Initialize the editor - var configurationEditor = new JSONEditor(document.getElementById("configForHumans"),{ - theme: 'bootstrap3', - schema: schema - }); - - function loadConfiguationIntoEditor(){ - $.get(superApiPrefix + $user.sessionKey + '/system/configure',function(data){ - configurationEditor.setValue(data.config); - }) } - // configurationEditor.on("change", function() { - // // Do something... - // }); - var submitConfiguration = function(){ - var errors = configurationEditor.validate(); - console.log(errors.length) - console.log(errors) - if(errors.length === 0) { - var newConfiguration = JSON.stringify(configurationEditor.getValue(),null,3) - var html = '

This is a change being applied to the configuration file (conf.json). Are you sure you want to do this? You must restart Shinobi for these changes to take effect. The JSON below is what you are about to save.

' - html += `
${newConfiguration}
` - $.confirm.create({ - title: 'Save Configuration', - body: html, - clickOptions: { - class: 'btn-success', - title: lang.Save, - }, - clickCallback: function(){ - $.post(superApiPrefix + $user.sessionKey + '/system/configure',{ - data: newConfiguration - },function(data){ - // console.log(data) - }) - } - }) - }else{ - new PNotify({text:'Invalid JSON Syntax, Cannot Save.',type:'error'}) + }; + + var configurationTab = $('#config') + var configurationForm = configurationTab.find('form') + + // Set default options + JSONEditor.defaults.options.theme = 'bootstrap3'; + JSONEditor.defaults.options.iconlib = 'fontawesome4'; + + // Initialize the editor + let configurationEditor = null; + + function loadConfiguationIntoEditor(){ + $.get(superApiPrefix + $user.sessionKey + '/system/configure', function(data){ + const dataConfig = data.config; + const dataConfigKeys = Object.keys(dataConfig); + const schemaItemsKeys = Object.keys(schema.properties); + + const schemaWithoutData = schemaItemsKeys.filter(sk => !dataConfigKeys.includes(sk)); + const dataWithoutSchema = dataConfigKeys.filter(dk => !schemaItemsKeys.includes(dk)); + + schemaWithoutData.forEach(sk => { + const schemaItem = schema.properties[sk]; + const defaultConfig = schemaItem.default; + + data.config[sk] = defaultConfig; + }); + + if(dataWithoutSchema.length > 0) { + dataWithoutSchema.forEach(dk => { + const schemaItem ={ + title: dk, + options: { + hidden: true + } + }; + + schema.properties[dk] = schemaItem; + }); + + configurationEditor = new JSONEditor(document.getElementById("configForHumans"),{ + theme: 'bootstrap3', + schema: schema + }); } - } - configurationTab.find('.submit').click(function(){ - submitConfiguration() - }) - configurationForm.submit(function(e){ - e.preventDefault() - submitConfiguration() - return false; - }) - $.ccio.ws.on('f',function(d){ - switch(d.f){ - case'init_success': - loadConfiguationIntoEditor() - break; - } - }) - window.configurationEditor = configurationEditor + + configurationEditor.setValue(data.config); + + window.configurationEditor = configurationEditor; + }); + } + + // configurationEditor.on("change", function() { + // // Do something... + // }); + var submitConfiguration = function(){ + var errors = configurationEditor.validate(); + console.log(errors.length) + console.log(errors) + if(errors.length === 0) { + var newConfiguration = JSON.stringify(configurationEditor.getValue(),null,3) + var html = '

This is a change being applied to the configuration file (conf.json). Are you sure you want to do this? You must restart Shinobi for these changes to take effect. The JSON below is what you are about to save.

' + html += `
${newConfiguration}
` + $.confirm.create({ + title: 'Save Configuration', + body: html, + clickOptions: { + class: 'btn-success', + title: lang.Save, + }, + clickCallback: function(){ + $.post(superApiPrefix + $user.sessionKey + '/system/configure',{ + data: newConfiguration + },function(data){ + // console.log(data) + }) + } + }) + }else{ + new PNotify({text:'Invalid JSON Syntax, Cannot Save.',type:'error'}) + } + } + + + + configurationTab.find('.submit').click(function(){ + submitConfiguration() + }); + + configurationForm.submit(function(e){ + e.preventDefault() + submitConfiguration() + return false; + }); + + $.ccio.ws.on('f',function(d){ + switch(d.f){ + case'init_success': + loadConfiguationIntoEditor() + break; + } + }) + }) diff --git a/web/assets/js/super.faceManager.js b/web/assets/js/super.faceManager.js index 065b441a..b0aadb42 100644 --- a/web/assets/js/super.faceManager.js +++ b/web/assets/js/super.faceManager.js @@ -173,10 +173,7 @@ $(document).ready(() => { const faceKeys = Object.keys(moduleData.faces); - if(faceKeys.length === 0) { - console.info("No face found"); - - } else { + if(faceKeys.length > 0) { if(moduleData.selectedFace === null) { moduleData.selectedFace = faceKeys[0]; } @@ -459,9 +456,7 @@ $(document).ready(() => { }, 100); } catch (error) { console.error(`Failed to handle event ${d.f}, Error: ${error}`); - } - - + } } }); diff --git a/web/pages/super.ejs b/web/pages/super.ejs index 076e9df1..6ff9051f 100644 --- a/web/pages/super.ejs +++ b/web/pages/super.ejs @@ -90,9 +90,11 @@ - + <% if(config.enableFaceManager) { %> + + <% } %>
@@ -129,9 +131,11 @@
<% include blocks/superPluginManager.ejs %>
-
- <% include blocks/superFaceManager.ejs %> -
+ <% if(config.enableFaceManager) { %> +
+ <% include blocks/superFaceManager.ejs %> +
+ <% } %>
From 335d2598ec7cba1f0d6557e4549feea56de32b7c Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 9 Dec 2022 16:39:32 +0200 Subject: [PATCH 05/11] fix submit client side failure --- web/assets/js/super.configEditor.js | 206 +++++++++++++++------------- 1 file changed, 113 insertions(+), 93 deletions(-) diff --git a/web/assets/js/super.configEditor.js b/web/assets/js/super.configEditor.js index 5a06d082..4c13e3c2 100644 --- a/web/assets/js/super.configEditor.js +++ b/web/assets/js/super.configEditor.js @@ -1,5 +1,5 @@ -$(document).ready(function(){ - var schema = { +$(document).ready(function () { + const schema = { "title": "Main Configuration", "type": "object", "properties": { @@ -256,105 +256,125 @@ $(document).ready(function(){ } }; - var configurationTab = $('#config') - var configurationForm = configurationTab.find('form') + const configurationTab = $("#config"); + const configurationForm = configurationTab.find("form"); - // Set default options - JSONEditor.defaults.options.theme = 'bootstrap3'; - JSONEditor.defaults.options.iconlib = 'fontawesome4'; - - // Initialize the editor - let configurationEditor = null; - - function loadConfiguationIntoEditor(){ - $.get(superApiPrefix + $user.sessionKey + '/system/configure', function(data){ - const dataConfig = data.config; - const dataConfigKeys = Object.keys(dataConfig); - const schemaItemsKeys = Object.keys(schema.properties); - - const schemaWithoutData = schemaItemsKeys.filter(sk => !dataConfigKeys.includes(sk)); - const dataWithoutSchema = dataConfigKeys.filter(dk => !schemaItemsKeys.includes(dk)); - - schemaWithoutData.forEach(sk => { - const schemaItem = schema.properties[sk]; - const defaultConfig = schemaItem.default; - - data.config[sk] = defaultConfig; - }); - - if(dataWithoutSchema.length > 0) { - dataWithoutSchema.forEach(dk => { - const schemaItem ={ - title: dk, - options: { - hidden: true - } - }; - - schema.properties[dk] = schemaItem; - }); - - configurationEditor = new JSONEditor(document.getElementById("configForHumans"),{ - theme: 'bootstrap3', - schema: schema - }); - } - - configurationEditor.setValue(data.config); - - window.configurationEditor = configurationEditor; - }); + const moduleData = { + endpoint: null, + configurationEditor: null } - + + const handleGetConfigurationData = data => { + const dataConfig = data.config; + const dataConfigKeys = Object.keys(dataConfig); + const schemaItemsKeys = Object.keys(schema.properties); + + const schemaWithoutData = schemaItemsKeys.filter( + (sk) => !dataConfigKeys.includes(sk) + ); + const dataWithoutSchema = dataConfigKeys.filter( + (dk) => !schemaItemsKeys.includes(dk) + ); + + schemaWithoutData.forEach((sk) => { + const schemaItem = schema.properties[sk]; + const defaultConfig = schemaItem.default; + + data.config[sk] = defaultConfig; + }); + + if (dataWithoutSchema.length > 0) { + dataWithoutSchema.forEach((dk) => { + const schemaItem = { + title: dk, + options: { + hidden: true, + }, + }; + + schema.properties[dk] = schemaItem; + }); + + // Set default options + JSONEditor.defaults.options.theme = "bootstrap3"; + JSONEditor.defaults.options.iconlib = "fontawesome4"; + } + + const configurationEditor = new JSONEditor( + document.getElementById("configForHumans"), { + schema: schema, + } + ); + + configurationEditor.setValue(data.config); + + moduleData.configurationEditor = configurationEditor; + window.configurationEditor = configurationEditor; + }; + + const handlePostConfigurationData = data => { + // console.log(data); + } + + function loadConfiguationIntoEditor(d) { + moduleData.endpoint = `${superApiPrefix}${$user.sessionKey}/system/configure`; + + $.get(moduleData.endpoint, handleGetConfigurationData); + } + // configurationEditor.on("change", function() { // // Do something... // }); - var submitConfiguration = function(){ - var errors = configurationEditor.validate(); - console.log(errors.length) - console.log(errors) - if(errors.length === 0) { - var newConfiguration = JSON.stringify(configurationEditor.getValue(),null,3) - var html = '

This is a change being applied to the configuration file (conf.json). Are you sure you want to do this? You must restart Shinobi for these changes to take effect. The JSON below is what you are about to save.

' - html += `
${newConfiguration}
` - $.confirm.create({ - title: 'Save Configuration', - body: html, - clickOptions: { - class: 'btn-success', - title: lang.Save, - }, - clickCallback: function(){ - $.post(superApiPrefix + $user.sessionKey + '/system/configure',{ - data: newConfiguration - },function(data){ - // console.log(data) - }) - } - }) - }else{ - new PNotify({text:'Invalid JSON Syntax, Cannot Save.',type:'error'}) - } - } + const submitConfiguration = function () { + const configurationEditor = moduleData.configurationEditor; + const errors = configurationEditor.validate(); - + if (errors.length === 0) { + const newConfiguration = JSON.stringify(configurationEditor.getValue(), null, 3); - configurationTab.find('.submit').click(function(){ - submitConfiguration() + const html = `

+ This is a change being applied to the configuration file (conf.json). + Are you sure you want to do this? You must restart Shinobi for these changes to take effect. + + The JSON below is what you are about to save. +

+
+                      ${newConfiguration}
+                    
`; + + $.confirm.create({ + title: "Save Configuration", + body: html, + clickOptions: { + class: "btn-success", + title: lang.Save, + }, + clickCallback: () => { + const requestData = { + data: newConfiguration + }; + + $.post(moduleData.endpoint, requestData, handlePostConfigurationData); + }, + }); + } else { + new PNotify({ text: "Invalid JSON Syntax, Cannot Save.", type: "error" }); + } + }; + + configurationTab.find(".submit").click(function () { + submitConfiguration(); }); - configurationForm.submit(function(e){ - e.preventDefault() - submitConfiguration() - return false; + configurationForm.submit(function (e) { + e.preventDefault(); + submitConfiguration(); + return false; }); - $.ccio.ws.on('f',function(d){ - switch(d.f){ - case'init_success': - loadConfiguationIntoEditor() - break; - } - }) - -}) + $.ccio.ws.on("f", d => { + if (d.f === "init_success") { + loadConfiguationIntoEditor(); + } + }); +}); From 330c6b751a5f7ceb3098b25cfa9adb5f299fd491 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 9 Dec 2022 21:17:49 +0200 Subject: [PATCH 06/11] Update configuration save / restore for docker --- Docker/init.sh | 2 +- libs/system/utils.js | 31 ++++++++++++++----------------- libs/webServerSuperPaths.js | 15 +++++---------- tools/modifyConfiguration.js | 14 ++++---------- 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/Docker/init.sh b/Docker/init.sh index 64b6c1db..5b3438ec 100644 --- a/Docker/init.sh +++ b/Docker/init.sh @@ -84,7 +84,7 @@ elif [ ! -e "./conf.json" ]; then cp conf.sample.json conf.json fi sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json -# node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG" +node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG" echo "=============" diff --git a/libs/system/utils.js b/libs/system/utils.js index c41c4f62..4c42f310 100644 --- a/libs/system/utils.js +++ b/libs/system/utils.js @@ -23,26 +23,23 @@ module.exports = (config) => { return response }, getConfiguration: () => { - return new Promise((resolve,reject) => { - fs.readFile(s.location.config,'utf8',function(err,data){ + return new Promise((resolve, reject) => { + const configPath = config.thisIsDocker ? "/config/conf.json" : s.location.config; + + fs.readFile(configPath, 'utf8', (err,data) => { resolve(JSON.parse(data)) - }) - }) + }); + }); }, modifyConfiguration: (postBody) => { - return new Promise((resolve,reject) => { - try{ - if(config.thisIsDocker){ - const dockerConfigFile = '/config/conf.json' - fs.writeFileSync(dockerConfigFile,JSON.stringify(postBody,null,3)) - } - }catch(err){ - console.log(err) - } - fs.writeFile(s.location.config,JSON.stringify(postBody,null,3),function(err){ - resolve(err) - }) - }) + return new Promise((resolve, reject) => { + console.log(s.location.config) + + const configPath = config.thisIsDocker ? "/config/conf.json" : s.location.config; + const configData = JSON.stringify(postBody,null,3); + console.log(postBody) + fs.writeFile(configPath, configData, resolve); + }); }, updateSystem: () => { return new Promise((resolve,reject) => { diff --git a/libs/webServerSuperPaths.js b/libs/webServerSuperPaths.js index 33a29754..ef7292c7 100644 --- a/libs/webServerSuperPaths.js +++ b/libs/webServerSuperPaths.js @@ -250,17 +250,12 @@ module.exports = function(s,config,lang,app){ currentSuperUserList.push(currentSuperUser) } //update master list in system - try{ - if(config.thisIsDocker){ - const dockerSuperFile = '/config/super.json' - fs.writeFileSync(dockerSuperFile,JSON.stringify(currentSuperUserList,null,3)) - } - }catch(err){ - console.log(err) - } - fs.writeFile(s.location.super,JSON.stringify(currentSuperUserList,null,3),function(){ + const configPath = config.thisIsDocker ? "/config/super.json" : s.location.super; + const configData = JSON.stringify(postBody,null,3); + fs.writeFile(configPath, configData, () => { s.tx({f:'save_preferences'},'$') - }) + }); + }else{ endData.ok = false endData.msg = lang.postDataBroken diff --git a/tools/modifyConfiguration.js b/tools/modifyConfiguration.js index e552b742..981fa594 100644 --- a/tools/modifyConfiguration.js +++ b/tools/modifyConfiguration.js @@ -56,16 +56,10 @@ processArgv.forEach(function(val) { console.log(index + ': ' + value); }); -try{ - if(config.thisIsDocker){ - const dockerConfigFile = '/config/conf.json' - fs.writeFileSync(dockerConfigFile,JSON.stringify(config,null,3)) - } -}catch(err){ - console.log(err) -} +const configPath = config.thisIsDocker ? "/config/conf.json" : configLocation; +const configData = JSON.stringify(config,null,3); -fs.writeFile(configLocation,JSON.stringify(config,null,3),function(){ +fs.writeFile(configPath, configData, () =>{ console.log('Changes Complete. Here is what it is now.') console.log(JSON.stringify(config,null,2)) -}) +}); From a7f70803d6611a7861df4d220e60435f5fa95849 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 9 Dec 2022 21:18:10 +0200 Subject: [PATCH 07/11] Fix register / unregister --- libs/faceManager.js | 14 +++++------ .../deepstack-face/shinobi-deepstack-face.js | 25 ++++++++++--------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/libs/faceManager.js b/libs/faceManager.js index 6ed2b8a1..ad7374e0 100644 --- a/libs/faceManager.js +++ b/libs/faceManager.js @@ -93,14 +93,14 @@ module.exports = (s, config, lang, app, io) => { const checkFile = (file, name, authUser) => { const fileName = file.name; const fileParts = fileName.split("."); - const fileExt = fileParts[fileParts.length]; - const allowedExtensions = ["jpeg", "jpg"]; + const fileExt = fileParts[fileParts.length - 1]; + const allowedExtensions = ["jpeg", "jpg", "png"]; const canUpload = allowedExtensions.includes(fileExt); const result = canUpload ? fileName : null; - + if(canUpload) { - const facePath = getFacePath(req.params.name); - const imagePath = getImagePath(req.params.name, fileName); + const facePath = getFacePath(name); + const imagePath = getImagePath(name, fileName); if(!fs.existsSync(facePath)){ fs.mkdirSync(facePath); @@ -108,7 +108,7 @@ module.exports = (s, config, lang, app, io) => { file.mv(imagePath, function(err) { if(err) { - console.error(`Failed to store image in ${imagePath}`); + console.error(`Failed to store image in ${imagePath}, Error: ${err}`); } else { notifyImageUploaded(name, fileName, authUser); } @@ -264,7 +264,7 @@ module.exports = (s, config, lang, app, io) => { if(fileKeys.length == 0){ return res.status(400).send('No files were uploaded.'); } - + fileKeys.forEach(key => { const file = req.files[key]; diff --git a/plugins/deepstack-face/shinobi-deepstack-face.js b/plugins/deepstack-face/shinobi-deepstack-face.js index 77a72074..65913e16 100644 --- a/plugins/deepstack-face/shinobi-deepstack-face.js +++ b/plugins/deepstack-face/shinobi-deepstack-face.js @@ -1,3 +1,4 @@ +// // Shinobi - DeepStack Face Recognition Plugin // Copyright (C) 2021 Elad Bar // @@ -123,6 +124,7 @@ const initialize = () => { server: null, legacy: null }, + facesPath: null, eventMapping: { "recompileFaceDescriptors": onRecompileFaceDescriptors } @@ -167,24 +169,25 @@ const handleStartupResponse = (err, res, body) => { }; const registerFace = (serverFileName) => { + const facesPath = detectorSettings.facesPath; const shinobiFileParts = serverFileName.split(DELIMITER); const faceName = shinobiFileParts[0]; const image = shinobiFileParts[1]; - const frameLocation = `${config.facesFolder}${faceName}/${image}`; + const frameLocation = `${facesPath}${faceName}/${image}`; const imageStream = fs.createReadStream(frameLocation); const form = { image: imageStream, - userId: serverFileName + userid: serverFileName }; const requestData = getFormData(detectorSettings.registerEndpoint, form); request.post(requestData, (err, res, body) => { if (err) { - logError(`Failed to register face, Face: ${faceName}, Image: ${image}`); + logError(`Failed to register face, Face: ${faceName}, Image: ${image}, Error: ${err}`); } else { logInfo(`Register face, Face: ${faceName}, Image: ${image}`); } @@ -193,14 +196,14 @@ const registerFace = (serverFileName) => { const unregisterFace = (serverFileName) => { const form = { - userId: serverFileName + userid: serverFileName }; const requestData = getFormData(detectorSettings.deleteEndpoint, form); request.post(requestData, (err, res, body) => { if (err) { - logError(`Failed to delete face, UserID: ${serverFileName}`); + logError(`Failed to delete face, UserID: ${serverFileName}, Error: ${err}`); } else { logInfo(`Deleted face, UserID: ${serverFileName}`); } @@ -214,7 +217,7 @@ const getServerFileNameByShinobi = (name, image) => { } const compareShinobiVSServer = () => { - const allFaces = detectorSettings.faces; + const allFaces = detectorSettings.faces; const shinobiFaces = allFaces.shinobi; const serverFaces = allFaces.server; const compareShinobiVSServerDelayID = detectorSettings.compareShinobiVSServerDelayID || null; @@ -241,9 +244,6 @@ const compareShinobiVSServer = () => { return value; }) .reduce((acc, item) => { - console.log("acc: ", acc); - console.log("item: ", item); - const result = [...acc, ...item]; return result; @@ -320,19 +320,19 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { d.dir = `${s.dir.streams}${d.ke}/${d.id}/`; - const filePath = `${d.dir}${s.gid(5)}.jpg`; + const path = `${d.dir}${s.gid(5)}.jpg`; if(!fs.existsSync(d.dir)) { fs.mkdirSync(d.dir, dirCreationOptions); } - fs.writeFile(filePath, frameBuffer, function(err) { + fs.writeFile(path, frameBuffer, function(err) { if(err) { return s.systemLog(err); } try { - processImage(frameBuffer, d, tx, filePath, callback); + processImage(frameBuffer, d, tx, path, callback); } catch(ex) { logError(`Detector failed to parse frame, Error: ${ex}`); @@ -516,6 +516,7 @@ const getPredictionDescripton = (prediction) => { const onRecompileFaceDescriptors = (d) => { if(detectorSettings.faces.shinobi !== d.faces) { detectorSettings.faces.shinobi = d.faces; + detectorSettings.facesPath = d.path; compareShinobiVSServer(); } From 57d2e3879df55bfafa184e1d9c457651784d3725 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 9 Dec 2022 22:17:27 +0200 Subject: [PATCH 08/11] Fix plugin configuration --- plugins/deepstack-face/shinobi-deepstack-face.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/deepstack-face/shinobi-deepstack-face.js b/plugins/deepstack-face/shinobi-deepstack-face.js index 65913e16..25eaab9c 100644 --- a/plugins/deepstack-face/shinobi-deepstack-face.js +++ b/plugins/deepstack-face/shinobi-deepstack-face.js @@ -113,12 +113,12 @@ const initialize = () => { const detectionType = config.plug.split("-")[1].toLowerCase(); const detectorConfig = DETECTOR_CONFIGUTATION[detectionType]; const detectorConfigKeys = Object.keys(detectorConfig); - detectorSettings = { type: detectionType, active: false, baseUrl: baseUrl, apiKey: config.deepStack.apiKey, + fullyControlledByFaceManager: config.fullyControlledByFaceManager || false, faces: { shinobi: null, server: null, @@ -195,6 +195,10 @@ const registerFace = (serverFileName) => { }; const unregisterFace = (serverFileName) => { + if (serverFileName === null) { + return; + } + const form = { userid: serverFileName }; @@ -216,12 +220,12 @@ const getServerFileNameByShinobi = (name, image) => { return fileName; } -const compareShinobiVSServer = () => { +const compareShinobiVSServer = () => { const allFaces = detectorSettings.faces; const shinobiFaces = allFaces.shinobi; const serverFaces = allFaces.server; const compareShinobiVSServerDelayID = detectorSettings.compareShinobiVSServerDelayID || null; - + if (compareShinobiVSServerDelayID !== null) { clearTimeout(compareShinobiVSServerDelayID) } @@ -268,8 +272,9 @@ const compareShinobiVSServer = () => { logInfo(`Skip unregistering the following faces: ${facesToUnregister}`); detectorSettings.faces.legacy = facesToUnregister; - } + } } + }; const processImage = (frameBuffer, d, tx, frameLocation, callback) => { @@ -517,8 +522,8 @@ const onRecompileFaceDescriptors = (d) => { if(detectorSettings.faces.shinobi !== d.faces) { detectorSettings.faces.shinobi = d.faces; detectorSettings.facesPath = d.path; - compareShinobiVSServer(); + } }; From 3bbd0d30db3a14c9a30e4a8d8a81567b712c130f Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 9 Dec 2022 22:42:11 +0200 Subject: [PATCH 09/11] reload DeepStack / CodeProject AI data after registering / unregistering --- .../deepstack-face/shinobi-deepstack-face.js | 10 +++++-- web/assets/js/super.faceManager.js | 30 ------------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/plugins/deepstack-face/shinobi-deepstack-face.js b/plugins/deepstack-face/shinobi-deepstack-face.js index 25eaab9c..a41ab417 100644 --- a/plugins/deepstack-face/shinobi-deepstack-face.js +++ b/plugins/deepstack-face/shinobi-deepstack-face.js @@ -255,6 +255,7 @@ const compareShinobiVSServer = () => { const facesToRegister = shinobiFiles.filter(f => !serverFaces.includes(f)); const facesToUnregister = serverFaces.filter(f => !shinobiFiles.includes(f)); + const allowUnregister = detectorSettings.fullyControlledByFaceManager || false; if(facesToRegister.length > 0) { logInfo(`Registering the following faces: ${facesToRegister}`); @@ -262,8 +263,6 @@ const compareShinobiVSServer = () => { } if(facesToUnregister.length > 0) { - const allowUnregister = detectorSettings.fullyControlledByFaceManager || false; - if(allowUnregister) { logInfo(`Unregister the following faces: ${facesToUnregister}`); @@ -274,7 +273,12 @@ const compareShinobiVSServer = () => { detectorSettings.faces.legacy = facesToUnregister; } } - + + if(facesToRegister.length > 0 || (facesToUnregister.length > 0 && allowUnregister)) { + const startupRequestData = getFormData(detectorSettings.startupEndpoint); + + request.post(startupRequestData, onFaceListResult); + } }; const processImage = (frameBuffer, d, tx, frameLocation, callback) => { diff --git a/web/assets/js/super.faceManager.js b/web/assets/js/super.faceManager.js index b0aadb42..a06484f9 100644 --- a/web/assets/js/super.faceManager.js +++ b/web/assets/js/super.faceManager.js @@ -109,7 +109,6 @@ $(document).ready(() => { faceNameField.val(""); fileinput.val(""); - } catch (error) { console.error(`Failed to handle form submit event, Error: ${error}`); } @@ -129,7 +128,6 @@ $(document).ready(() => { const getFaceDelete = (name) => { const navigationItem = ``; return navigationItem; @@ -336,30 +334,6 @@ $(document).ready(() => { return false; }; - const onUploadImageRequest = (e) => { - try { - e.preventDefault(); - const faceName = getEventItemAttribute(e, "face"); - - faceNameField.val(faceName); - fileinput.trigger('click'); - - const image = fileinput.val(); - - if(image !== undefined && image !== null && image !== "") { - setTimeout(() => onFormSubmitted(), 2000); - } else { - faceNameField.val(""); - fileinput.val(""); - } - - } catch (error) { - console.error(`Failed to handle delete image request event, Error: ${error}`); - } - - return false; - }; - const onDeleteFaceRequest = (e) => { try { e.preventDefault(); @@ -475,10 +449,6 @@ $(document).ready(() => { onDeleteImageRequest(e); }); - faceImageList.on("click", ".uploadImage", e => { - onUploadImageRequest(e); - }); - faceNameField.change(() => { onFormDataChanged(); }); From aa2a43e79a26eae3fb097924c7bffd44467a16f0 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Sun, 11 Dec 2022 08:52:51 +0200 Subject: [PATCH 10/11] prevent same event source (groupId + monitorId) to run in parallel in the same plugin, aligned DeepStack object detection plugin --- .../deepstack-face/shinobi-deepstack-face.js | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/plugins/deepstack-face/shinobi-deepstack-face.js b/plugins/deepstack-face/shinobi-deepstack-face.js index a41ab417..fb1608c1 100644 --- a/plugins/deepstack-face/shinobi-deepstack-face.js +++ b/plugins/deepstack-face/shinobi-deepstack-face.js @@ -137,6 +137,29 @@ const initialize = () => { request.post(startupRequestData, handleStartupResponse); }; +const getJobKey = (groupId, monitorId) => { + const jobKey = `${groupId}_${monitorId}`; + + return jobKey; +} + +const addJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + const jobExists = detectorSettings.jobs.includes(jobKey); + + if(!jobExists) { + detectorSettings.jobs.push(jobKey); + } + + return !jobExists; +} + +const removeJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + + detectorSettings.jobs = detectorSettings.jobs.filter(j => j !== jobKey); +} + const handleStartupResponse = (err, res, body) => { try { if(err) { @@ -306,8 +329,12 @@ const processImage = (frameBuffer, d, tx, frameLocation, callback) => { onImageProcessed(d, tx, err, res, body, frameBuffer); fs.unlinkSync(frameLocation); + + removeJob(d.ke, d.id); }); } catch(ex) { + removeJob(d.ke, d.id); + logError(`Failed to process image, Error: ${ex}`); if(fs.existsSync(frameLocation)) { @@ -323,6 +350,12 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { return; } + const jobCreated = addJob(d.ke, d.id); + + if(!jobCreated) { + return; + } + const dirCreationOptions = { recursive: true }; @@ -337,6 +370,8 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { fs.writeFile(path, frameBuffer, function(err) { if(err) { + removeJob(d.ke, d.id); + return s.systemLog(err); } @@ -344,6 +379,8 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { processImage(frameBuffer, d, tx, path, callback); } catch(ex) { + removeJob(d.ke, d.id); + logError(`Detector failed to parse frame, Error: ${ex}`); } }); From 19db81485c43e838133d1948ba7de13f97701d61 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Sun, 11 Dec 2022 08:53:04 +0200 Subject: [PATCH 11/11] // // Shinobi - DeepStack Face Recognition Plugin // Copyright (C) 2021 Elad Bar // // Base Init >> const { spawn } = require('child_process'); const fs = require('fs'); const request = require("request"); const moment = require('moment'); const config = require('./conf.json'); let s = null; const { workerData } = require('worker_threads'); const isWorker = workerData && workerData.ok === true; const pluginBasePath = isWorker ? "pluginWorkerBase.js" : "pluginBase.js"; for(let i = 0; i < 2; i++) { try { s = require(`../${pluginBasePath}`)(__dirname, config); } catch(err) { console.log(err); s = null; } } if(s === null) { console.log(config.plug, `Plugin start has failed. ${pluginBasePath} was not found.`); } else { if(!isWorker) { const { haltMessage, checkStartTime, setStartTime, } = require('../pluginCheck.js'); if(!checkStartTime()) { console.log(haltMessage, new Date()); s.disconnectWebSocket(); } setStartTime(); } } // Base Init />> let detectorSettings = null; const DETECTOR_TYPE_FACE = 'face'; const DETECTOR_TYPE_OBJECT = 'object'; const FACE_UNKNOWN = 'unknown'; const DEEPSTACK_API_KEY = 'api_key'; const DETECTOR_CONFIGUTATION = { face: { detectEndpoint: '/vision/face/recognize', startupEndpoint: '/vision/face/list', key: 'userid' }, object: { detectEndpoint: '/vision/detection', key: 'label' } } const PROTOCOLS = { true: "https", false: "http" }; const log = (logger, message) => { logger(`${moment().format()} [${config.plug}] ${message}`); } const logError = (message) => { log(console.error, message); }; const logWarn = (message) => { log(console.warn, message); }; const logInfo = (message) => { log(console.info, message); }; const logDebug = (message) => { log(console.debug, message); }; const postMessage = (data) => { const message = JSON.stringify(data); logInfo(message); }; const initialize = () => { const deepStackProtocol = PROTOCOLS[config.deepStack.isSSL]; baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; const detectionType = config.plug.split("-")[1].toLowerCase(); const detectorConfig = DETECTOR_CONFIGUTATION[detectionType]; const detectorConfigKeys = Object.keys(detectorConfig); detectorSettings = { type: detectionType, active: false, baseUrl: baseUrl, apiKey: config.deepStack.apiKey, jobs: [] }; if(detectionType === DETECTOR_TYPE_FACE) { detectorSettings["registeredPersons"] = config.persons === undefined ? [] : config.persons; } detectorConfigKeys.forEach(k => detectorSettings[k] = detectorConfig[k]); const testRequestData = getFormData(detectorSettings.detectEndpoint); request.post(testRequestData, (err, res, body) => { try { if(err) { throw err; } const response = JSON.parse(body); if(response.error) { detectorSettings.active = !response.error.endsWith('endpoint not activated'); } else { detectorSettings.active = response.success; } const detectorSettingsKeys = Object.keys(detectorSettings); const pluginMessageHeader = []; pluginMessageHeader.push(`${config.plug} loaded`); const configMessage = detectorSettingsKeys.map(k => `${k}: ${detectorSettings[k]}`); const fullPluginMessage = pluginMessageHeader.concat(configMessage); const pluginMessage = fullPluginMessage.join(", "); logInfo(pluginMessage); if (detectorSettings.active) { s.detectObject = detectObject; if(detectionType === DETECTOR_TYPE_FACE) { const requestData = getFormData(detectorSettings.startupEndpoint); const requestTime = getCurrentTimestamp(); request.post(requestData, (errStartup, resStartup, bodyStartup) => { if (!!resStartup) { resStartup.duration = getDuration(requestTime); } onFaceListResult(errStartup, resStartup, bodyStartup); }); } } } catch(ex) { logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`) } }); }; const processImage = (imageB64, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; } try{ const imageStream = fs.createReadStream(frameLocation); const form = { image: imageStream, min_confidence: 0.7 }; const requestData = getFormData(detectorSettings.detectEndpoint, form); const requestTime = getCurrentTimestamp(); request.post(requestData, (err, res, body) => { if (!!res) { res.duration = getDuration(requestTime); } onImageProcessed(d, tx, err, res, body, imageB64); fs.unlinkSync(frameLocation); removeJob(d.ke, d.id); }); }catch(ex){ removeJob(d.ke, d.id); logError(`Failed to process image, Error: ${ex}`); if(fs.existsSync(frameLocation)) { fs.unlinkSync(frameLocation); } } callback(); }; const getJobKey = (groupId, monitorId) => { const jobKey = `${groupId}_${monitorId}`; return jobKey; } const addJob = (groupId, monitorId) => { const jobKey = getJobKey(groupId, monitorId); const jobExists = detectorSettings.jobs.includes(jobKey); if(!jobExists) { detectorSettings.jobs.push(jobKey); } return !jobExists; } const removeJob = (groupId, monitorId) => { const jobKey = getJobKey(groupId, monitorId); detectorSettings.jobs = detectorSettings.jobs.filter(j => j !== jobKey); } const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; } const jobCreated = addJob(d.ke, d.id); if(!jobCreated) { return; } const dirCreationOptions = { recursive: true }; d.dir = `${s.dir.streams}${d.ke}/${d.id}/`; frameLocation = `${d.dir}${s.gid(5)}.jpg`; if(!fs.existsSync(d.dir)) { fs.mkdirSync(d.dir, dirCreationOptions); } fs.writeFile(frameLocation, frameBuffer, function(err) { if(err) { removeJob(d.ke, d.id); return s.systemLog(err); } try { const imageB64 = frameBuffer.toString('base64'); processImage(imageB64, d, tx, frameLocation, callback); } catch(ex) { removeJob(d.ke, d.id); logError(`Detector failed to parse frame, Error: ${ex}`); } }); }; const getCurrentTimestamp = () => { const currentTime = new Date(); const currentTimestamp = currentTime.getTime(); return currentTimestamp }; const getDuration = (requestTime) => { const currentTime = new Date(); const currentTimestamp = currentTime.getTime(); const duration = currentTimestamp - requestTime; return duration; }; const onFaceListResult = (err, res, body) => { const duration = !!res ? res.duration : 0; try { const response = JSON.parse(body); const success = response.success; const facesArr = response.faces; const faceStr = facesArr.join(","); if(success) { logInfo(`DeepStack loaded with the following faces: ${faceStr}, Response time: ${duration} ms`); } else { logWarn(`Failed to connect to DeepStack server, Error: ${err}, Response time: ${duration} ms`); } } catch(ex) { logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}, Response time: ${duration} ms`); } }; const onImageProcessed = (d, tx, err, res, body, imageStream) => { const duration = !!res ? res.duration : 0; let objects = []; try { if(err) { throw err; } const response = JSON.parse(body); const success = response.success; if(success) { const predictions = response.predictions; if(predictions !== null && predictions.length > 0) { objects = predictions.map(p => getDeepStackObject(p)).filter(p => !!p); if(objects.length === 0) { logInfo(`Processed image for ${detectorSettings.type} on monitor ${d.id} returned no results, Response time: ${duration} ms`); } else { const identified = objects.filter(p => p.tag !== FACE_UNKNOWN); const unknownCount = objects.length - identified.length; if(unknownCount > 0) { logInfo(`{d.id}$ detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); } if(identified.length > 0) { const detectedObjectsStrArr = []; if (detectorSettings.type === DETECTOR_TYPE_FACE) { identified.forEach(f => detectedObjectsStrArr.push(`${f.tag} (${f.person}) [${(f.confidence * 100).toFixed(2)}]`)); } else { identified.forEach(f => detectedObjectsStrArr.push(`${f.tag} [${(f.confidence * 100).toFixed(2)}]`)); } const detectedObjectsStr = detectedObjectsStrArr.join(", "); logInfo(`${d.id} detected ${detectorSettings.type}s: ${detectedObjectsStr}, Response time: ${duration} ms`); } const isObjectDetectionSeparate = d.mon.detector_pam === '1' && d.mon.detector_use_detect_object === '1'; const width = parseFloat(isObjectDetectionSeparate && d.mon.detector_scale_y_object ? d.mon.detector_scale_y_object : d.mon.detector_scale_y); const height = parseFloat(isObjectDetectionSeparate && d.mon.detector_scale_x_object ? d.mon.detector_scale_x_object : d.mon.detector_scale_x); const eventData = { f: 'trigger', id: d.id, ke: d.ke, details: { plug: config.plug, name: d.id, reason: detectorSettings.type, matrices: objects, imgHeight: width, imgWidth: height, time: duration, imageStream: imageStream } }; tx(eventData); } } } else { logWarn(`Processed image for ${detectorSettings.type} on monitor ${d.id} failed, Reason: ${response.error}, Response time: ${duration} ms`); } } catch(ex) { logError(`Error while processing image, Error: ${ex} | ${err}, Response time: ${duration} ms, Body: ${body}`); } return objects }; const getFormData = (endpoint, additionalParameters) => { const formData = {}; if(detectorSettings.apiKey) { formData[DEEPSTACK_API_KEY] = detectorSettings.apiKey; } if(additionalParameters !== undefined && additionalParameters !== null) { const keys = Object.keys(additionalParameters); keys.forEach(k => formData[k] = additionalParameters[k]); } const requestData = { url: `${detectorSettings.baseUrl}${endpoint}`, time: true, formData: formData }; return requestData; }; const getDeepStackObject = (prediction) => { if(prediction === undefined) { return null; } const tag = prediction[detectorSettings.key]; const confidence = prediction.confidence ?? 0; const y_min = prediction.y_min ?? 0; const x_min = prediction.x_min ?? 0; const y_max = prediction.y_max ?? 0; const x_max = prediction.x_max ?? 0; const width = x_max - x_min; const height = y_max - y_min; const obj = { x: x_min, y: y_min, width: width, height: height, tag: tag, confidence: confidence }; if (detectorSettings.type === DETECTOR_TYPE_FACE) { const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) const person = matchingPersons.length > 0 ? matchingPersons[0] : null; obj["person"] = person; } return obj; }; initialize(); --- .../shinobi-deepstack-object.js | 314 +++++++----------- 1 file changed, 115 insertions(+), 199 deletions(-) diff --git a/plugins/deepstack-object/shinobi-deepstack-object.js b/plugins/deepstack-object/shinobi-deepstack-object.js index 33fa9dc3..f79a6318 100644 --- a/plugins/deepstack-object/shinobi-deepstack-object.js +++ b/plugins/deepstack-object/shinobi-deepstack-object.js @@ -1,5 +1,5 @@ // -// Shinobi - DeepStack Object Detection Plugin +// Shinobi - DeepStack Face Recognition Plugin // Copyright (C) 2021 Elad Bar // // Base Init >> @@ -51,8 +51,6 @@ if(s === null) { let detectorSettings = null; -const DELIMITER = "___"; - const DETECTOR_TYPE_FACE = 'face'; const DETECTOR_TYPE_OBJECT = 'object'; @@ -63,13 +61,10 @@ const DETECTOR_CONFIGUTATION = { face: { detectEndpoint: '/vision/face/recognize', startupEndpoint: '/vision/face/list', - registerEndpoint: '/vision/face/register', - deleteEndpoint: '/vision/face/delete', key: 'userid' }, object: { detectEndpoint: '/vision/detection', - startupEndpoint: '/vision/detection', key: 'label' } } @@ -108,7 +103,7 @@ const postMessage = (data) => { const initialize = () => { const deepStackProtocol = PROTOCOLS[config.deepStack.isSSL]; - const baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; + baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; const detectionType = config.plug.split("-")[1].toLowerCase(); const detectorConfig = DETECTOR_CONFIGUTATION[detectionType]; @@ -119,161 +114,67 @@ const initialize = () => { active: false, baseUrl: baseUrl, apiKey: config.deepStack.apiKey, - faces: { - shinobi: null, - server: null, - legacy: null - }, - eventMapping: { - "recompileFaceDescriptors": onRecompileFaceDescriptors - } + jobs: [] }; - + + if(detectionType === DETECTOR_TYPE_FACE) { + detectorSettings["registeredPersons"] = config.persons === undefined ? [] : config.persons; + } + detectorConfigKeys.forEach(k => detectorSettings[k] = detectorConfig[k]); - const startupRequestData = getFormData(detectorSettings.startupEndpoint); + const testRequestData = getFormData(detectorSettings.detectEndpoint); - request.post(startupRequestData, handleStartupResponse); -}; - -const handleStartupResponse = (err, res, body) => { - try { - if(err) { - logError(`Failed to initialize ${config.plug} plugin, Error: ${err}`); - - } else { - const response = JSON.parse(body); + request.post(testRequestData, (err, res, body) => { + try { + if(err) { + throw err; + } + const response = JSON.parse(body); + if(response.error) { detectorSettings.active = !response.error.endsWith('endpoint not activated'); } else { detectorSettings.active = response.success; } - logInfo(`${config.plug} loaded, Configuration: ${JSON.stringify(detectorSettings)}`); + const detectorSettingsKeys = Object.keys(detectorSettings); + + const pluginMessageHeader = []; + pluginMessageHeader.push(`${config.plug} loaded`); + + const configMessage = detectorSettingsKeys.map(k => `${k}: ${detectorSettings[k]}`); + + const fullPluginMessage = pluginMessageHeader.concat(configMessage); + + const pluginMessage = fullPluginMessage.join(", "); + + logInfo(pluginMessage); if (detectorSettings.active) { s.detectObject = detectObject; - if(detectorSettings.type === DETECTOR_TYPE_FACE) { - onFaceListResult(err, res, body); + if(detectionType === DETECTOR_TYPE_FACE) { + const requestData = getFormData(detectorSettings.startupEndpoint); + const requestTime = getCurrentTimestamp(); + + request.post(requestData, (errStartup, resStartup, bodyStartup) => { + if (!!resStartup) { + resStartup.duration = getDuration(requestTime); + } + + onFaceListResult(errStartup, resStartup, bodyStartup); + }); } - } + } + } catch(ex) { + logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`) } - - - } catch(ex) { - logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`); - } + }); }; -const registerFace = (serverFileName) => { - const shinobiFileParts = serverFileName.split(DELIMITER); - const faceName = shinobiFileParts[0]; - const image = shinobiFileParts[1]; - - const frameLocation = `${config.facesFolder}${faceName}/${image}`; - - const imageStream = fs.createReadStream(frameLocation); - - const form = { - image: imageStream, - userId: serverFileName - }; - - const requestData = getFormData(detectorSettings.registerEndpoint, form); - - request.post(requestData, (err, res, body) => { - if (err) { - logError(`Failed to register face, Face: ${faceName}, Image: ${image}`); - } else { - logInfo(`Register face, Face: ${faceName}, Image: ${image}`); - } - }); -}; - -const unregisterFace = (serverFileName) => { - const form = { - userId: serverFileName - }; - - const requestData = getFormData(detectorSettings.deleteEndpoint, form); - - request.post(requestData, (err, res, body) => { - if (err) { - logError(`Failed to delete face, UserID: ${serverFileName}`); - } else { - logInfo(`Deleted face, UserID: ${serverFileName}`); - } - }); -}; - -const getServerFileNameByShinobi = (name, image) => { - const fileName = `${name}${DELIMITER}${image}`; - - return fileName; -} - -const compareShinobiVSServer = () => { - const allFaces = detectorSettings.faces; - const shinobiFaces = allFaces.shinobi; - const serverFaces = allFaces.server; - const compareShinobiVSServerDelayID = detectorSettings.compareShinobiVSServerDelayID || null; - - if (compareShinobiVSServerDelayID !== null) { - clearTimeout(compareShinobiVSServerDelayID) - } - - if(serverFaces === null || shinobiFaces === null) { - detectorSettings.compareShinobiVSServerDelayID = setTimeout(compareShinobiVSServer, 5000); - logWarn("AI Server not ready yet, will retry in 5 seconds"); - - return; - } - - const shinobiFaceKeys = Object.keys(shinobiFaces); - - const shinobiFiles = shinobiFaceKeys.length === 0 ? - [] : - shinobiFaceKeys - .map(faceName => { - const value = shinobiFaces[faceName].map(image => getServerFileNameByShinobi(faceName, image)); - - return value; - }) - .reduce((acc, item) => { - console.log("acc: ", acc); - console.log("item: ", item); - - const result = [...acc, ...item]; - - return result; - }); - - const facesToRegister = shinobiFiles.filter(f => !serverFaces.includes(f)); - const facesToUnregister = serverFaces.filter(f => !shinobiFiles.includes(f)); - - if(facesToRegister.length > 0) { - logInfo(`Registering the following faces: ${facesToRegister}`); - facesToRegister.forEach(f => registerFace(f)); - } - - if(facesToUnregister.length > 0) { - const allowUnregister = detectorSettings.fullyControlledByFaceManager || false; - - if(allowUnregister) { - logInfo(`Unregister the following faces: ${facesToUnregister}`); - - facesToUnregister.forEach(f => unregisterFace(f)); - } else { - logInfo(`Skip unregistering the following faces: ${facesToUnregister}`); - - detectorSettings.faces.legacy = facesToUnregister; - } - } -}; - -const processImage = (frameBuffer, d, tx, frameLocation, callback) => { +const processImage = (imageB64, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; } @@ -295,11 +196,15 @@ const processImage = (frameBuffer, d, tx, frameLocation, callback) => { res.duration = getDuration(requestTime); } - onImageProcessed(d, tx, err, res, body, frameBuffer); + onImageProcessed(d, tx, err, res, body, imageB64); fs.unlinkSync(frameLocation); + + removeJob(d.ke, d.id); }); - } catch(ex) { + }catch(ex){ + removeJob(d.ke, d.id); + logError(`Failed to process image, Error: ${ex}`); if(fs.existsSync(frameLocation)) { @@ -309,10 +214,38 @@ const processImage = (frameBuffer, d, tx, frameLocation, callback) => { callback(); }; +const getJobKey = (groupId, monitorId) => { + const jobKey = `${groupId}_${monitorId}`; + + return jobKey; +} + +const addJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + const jobExists = detectorSettings.jobs.includes(jobKey); + + if(!jobExists) { + detectorSettings.jobs.push(jobKey); + } + + return !jobExists; +} + +const removeJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + + detectorSettings.jobs = detectorSettings.jobs.filter(j => j !== jobKey); +} const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; + } + + const jobCreated = addJob(d.ke, d.id); + + if(!jobCreated) { + return; } const dirCreationOptions = { @@ -321,21 +254,27 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { d.dir = `${s.dir.streams}${d.ke}/${d.id}/`; - const filePath = `${d.dir}${s.gid(5)}.jpg`; + frameLocation = `${d.dir}${s.gid(5)}.jpg`; if(!fs.existsSync(d.dir)) { fs.mkdirSync(d.dir, dirCreationOptions); } - fs.writeFile(filePath, frameBuffer, function(err) { + fs.writeFile(frameLocation, frameBuffer, function(err) { if(err) { + removeJob(d.ke, d.id); + return s.systemLog(err); } try { - processImage(frameBuffer, d, tx, filePath, callback); + const imageB64 = frameBuffer.toString('base64'); + + processImage(imageB64, d, tx, frameLocation, callback); } catch(ex) { + removeJob(d.ke, d.id); + logError(`Detector failed to parse frame, Error: ${ex}`); } }); @@ -358,6 +297,8 @@ const getDuration = (requestTime) => { }; const onFaceListResult = (err, res, body) => { + const duration = !!res ? res.duration : 0; + try { const response = JSON.parse(body); @@ -365,22 +306,20 @@ const onFaceListResult = (err, res, body) => { const facesArr = response.faces; const faceStr = facesArr.join(","); - detectorSettings.faces.server = facesArr; - if(success) { - logInfo(`DeepStack loaded with the following faces: ${faceStr}`); + logInfo(`DeepStack loaded with the following faces: ${faceStr}, Response time: ${duration} ms`); } else { - logWarn(`Failed to connect to DeepStack server, Error: ${err}`); + logWarn(`Failed to connect to DeepStack server, Error: ${err}, Response time: ${duration} ms`); } } catch(ex) { - logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}`); + logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}, Response time: ${duration} ms`); } }; -const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { +const onImageProcessed = (d, tx, err, res, body, imageStream) => { const duration = !!res ? res.duration : 0; - const result = []; + let objects = []; try { if(err) { @@ -395,16 +334,16 @@ const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { const predictions = response.predictions; if(predictions !== null && predictions.length > 0) { - const predictionDescriptons = predictions.map(p => getPredictionDescripton(p)).filter(p => !!p); + objects = predictions.map(p => getDeepStackObject(p)).filter(p => !!p); - result.push(...predictionDescriptons); - - if(predictionDescriptons.length > 0) { - const identified = predictionDescriptons.filter(p => p.tag !== FACE_UNKNOWN); - const unknownCount = predictionDescriptons.length - identified.length; + if(objects.length === 0) { + logInfo(`Processed image for ${detectorSettings.type} on monitor ${d.id} returned no results, Response time: ${duration} ms`); + } else { + const identified = objects.filter(p => p.tag !== FACE_UNKNOWN); + const unknownCount = objects.length - identified.length; if(unknownCount > 0) { - logInfo(`${d.id} detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); + logInfo(`{d.id}$ detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); } if(identified.length > 0) { @@ -436,20 +375,22 @@ const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { matrices: objects, imgHeight: width, imgWidth: height, - time: duration - }, - frame: frameBuffer + time: duration, + imageStream: imageStream + } }; tx(eventData); } } - } + } else { + logWarn(`Processed image for ${detectorSettings.type} on monitor ${d.id} failed, Reason: ${response.error}, Response time: ${duration} ms`); + } } catch(ex) { - logError(`Error while processing image, Error: ${ex} | ${err},, Response time: ${duration} ms, Body: ${body}`); + logError(`Error while processing image, Error: ${ex} | ${err}, Response time: ${duration} ms, Body: ${body}`); } - return result + return objects }; const getFormData = (endpoint, additionalParameters) => { @@ -474,7 +415,7 @@ const getFormData = (endpoint, additionalParameters) => { return requestData; }; -const getPredictionDescripton = (prediction) => { +const getDeepStackObject = (prediction) => { if(prediction === undefined) { return null; } @@ -499,39 +440,14 @@ const getPredictionDescripton = (prediction) => { }; if (detectorSettings.type === DETECTOR_TYPE_FACE) { - const legacyFaces = detectorSettings.faces.legacy || []; + const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) + const person = matchingPersons.length > 0 ? matchingPersons[0] : null; - if (legacyFaces.includes(tag)) { - const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) - obj.person = matchingPersons.length > 0 ? matchingPersons[0] : null; - - } else { - const shinobiFileParts = tag.split(DELIMITER); - obj.person = shinobiFileParts[0]; - } - } + obj["person"] = person; + } + return obj; }; -const onRecompileFaceDescriptors = (d) => { - if(detectorSettings.faces.shinobi !== d.faces) { - detectorSettings.faces.shinobi = d.faces; - - compareShinobiVSServer(); - } -}; - -s.MainEventController = (d,cn,tx) => { - const handler = detectorSettings.eventMapping[d.f]; - - if (handler !== undefined) { - try { - handler(d); - } catch (error) { - logError(`Failed to handle event ${d.f}, Error: ${error}`); - } - } -} - initialize();