diff --git a/frontend/identities.html b/frontend/identities.html
index 10d4609..4002f72 100755
--- a/frontend/identities.html
+++ b/frontend/identities.html
@@ -13,7 +13,6 @@
*/
-
function createFace(faceId, photoId, selectable) {
var div = document.createElement("div");
div.classList.add("face");
@@ -21,7 +20,14 @@ function createFace(faceId, photoId, selectable) {
div.setAttribute("face-id", faceId);
div.style.backgroundImage = "url(face-data/" + (faceId % 100) + "/" + faceId + "-original.png)";
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"));
if (photoId) {
window.open("face-explorer.html?" + photoId, "photo-" + photoId);
@@ -39,20 +45,22 @@ function createFace(faceId, photoId, selectable) {
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) => {
- 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");
newIdentity.appendChild(createNewIdenityEditor());
});
@@ -68,7 +76,11 @@ function loadFace(id) {
const face = faces[0];
- getIdentities(face.id);
+ if (face.identityId) {
+ getIdentity(face.identityId);
+ } else {
+ getIdentities(face.id);
+ }
const editor = document.createElement("div");
editor.classList.add("editor");
@@ -86,7 +98,7 @@ function loadFace(id) {
editor.appendChild(row);
row = document.createElement("div");
row.classList.add("editor-row");
- let message = "Related faces: ";
+ let message = "Unassigned related faces: ";
if (face.relatedFaces.length > 0) {
message += face.relatedFaces.length + " related (tap to deselect)";
} else {
@@ -133,11 +145,26 @@ function showMore(row, faces) {
}
}
-function getIdentities(id) {
+
+function getIdentity(identityId) {
const identitiesBlock = document.getElementById("identities");
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) => {
identities.sort((a, b) => {
if (a.lastName == b.lastName) {
@@ -147,88 +174,109 @@ function getIdentities(id) {
});
identities.forEach((identity) => {
- const block = document.createElement("div");
- block.classList.add("block");
-
- 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);
+ const block = createIdentityBlock(identity),
+ button = createUseThisIdentityButton(identity);
+ block.insertBefore(button, block.firstChild);
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() {
const block = document.createElement("div");
block.classList.add("block");
@@ -343,6 +391,13 @@ body {
max-height: 128px;
}
+.face-block-nolimit {
+ min-width: 128px;
+ max-width: initial;
+ min-height: 128px;
+ max-height: initial;
+}
+
.face-block .face {
max-width: 64px;
max-height: 64px;
diff --git a/server/routes/faces.js b/server/routes/faces.js
old mode 100644
new mode 100755
index af89ba8..c370223
--- a/server/routes/faces.js
+++ b/server/routes/faces.js
@@ -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", {
- 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", {
+ let promise;
+ if (id) {
+ promise = photoDB.sequelize.query(
+ "SELECT * FROM faces WHERE id=:id", {
replacements: {
- index: random
+ id: id
},
type: photoDB.Sequelize.QueryTypes.SELECT,
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(
"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 " +
diff --git a/server/routes/identities.js b/server/routes/identities.js
index 3b506d7..db76a08 100755
--- a/server/routes/identities.js
+++ b/server/routes/identities.js
@@ -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 " +
"identities.*," +
"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 " +
"INNER JOIN faces ON identities.id=faces.identityId " +
filter +
@@ -128,7 +129,8 @@ router.get("/:id?", (req, res) => {
}).then((identities) => {
identities.forEach((identity) => {
const relatedFaces = identity.relatedFaceIds.split(","),
- relatedFacePhotos = identity.relatedFacePhotoIds.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!");
}
@@ -137,7 +139,8 @@ router.get("/:id?", (req, res) => {
identity.relatedFaces = relatedFaces.map((faceId, index) => {
return {
faceId: faceId,
- photoId: relatedFacePhotos[index]
+ photoId: relatedFacePhotos[index],
+ distance: parseFloat(relatedIdentityDistances[index] !== undefined ? relatedIdentityDistances[index] : -1)
};
});
});