ketr.photos/server/routes/identities.js
James P. Ketrenos 83e006b43c ID can now be set per face
Signed-off-by: James P. Ketrenos <james.p.ketrenos@intel.com>
2023-01-22 18:12:54 -08:00

457 lines
12 KiB
JavaScript
Executable File

"use strict";
const express = require("express"),
Promise = require("bluebird");
let photoDB;
require("../db/photos").then(function(db) {
photoDB = db;
});
const router = express.Router();
const addOrUpdateIdentity = async(id, {
displayName,
firstName,
lastName,
middleName
}, res) => {
if (displayName === undefined
|| firstName === undefined
|| lastName === undefined
|| middleName === undefined) {
res.status(400).send({ message: `Missing fields` });
return undefined;
}
const identity = {
displayName,
firstName,
lastName,
middleName,
id
};
if (id === -1 || !id) {
const [results, { lastId }] = await photoDB.sequelize.query(
`INSERT INTO identities ` +
'(displayName,firstName,lastName,middleName)' +
'VALUES(:displayName,:firstName,:lastName,:middleName)', {
replacements: identity
});
identity.id = lastId;
} else {
await photoDB.sequelize.query(
`UPDATE identities ` +
'SET ' +
'displayName=:displayName, ' +
'firstName=:firstName, ' +
'lastName=:lastName, ' +
'middleName=:middleName ' +
'WHERE id=:id', {
replacements: identity
});
}
return identity;
};
const populateRelatedFaces = async (identity, count) => {
let limit = '';
if (count) {
limit = ` LIMIT ${count} `;
}
/* If this is a new identity, no faces are being requested --
* just return the empty 'unknown face'.
*
* Otherwise, query the DB for 'count' faces */
if (count === undefined) {
identity.relatedFaces = await photoDB.sequelize.query(
"SELECT id AS faceId,photoId,faceConfidence " +
"FROM faces " +
"WHERE identityId=:identityId " +
limit, {
replacements: { identityId: identity.id },
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
} else {
identity.relatedFaces = [];
}
/* If there are no faces, return the 'unknown face' */
if (identity.relatedFaces.length === 0) {
identity.relatedFaces.push({
faceId: -1,
photoId: -1,
faceConfidence: 0
});
}
identity.relatedFaces.forEach(face => {
face.identityId = identity.id;
face.distance = face.faceConfidence;
face.descriptors = [];
delete face.faceConfidence;
});
}
router.post('/', async (req, res) => {
console.log(`POST ${req.url}`)
if (!req.user.maintainer) {
console.warn(`${req.user.name} attempted to modify photos.`);
return res.status(401).send({ message: "Unauthorized to modify photos." });
}
const identity = await addOrUpdateIdentity(-1, req.body, res);
if (!identity) {
return;
}
populateRelatedFaces(identity, 1);
return res.status(200).send(identity);
});
router.put('/:id', async (req, res) => {
console.log(`PUT ${req.url}`)
if (!req.user.maintainer) {
console.warn(`${req.user.name} attempted to modify photos.`);
return res.status(401).send({ message: "Unauthorized to modify photos." });
}
const { id } = req.params;
if (!id || isNaN(+id)) {
return res.status(400).send({ message: `Invalid identity id ${id}` });
}
const identity = await addOrUpdateIdentity(id, req.body, res);
if (!identity) {
return;
}
populateRelatedFaces(identity);
return res.status(200).send(identity);
});
const addFaceToIdentityDescriptors = (identity, face) => {
};
const removeFaceToIdentityDescriptors = (identity, face) => {
};
const writeIdentityDescriptors = async (identity) => {
await photoDB.sequelize.query(
'UPDATE identities ' +
'SET descriptors=:descriptors' +
'WHERE id=:identityId', {
replacements: identity
}
);
};
router.put("/faces/remove/:id", async (req, res) => {
console.log(`PUT ${req.url}`)
if (!req.user.maintainer) {
console.warn(`${req.user.name} attempted to modify photos.`);
return res.status(401).send({ message: "Unauthorized to modify photos." });
}
const id = parseInt(req.params.id);
if (id != req.params.id) {
return res.status(400).send({ message: "Invalid identity id." });
}
if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) {
return res.status(400).send({ message: "No faces supplied." });
}
try {
await photoDB.sequelize.query(
"UPDATE faces SET identityId=null " +
"WHERE id IN (:faceIds)", {
replacements: {
identityId: id,
faceIds: req.body.faces
}
});
const identity = {
id: id,
faces: req.body.faces
};
identity.faces = identity.faces.map(id => +id);
updateIdentityDescriptors(identity);
return res.status(200).json(identity);
} catch (error) {
console.error(error);
return res.status(500).send({message: "Error processing request." });
};
});
router.put("/faces/add/:id", async (req, res) => {
if (!req.user.maintainer) {
console.warn(`${req.user.name} attempted to modify photos.`);
return res.status(401).send("Unauthorized to modify photos.");
}
const id = parseInt(req.params.id);
if (id != req.params.id) {
return res.status(400).send("Invalid identity id.");
}
if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) {
return res.status(400).send("No faces supplied.");
}
try {
await photoDB.sequelize.query(
"UPDATE faces SET identityId=:identityId,classifiedBy='human' " +
"WHERE id IN (:faceIds)", {
replacements: {
identityId: id,
faceIds: req.body.faces
}
});
const identity = {
id: id,
faces: req.body.faces
};
identity.faces = identity.faces.map(id => +id);
updateIdentityDescriptors(identity);
return res.status(200).json(identity);
} catch (error) {
console.error(error);
return res.status(500).send("Error processing request.");
};
});
router.post("/", (req, res) => {
if (!req.user.maintainer) {
console.warn(`${req.user.name} attempted to modify photos.`);
return res.status(401).send("Unauthorized to modify photos.");
}
const identity = {
lastName: req.body.lastName || "",
firstName: req.body.firstName || "",
middleName: req.body.middleName || ""
};
identity.name = req.body.name || (identity.firstName + " " + identity.lastName);
let fields = [];
for (let key in identity) {
fields.push(key);
}
if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) {
return res.status(400).send("No faces supplied.");
}
return photoDB.sequelize.query("INSERT INTO identities " +
"(" + fields.join(",") + ") " +
"VALUES(:" + fields.join(",:") + ")", {
replacements: identity,
}).then(([ results, metadata ]) => {
identity.id = metadata.lastID;
return photoDB.sequelize.query(
"UPDATE faces SET identityId=:identityId " +
"WHERE id IN (:faceIds)", {
replacements: {
identityId: identity.id,
faceIds: req.body.faces
}
}).then(() => {
identity.faces = req.body.faces;
return res.status(200).json([identity]);
});
}).catch((error) => {
console.error(error);
return res.status(500).send("Error processing request.");
});
});
function bufferToFloat32Array(buffer) {
return new Float64Array(buffer.buffer,
buffer.byteOffset,
buffer.byteLength / Float64Array.BYTES_PER_ELEMENT);
}
function euclideanDistance(a, b) {
if (!a.buffer || !b.buffer) {
return -1;
}
let A = bufferToFloat32Array(a);
let B = bufferToFloat32Array(b);
let sum = 0;
for (let i = 0; i < A.length; i++) {
let delta = A[i] - B[i];
sum += delta * delta;
}
return Math.sqrt(sum);
}
const getUnknownIdentity = async (faceCount) => {
const unknownIdentity = {
identityId: -1,
lastName: '',
firstName: '',
middleName: '',
displayName: 'Unknown',
descriptors: new Float32Array(0),
relatedFaces: []
};
const limit = faceCount
? ` LIMIT ${faceCount} `
: ' ORDER BY faceConfidence DESC ';
unknownIdentity.relatedFaces = await photoDB.sequelize.query(
"SELECT id AS faceId,photoId,faceConfidence " +
"FROM faces WHERE identityId IS NULL AND classifiedBy != 'not-a-face' " +
limit, {
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
if (unknownIdentity.relatedFaces.length === 0) {
unknownIdentity.relatedFaces.push({
faceId: -1,
photoId: -1,
faceConfidence: 0
});
}
unknownIdentity.relatedFaces.forEach(face => {
face.identityId = -1;
face.distance = face.faceConfidence;
face.descriptors = new Float32Array(0);
delete face.faceConfidence;
});
return unknownIdentity;
}
router.get("/:id?", async (req, res) => {
console.log(`GET ${req.url}`);
let id;
if (req.params.id) {
id = parseInt(req.params.id);
if (id != req.params.id) {
return res.status(400).send({ message: "Usage /[id]"});
}
}
/* If identityId requested is -1, this is the "Unknown" identity
* where all unmapped faces live. */
if (id === -1) {
const unknownIdentity = await getUnknownIdentity()
return res.status(200).json([ unknownIdentity ]);
}
const filter = id ? "WHERE identities.id=:id " : "";
const identities = await photoDB.sequelize.query("SELECT " +
"identities.*," +
"GROUP_CONCAT(faces.id) AS relatedFaceIds," +
"GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " +
"FROM identities " +
"LEFT JOIN faces ON identities.id=faces.identityId " +
filter +
"GROUP BY identities.id", {
replacements: { id },
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
await Promise.map(identities, async (identity) => {
[ 'firstName', 'middleName', 'lastName' ].forEach(key => {
if (!identity[key]) {
identity[key] = '';
}
});
identity.identityId = identity.id;
if (!identity.relatedFaceIds) {
identity.relatedFaces = [];
} else {
const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(",");
let descriptors = await photoDB.sequelize.query(
`SELECT descriptors FROM facedescriptors WHERE id in (:ids)`, {
replacements: {
ids: identity.relatedFaceDescriptorIds.split(',')
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}
);
descriptors = descriptors.map(entry => entry.descriptors);
identity.relatedFaces = relatedFaces.map((faceId, index) => {
let distance = 0;
if (descriptors[index] && identity.descriptors) {
distance = euclideanDistance(
descriptors[index],
identity.descriptors
);
} else {
distance = -1;
}
return {
identityId: identity.id,
faceId,
photoId: relatedFacePhotos[index],
distance
};
});
}
if (identity.relatedFaces.length === 0) {
identity.relatedFaces.push({
faceId: -1,
photoId: -1,
identityId: identity.id,
distance: 0,
faceConfidence: 0
});
}
identity
.relatedFaces
.sort((A, B) => {
return A.distance - B.distance;
});
/* If no filter was specified, only return the best face for
* the identity */
if (!filter) {
identity.relatedFaces = [ identity.relatedFaces[0] ];
}
delete identity.id;
delete identity.descriptors;
delete identity.relatedFaceIds;
delete identity.relatedFacePhotoIds;
delete identity.relatedFaceDescriptorIds;
delete identity.relatedIdentityDescriptors;
});
/* If no ID was provided (so no 'filter') then this call is returning
* a list of all identities -- we create a fake identity for all
* unlabeled faces */
if (!filter) {
const unknownIdentity = await getUnknownIdentity(1)
identities.push(unknownIdentity);
}
return res.status(200).json(identities);
});
module.exports = router;