Working through identity editor

Signed-off-by: James Ketrenos <james_gitlab@ketrenos.com>
This commit is contained in:
James Ketrenos 2020-02-02 15:19:19 -08:00
parent efcaa7f5b8
commit 2037a3f636
3 changed files with 183 additions and 109 deletions

View File

@ -13,7 +13,6 @@
*/ */
function createFace(faceId, photoId, selectable) { function createFace(faceId, photoId, selectable) {
var div = document.createElement("div"); var div = document.createElement("div");
div.classList.add("face"); div.classList.add("face");
@ -21,7 +20,14 @@ function createFace(faceId, photoId, selectable) {
div.setAttribute("face-id", faceId); div.setAttribute("face-id", faceId);
div.style.backgroundImage = "url(face-data/" + (faceId % 100) + "/" + faceId + "-original.png)"; div.style.backgroundImage = "url(face-data/" + (faceId % 100) + "/" + faceId + "-original.png)";
div.addEventListener("click", (event) => { div.addEventListener("click", (event) => {
if (!selectable || event.ctrlKey) { if (event.shiftKey) { /* identities */
let faceId = parseInt(event.currentTarget.getAttribute("face-id"));
if (faceId) {
window.location.href = "identities.html?id=" + faceId;
} else {
alert("No face id mapped to face.");
}
} else if (!selectable || event.ctrlKey) { /* face-explorer */
let photoId = parseInt(event.currentTarget.getAttribute("photo-id")); let photoId = parseInt(event.currentTarget.getAttribute("photo-id"));
if (photoId) { if (photoId) {
window.open("face-explorer.html?" + photoId, "photo-" + photoId); window.open("face-explorer.html?" + photoId, "photo-" + photoId);
@ -39,20 +45,22 @@ function createFace(faceId, photoId, selectable) {
return div; return div;
} }
function shuffle(array) {
var i = array.length, tmp, random;
while (i > 0) {
random = Math.floor(Math.random() * i);
i--;
tmp = array[i];
array[i] = array[random];
array[random] = tmp;
}
return array;
}
document.addEventListener("DOMContentLoaded", (event) => { document.addEventListener("DOMContentLoaded", (event) => {
loadFace(); let params = window.location.search ? window.location.search.replace(/^\?/, "").split("&") : [],
id;
for (let i in params) {
let parts = params[i].split("=");
switch (parts[0]) {
case "id":
id = parseInt(parts[1]);
if (id != parts[1]) {
id = undefined;
}
break;
}
}
loadFace(id);
const newIdentity = document.getElementById("new-identity"); const newIdentity = document.getElementById("new-identity");
newIdentity.appendChild(createNewIdenityEditor()); newIdentity.appendChild(createNewIdenityEditor());
}); });
@ -68,7 +76,11 @@ function loadFace(id) {
const face = faces[0]; const face = faces[0];
getIdentities(face.id); if (face.identityId) {
getIdentity(face.identityId);
} else {
getIdentities(face.id);
}
const editor = document.createElement("div"); const editor = document.createElement("div");
editor.classList.add("editor"); editor.classList.add("editor");
@ -86,7 +98,7 @@ function loadFace(id) {
editor.appendChild(row); editor.appendChild(row);
row = document.createElement("div"); row = document.createElement("div");
row.classList.add("editor-row"); row.classList.add("editor-row");
let message = "Related faces: "; let message = "Unassigned related faces: ";
if (face.relatedFaces.length > 0) { if (face.relatedFaces.length > 0) {
message += face.relatedFaces.length + " related (tap to deselect)"; message += face.relatedFaces.length + " related (tap to deselect)";
} else { } else {
@ -133,11 +145,26 @@ function showMore(row, faces) {
} }
} }
function getIdentities(id) {
function getIdentity(identityId) {
const identitiesBlock = document.getElementById("identities"); const identitiesBlock = document.getElementById("identities");
identitiesBlock.innerHTML = ""; identitiesBlock.innerHTML = "";
const search = id ? "?withScore=" + id : ""; window.fetch("api/v1/identities/" + identityId).then(res => res.json()).then((identities) => {
identities.forEach((identity) => {
const block = createIdentityBlock(identity, true),
button = createUseThisIdentityButton(identity);
block.insertBefore(button, block.firstChild);
identitiesBlock.appendChild(block);
})
});
}
function getIdentities(faceId) {
const identitiesBlock = document.getElementById("identities");
identitiesBlock.innerHTML = "";
const search = faceId ? "?withScore=" + faceId : "";
window.fetch("api/v1/identities" + search).then(res => res.json()).then((identities) => { window.fetch("api/v1/identities" + search).then(res => res.json()).then((identities) => {
identities.sort((a, b) => { identities.sort((a, b) => {
if (a.lastName == b.lastName) { if (a.lastName == b.lastName) {
@ -147,88 +174,109 @@ function getIdentities(id) {
}); });
identities.forEach((identity) => { identities.forEach((identity) => {
const block = document.createElement("div"); const block = createIdentityBlock(identity),
block.classList.add("block"); button = createUseThisIdentityButton(identity);
block.insertBefore(button, block.firstChild);
const button = document.createElement("button");
button.textContent = "Use this identity";
block.appendChild(button);
button.addEventListener("click", (event) => {
const object = {
faces: []
};
Array.prototype.forEach.call(document.body.querySelectorAll("#face-editor .face"), (face) => {
if (!face.hasAttribute("disabled")) {
object.faces.push(face.getAttribute("face-id"));
}
});
window.fetch("api/v1/identities/faces/add/" + identity.id, {
method: "PUT",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(object)
}).then(res => res.json()).then((identities) => {
console.log("Updated identity: ", identities[0]);
const face = document.body.querySelector("#face-editor .face");
loadFace(parseInt(face.getAttribute("face-id")));
});
});
let div = document.createElement("div");
div.textContent = identity.name;
block.appendChild(div);
div = document.createElement("div");
div.textContent = "Related faces " + identity.relatedFaces.length;
block.appendChild(div);
identity.relatedFaces.sort((a, b) => {
return a.distance - b.distance;
});
div = document.createElement("div");
div.classList.add("face-block");
let minDistance = {
distance: 1
};
for (let i = 0; i < identity.relatedFaces.length && i < 4; i++) {
const target = identity.relatedFaces[i];
const facePhoto = createFace(target.faceId, target.photoId),
distance = document.createElement("div");
if (target.distance < minDistance.distance) {
minDistance.distance = target.distance;
minDistance.photoId = target.photoId;
minDistance.faceId = target.faceId;
}
distance.classList.add("distance");
distance.textContent = target.distance.toFixed(2);
facePhoto.appendChild(distance);
div.appendChild(facePhoto);
}
if (minDistance.distance < 0.45) {
const bestMatch = document.getElementById("best-match");
const distance = document.createElement("div"),
facePhoto = createFace(minDistance.faceId, minDistance.photoId);
distance.classList.add("distance");
distance.textContent = minDistance.distance.toFixed(2);
facePhoto.appendChild(distance);
bestMatch.innerHTML = "";
bestMatch.appendChild(facePhoto);
} else {
const bestMatch = document.getElementById("best-match");
bestMatch.innerHTML = "No best guess for match.";
}
block.appendChild(div);
identitiesBlock.appendChild(block); identitiesBlock.appendChild(block);
}); });
}); });
} }
function createUseThisIdentityButton(identity) {
const button = document.createElement("button");
button.textContent = "Use this identity";
button.addEventListener("click", (event) => {
const object = {
faces: []
};
Array.prototype.forEach.call(document.body.querySelectorAll("#face-editor .face"), (face) => {
if (!face.hasAttribute("disabled")) {
object.faces.push(face.getAttribute("face-id"));
}
});
window.fetch("api/v1/identities/faces/add/" + identity.id, {
method: "PUT",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(object)
}).then(res => res.json()).then((identities) => {
console.log("Updated identity: ", identities[0]);
const face = document.body.querySelector("#face-editor .face");
loadFace(parseInt(face.getAttribute("face-id")));
});
});
return button;
}
function createIdentityBlock(identity, nolimit) {
const block = document.createElement("div");
block.classList.add("block");
let div = document.createElement("div");
div.textContent = identity.name;
block.appendChild(div);
div = document.createElement("div");
div.textContent = "Related faces " + identity.relatedFaces.length;
block.appendChild(div);
identity.relatedFaces.sort((a, b) => {
return a.distance - b.distance;
});
div = document.createElement("div");
div.classList.add("face-block");
if (nolimit) {
div.classList.add("face-block-nolimit");
}
let minDistance = {
distance: 1
};
for (let i = 0; i < identity.relatedFaces.length; i++) {
if (!nolimit && i >= 4) {
break;
}
const target = identity.relatedFaces[i];
const facePhoto = createFace(target.faceId, target.photoId),
distance = document.createElement("div");
if (target.distance < minDistance.distance) {
minDistance.distance = target.distance;
minDistance.photoId = target.photoId;
minDistance.faceId = target.faceId;
}
distance.classList.add("distance");
distance.textContent = target.distance.toFixed(2);
facePhoto.appendChild(distance);
div.appendChild(facePhoto);
}
if (minDistance.distance < 0.45) {
const bestMatch = document.getElementById("best-match");
const distance = document.createElement("div"),
facePhoto = createFace(minDistance.faceId, minDistance.photoId);
distance.classList.add("distance");
distance.textContent = minDistance.distance.toFixed(2);
facePhoto.appendChild(distance);
bestMatch.innerHTML = "";
bestMatch.appendChild(facePhoto);
} else {
const bestMatch = document.getElementById("best-match");
bestMatch.innerHTML = "No best guess for match.";
}
block.appendChild(div);
return block;
}
function createNewIdenityEditor() { function createNewIdenityEditor() {
const block = document.createElement("div"); const block = document.createElement("div");
block.classList.add("block"); block.classList.add("block");
@ -343,6 +391,13 @@ body {
max-height: 128px; max-height: 128px;
} }
.face-block-nolimit {
min-width: 128px;
max-width: initial;
min-height: 128px;
max-height: initial;
}
.face-block .face { .face-block .face {
max-width: 64px; max-width: 64px;
max-height: 64px; max-height: 64px;

34
server/routes/faces.js Normal file → Executable file
View File

@ -81,20 +81,36 @@ router.get("/:id?", (req, res) => {
} }
} }
return photoDB.sequelize.query("SELECT COUNT(id) AS count FROM faces WHERE faceConfidence>=0.9 AND identityId IS NULL", { let promise;
type: photoDB.Sequelize.QueryTypes.SELECT, if (id) {
raw: true promise = photoDB.sequelize.query(
}).then((results) => { "SELECT * FROM faces WHERE id=:id", {
const random = Math.floor(Math.random() * results[0].count);
return photoDB.sequelize.query(
"SELECT * FROM faces WHERE faceConfidence>=0.9 AND identityId IS NULL ORDER BY id LIMIT :index,1", {
replacements: { replacements: {
index: random id: id
}, },
type: photoDB.Sequelize.QueryTypes.SELECT, type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}); });
}).then((faces) => { } else {
promise = photoDB.sequelize.query(
"SELECT COUNT(id) AS count FROM faces WHERE faceConfidence>=0.9 AND identityId IS NULL", {
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((results) => {
const random = Math.floor(Math.random() * results[0].count);
return photoDB.sequelize.query(
"SELECT * FROM faces WHERE faceConfidence>=0.9 AND identityId IS NULL ORDER BY id LIMIT :index,1", {
replacements: {
index: random
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
});
}
return promise.then((faces) => {
console.log("Looking up " + faces.map(face => face.id).join(","));
return photoDB.sequelize.query( return photoDB.sequelize.query(
"SELECT relatedFaces.photoId AS photoId,fd.face1Id,fd.face2Id,fd.distance,relatedFaces.faceConfidence " + "SELECT relatedFaces.photoId AS photoId,fd.face1Id,fd.face2Id,fd.distance,relatedFaces.faceConfidence " +
"FROM (SELECT id,photoId,faceConfidence FROM faces WHERE faces.id IN (:ids)) AS faces " + "FROM (SELECT id,photoId,faceConfidence FROM faces WHERE faces.id IN (:ids)) AS faces " +

View File

@ -110,12 +110,13 @@ router.get("/:id?", (req, res) => {
} }
} }
const filter = id ? "WHERE id=:id " : ""; const filter = id ? "WHERE identities.id=:id " : "";
return photoDB.sequelize.query("SELECT " + return photoDB.sequelize.query("SELECT " +
"identities.*," + "identities.*," +
"GROUP_CONCAT(faces.id) AS relatedFaceIds," + "GROUP_CONCAT(faces.id) AS relatedFaceIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " + "GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds," +
"GROUP_CONCAT(faces.identityDistance) AS relatedIdentityDistances " +
"FROM identities " + "FROM identities " +
"INNER JOIN faces ON identities.id=faces.identityId " + "INNER JOIN faces ON identities.id=faces.identityId " +
filter + filter +
@ -128,7 +129,8 @@ router.get("/:id?", (req, res) => {
}).then((identities) => { }).then((identities) => {
identities.forEach((identity) => { identities.forEach((identity) => {
const relatedFaces = identity.relatedFaceIds.split(","), const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(","); relatedFacePhotos = identity.relatedFacePhotoIds.split(","),
relatedIdentityDistances = identity.relatedIdentityDistances.split(",");
if (relatedFaces.length != relatedFacePhotos.length) { if (relatedFaces.length != relatedFacePhotos.length) {
console.warn("Face ID to Photo ID mapping doesn't match!"); console.warn("Face ID to Photo ID mapping doesn't match!");
} }
@ -137,7 +139,8 @@ router.get("/:id?", (req, res) => {
identity.relatedFaces = relatedFaces.map((faceId, index) => { identity.relatedFaces = relatedFaces.map((faceId, index) => {
return { return {
faceId: faceId, faceId: faceId,
photoId: relatedFacePhotos[index] photoId: relatedFacePhotos[index],
distance: parseFloat(relatedIdentityDistances[index] !== undefined ? relatedIdentityDistances[index] : -1)
}; };
}); });
}); });