ketr.photos/server/routes/identities.js
James Ketrenos d019af070d Updating slideshow and identity editor
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
2023-01-13 15:25:34 -08:00

206 lines
6.2 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();
router.put("/faces/add/:id", (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.");
}
return photoDB.sequelize.query("UPDATE faces SET identityId=:identityId,identityDistance=0 " +
"WHERE id IN (:faceIds)", {
replacements: {
identityId: id,
faceIds: req.body.faces
}
}).then(() => {
const identity = {
id: id,
faces: req.body.faces
};
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,identityDistance=0 " +
"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 Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Float32Array.BYTES_PER_ELEMENT);
}
function euclideanDistance(a, b) {
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);
}
router.get("/:id?", (req, res) => {
let id;
if (req.params.id) {
id = parseInt(req.params.id);
if (id != req.params.id) {
return res.status(400).send({ message: "Usage /[id]"});
}
}
const filter = id ? "WHERE identities.id=:id " : "";
return photoDB.sequelize.query("SELECT " +
"identities.*," +
"GROUP_CONCAT(faces.id) AS relatedFaceIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds," +
"GROUP_CONCAT(facedescriptors.descriptors) AS relatedIdentityDescriptors " +
"FROM identities " +
"INNER JOIN facedescriptors ON facedescriptors.id=faces.descriptorId " +
"INNER JOIN faces ON identities.id=faces.identityId " +
filter +
"GROUP BY identities.id", {
replacements: { id },
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((identities) => {
identities.forEach((identity) => {
const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(","),
relatedIdentityDistances = identity.relatedIdentityDistances.split(",");
if (relatedFaces.length != relatedFacePhotos.length) {
console.warn("Face ID to Photo ID mapping doesn't match!");
}
delete identity.relatedFaceIds;
delete identity.relatedFacePhotoIds;
identity.relatedFaces = relatedFaces.map((faceId, index) => {
const distance = euclideanDistance(
relatedIdentityDistances[index],
identity.descriptors
);
return {
faceId: faceId,
photoId: relatedFacePhotos[index],
distance
};
});
});
if (!req.query.withScore) {
console.log("No score request.");
return identities;
}
console.log("Looking up score against: " + req.query.withScore);
return Promise.map(identities, (identity) => {
return photoDB.sequelize.query("SELECT * FROM facedescriptors WHERE faceId IN (:id,:faceIds)", {
replacements: {
id: parseInt(req.query.withScore),
faceIds: identity.relatedFaces.map(face => face.faceId)
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((descriptors) => {
let target;
for (let i = 0; i < descriptors.length; i++) {
if (descriptors[i].faceId == req.query.withScore) {
target = descriptors[i].descriptors;
break;
}
}
if (!target) {
console.warn("Could not find descriptor for requested face: " + req.query.withScore);
return;
}
/* For each face's descriptor returned for this identity, compute the distance between the
* requested photo and that face descriptor */
descriptors.forEach((descriptor) => {
for (let i = 0; i < identity.relatedFaces.length; i++) {
if (identity.relatedFaces[i].faceId == descriptor.faceId) {
identity.relatedFaces[i].distance = euclideanDistance(target, descriptor.descriptors);
identity.relatedFaces[i].descriptors = descriptor.descriptors;
return;
}
}
});
});
}, {
concurrency: 5
}).then(() => {
return identities;
});
}).then((identities) => {
return res.status(200).json(identities);
}).catch((error) => {
console.error(error);
return res.status(500).send("Error processing request.");
});
});
module.exports = router;