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) }; }); });