From f1c1b7967297bdf48ac8c152908ce215b5b7ec34 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Wed, 25 Jan 2023 22:57:42 -0800 Subject: [PATCH] Face selection is working pretty well Signed-off-by: James Ketrenos --- client/src/App.css | 43 +++++++-- client/src/App.tsx | 176 ++++++++++++++++++++---------------- server/routes/identities.js | 97 +++++++++++--------- 3 files changed, 188 insertions(+), 128 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 9584df9..af6ba70 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -49,10 +49,9 @@ div { user-select: none; overflow-y: scroll; overflow-x: clip; - height: 100%; width: 100%; gap: 0.25rem; - grid-template-columns: repeat(auto-fill, minmax(8.5rem, auto)); + grid-template-columns: repeat(auto-fill, minmax(4.25rem, auto)); } .Face { @@ -73,14 +72,29 @@ div { flex-direction: column; } +.Viewer { + display: flex; + flex-direction: column; + position: relative; +} + .PhotoPanel { display: flex; flex-direction: column; - height: 100%; - width: 100%; justify-content: center; } +.Guess { + display: flex; + justify-content: center; + align-items: center; +} + +button { + padding: 0.5rem; + min-width: 4rem; +} + .Image .FaceBox { border: 1px solid red; position: absolute; @@ -94,9 +108,6 @@ div { .Image { display: flex; position: relative; - background-size: contain !important; - background-repeat: no-repeat no-repeat !important; - background-position: 50% 50% !important; } .PhotoPanel .FaceInfo { @@ -112,6 +123,10 @@ div { margin-top: 0.25rem; } +.Identities .UnknownFace { + font-size: 2rem; +} + .UnknownFace { display: flex; align-items: center; @@ -158,6 +173,11 @@ div { height: 10rem; } +.Identities .Face .Image { + min-width: 4rem; + min-height: 4rem; +} + .Face .Image { position: relative; box-sizing: border-box; @@ -186,6 +206,13 @@ div { align-items: flex-start; } + +.Viewer .PhotoPanel img { + object-fit: contain; + max-width: 100%; + max-height: 100%; +} + .Image img { object-fit: cover; /* contain */ width: 100%; @@ -195,7 +222,7 @@ div { .Cluster .Faces { display: grid; gap: 0.25rem; - grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); width: 100%; flex-wrap: wrap; } \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 04c78d9..d950ee0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -129,12 +129,7 @@ const Photo = ({ photoId, onFaceClick }: any) => {
{image.filename} + src={`${base}/../${image.path}thumbs/scaled/${image.filename}`.replace(/ /g, '%20')}/> { faces }
{ @@ -326,20 +321,6 @@ const Cluster = ({ }); await res.json(); setIdentity({ ...identity, ...values }); - setIdentities( - [...identities] - .sort((A: IdentityData, B: IdentityData) => { - /* Sort the Unknown (-1) identity to the end */ - if (A.identityId === -1) { - return +1; - } - if (B.identityId === -1) { - return -1; - } - /* Otherwise sort alphabetically by displayName */ - return A.displayName.localeCompare(B.displayName); - }) - ); } catch (error) { console.error(error); } @@ -361,20 +342,6 @@ const Cluster = ({ }); const created = await res.json(); setIdentity(created); - setIdentities( - [created, ...identities] - .sort((A: IdentityData, B: IdentityData) => { - /* Sort the Unknown (-1) identity to the end */ - if (A.identityId === -1) { - return +1; - } - if (B.identityId === -1) { - return -1; - } - /* Otherwise sort alphabetically by displayName */ - return A.displayName.localeCompare(B.displayName); - }) - ); } catch (error) { console.error(error); } @@ -537,6 +504,62 @@ const Button = ({ onClick, children }: any) => { ); }; +/* returns true if update to identities array occurred */ +const updateIdentityReferences = ( + identities: IdentityData[], + identity: IdentityData) : boolean => { + + if (identity.identityId === -1) { + console.warn('Identity Unknown (-1) attempting to be updated'); + return false; + } + + const targetIndex = identities.findIndex( + x => x.identityId === identity.identityId); + if (targetIndex === -1) { + identities.push(identity); + return true; + } + const target = identities[targetIndex]; + + /* + IdentityData fields we check to make sure they are the same: + + lastName: string, + middleName: string, + firstName: string, + displayName: string, + facesCount: number, + faceId: number + !identityId: number + !relatedFaces: FaceData[], + !descriptors: number[], + */ + + let same = true; + + [ 'lastName', 'firstName', 'middleName', + 'displayName', 'faceId', 'facesCount', 'faceId' ] + .forEach((field: string) => { + same = same && (target as any)[field] === (identity as any)[field]; + }); + + if (same) { + return false; + } + + identities[targetIndex] = { + ...identity, + relatedFaces: target.relatedFaces + }; + + /* relatedFaces is a list of references to identity */ + identity.relatedFaces.forEach(face => { + face.identity = identity; + }); + + return true; +}; const App = () => { const { identityId, faceId } = useParams(); @@ -574,32 +597,28 @@ const App = () => { /* If the identity changes, update its entry in the identities list * NOTE: Blocks update to 'Unknown' (-1) fake identity */ useEffect(() => { - if (!identity || identities.length === 0 || identity.identityId === -1) { + if (identity.identityId === -1) { return; } - for (let key in identities) { - if (identities[key].identityId === identity.identityId) { - let same = true; - [ 'displayName', 'firstName', 'lastName', 'middleName' ] - .forEach((field: string) => { - same = same - && (identities[key] as any)[field] === (identity as any)[field]; - }); - if (!same) { - console.log(`Updating `, identity, identities[key]); - identities[key] = { - ...identity, - relatedFaces: identities[key].relatedFaces - }; - /* relatedFaces is a list of references to identity */ - identity.relatedFaces.forEach(face => { - face.identity = identity; - }); - setIdentities([...identities]); - } - return; - } + + if (!updateIdentityReferences(identities, identity)) { + return; } + + setIdentities( + [...identities] + .sort((A: IdentityData, B: IdentityData) => { + /* Sort the Unknown (-1) identity to the end */ + if (A.identityId === -1) { + return +1; + } + if (B.identityId === -1) { + return -1; + } + /* Otherwise sort alphabetically by displayName */ + return A.displayName.localeCompare(B.displayName); + }) + ); }, [identity, setIdentities, identities]); /* If the identity changes, scroll it into view in the Identities list */ @@ -720,7 +739,7 @@ const App = () => { } }; - const markSelectedIncorrectIdentity = async () => { + const removeFaceFromIdentity = async () => { if (!identity) { return; } @@ -735,9 +754,9 @@ const App = () => { removeFacesFromIdentity(results.removed); deselectAll(); - if (identity.faceId !== results.faceId) { - setIdentity({...identity, ...{ faceId: results.faceId }}); - setIdentities([...identities]); + if (results.faceId !== undefined + && identity.faceId !== results.faceId) { + setIdentity({...identity, faceId: results.faceId }); } } catch (error) { console.error(error); @@ -778,7 +797,6 @@ const App = () => { target.faceId = results.faceId; } deselectAll(); - setIdentities([...identities]); } catch (error) { console.error(error); } @@ -798,7 +816,7 @@ const App = () => { } }; - const markSelectedNotFace = async () => { + const updateFasAsNotFace = async () => { try { const res = await window.fetch( `${base}/api/v1/faces`, { @@ -871,7 +889,8 @@ const App = () => { e.stopPropagation(); e.preventDefault(); - if (identity.identityId !== identityId) { + if (identity.identityId !== identityId + || identity.facesCount === 0) { [...document.querySelectorAll('.Cluster .Faces img')] .forEach((img: any) => { img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; @@ -901,8 +920,8 @@ const App = () => { } { selected.length !== 0 && <> - - + + } @@ -913,18 +932,17 @@ const App = () => { - {image === 0 &&
Select image to view
} - {image !== 0 && } - {guess !== undefined && guess.identity &&
- -
} +
+ {image === 0 &&
Select image to view
} + {image !== 0 && } + {guess !== undefined && guess.identity &&
+ +
} +
@@ -932,7 +950,7 @@ const App = () => { Loading...
} { loaded && + {...{ onFaceClick: identitiesOnFaceClick, identities }}/> } diff --git a/server/routes/identities.js b/server/routes/identities.js index a7fa0c6..a9c6088 100755 --- a/server/routes/identities.js +++ b/server/routes/identities.js @@ -9,7 +9,7 @@ require("../db/photos").then(function(db) { photoDB = db; }); -const MIN_DISTANCE_COMMIT = 0.0000001 +const MIN_DISTANCE_COMMIT = 0.0001 const router = express.Router(); @@ -258,15 +258,15 @@ router.get("/faces/guess/:faceId", async (req, res) => { }; }); -router.put("/faces/remove/:id", async (req, res) => { +router.put("/faces/remove/:identityId", 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) { + const identityId = parseInt(req.params.identityId); + if (identityId != req.params.identityId) { return res.status(400).send({ message: "Invalid identity id." }); } @@ -274,30 +274,35 @@ router.put("/faces/remove/:id", async (req, res) => { return res.status(400).send({ message: "No faces supplied." }); } + /* Convert faces array to numbers and filter any non-numbers */ + let faceIds = req.body.faces + .map(faceId => +faceId) + .filter(faceId => !isNaN(faceId)); + try { await photoDB.sequelize.query( "UPDATE faces SET identityId=null " + "WHERE id IN (:faceIds)", { replacements: { - identityId: id, - faceIds: req.body.faces + identityId, + faceIds } }); const identity = { - identityId: id, - faces: req.body.faces + identityId, + removed: faceIds }; - identity.removed = identity.faces.map(id => +id); + /* If the primary faceId was removed, update the identity's faceId * to a new faceId */ - const faceIds = await photoDB.sequelize.query(` - SELECT faceId FROM identities WHERE id=:identityId`, { + const identityFaceIds = await photoDB.sequelize.query(` + SELECT faceId AS identityFaceId FROM identities WHERE id=:identityId`, { replacements: identity, type: photoDB.Sequelize.QueryTypes.SELECT, raw: true }); - if (identity.removed.indexOf(faceIds[0]) !== -1) { + if (identity.removed.indexOf(identityFaceIds[0]) !== -1) { const newFaceId = await photoDB.sequelize.query(` SELECT faceId FROM faces WHERE identityId=:identityId ORDER BY distance ASC @@ -343,7 +348,7 @@ router.put("/faces/add/:id", async (req, res) => { } if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) { - return res.status(400).send("No faces supplied."); + return res.status(400).send({ message: "No faces supplied." }); } /* Convert faces array to numbers and filter any non-numbers */ @@ -442,8 +447,13 @@ router.put("/faces/add/:id", async (req, res) => { /* Do not block on this call finishing -- update can occur * in the background */ - Promise.map([identity, ...tuples], identity => { - updateIdentityFaces(identity); + Promise.map([identity, ...tuples], (x, i) => { + try { + updateIdentityFaces(x); + } catch (error) { + console.log(i, x); + throw error; + } }, { concurrency: 1 }); @@ -649,32 +659,37 @@ const updateIdentityFaces = async (identity) => { const t = await photoDB.sequelize.transaction(); try { /* If the average position has not changed, then face distances should - * not change either! */ - await Promise.map(faces, async (face) => { - /* All the buffer are already arrays, so use the short-cut version */ - const distance = Number - .parseFloat(face.updatedDistance) - .toFixed(4); - - if (Math.abs(face.updatedDistance - face.distance) - > MIN_DISTANCE_COMMIT) { - console.log( - `Updating face ${face.id} to ${round(distance, 2)} ` + - `(${distance - face.distance}) ` + - `from identity ${identity.identityId} (${identity.displayName})`); - face.distance = face.updatedDistance; - delete face.updatedDistance; - await photoDB.sequelize.query( - 'UPDATE faces SET distance=:distance WHERE id=:id', { - replacements: face, - transaction: t - } - ); - } - }, { - concurrency: 5 - }); - + * not change either! + * + * Do not update all the faces unless the centroid has moved a fair + * amount */ + if (Math.abs(moved) > MIN_DISTANCE_COMMIT) { + await Promise.map(faces, async (face) => { + /* All the buffer are already arrays, so use the short-cut version */ + const distance = Number + .parseFloat(face.updatedDistance) + .toFixed(4); + + if (Math.abs(face.updatedDistance - face.distance) + > MIN_DISTANCE_COMMIT) { + console.log( + `Updating face ${face.id} to ${round(distance, 2)} ` + + `(${distance - face.distance}) ` + + `from identity ${identity.identityId} (${identity.displayName})`); + face.distance = face.updatedDistance; + delete face.updatedDistance; + await photoDB.sequelize.query( + 'UPDATE faces SET distance=:distance WHERE id=:id', { + replacements: face, + transaction: t + } + ); + } + }, { + concurrency: 1 + }); + } + let sql = ''; /* If there is a new closestId, then set the faceId field */ if (closestId !== -1 && closestId !== identity.faceId) {