diff --git a/client/src/App.tsx b/client/src/App.tsx index 4de907a..7b91ef2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -403,6 +403,7 @@ const Button = ({ onClick, children }: any) => { const App = () => { const [identities, setIdentities] = useState([]); const { identityId, faceId } = useParams(); + const [selectedIdentities, setSelectedIdentities] = useState([]); const [identity, setIdentity] = useState(undefined); const [image, setImage] = useState(0); const { loading, data } = useApi( @@ -503,6 +504,27 @@ const App = () => { } }; + const changeSelectedIdentity = async () => { + + if (selectedIdentities.length === 0) { + window.alert('You need to select an identity first (CTRL+CLICK)'); + return; + } + try { + const res = await window.fetch( + `${base}/api/v1/identities/faces/add/${selectedIdentities[0]}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ faces: selected }) + }); + const data = await res.json(); + + removeFacesFromIdentities(data.faces); + } catch (error) { + console.error(error); + } + }; + const markSelectedNotFace = async () => { try { const res = await window.fetch( @@ -534,7 +556,36 @@ const App = () => { }; const identitiesOnFaceClick = (e: any, face: FaceData) => { + const identitiesEl = document.querySelector('.Identities'); + if (!identitiesEl) { + return; + } const identityId = face.identityId; + + const el = e.currentTarget; + + /* Control -- select / deselect single item */ + if (e.ctrlKey) { + [...identitiesEl.querySelectorAll('.Selected')].forEach(item => { + item.classList.remove('Selected') + }); + el.classList.toggle('Selected'); + + const selected = [...identitiesEl.querySelectorAll('.Selected')] + .map((face: any) => face.getAttribute('data-identity-id')); + setSelectedIdentities(selected); + return; + } + + /* Shift -- select groups */ + if (e.shiftKey) { + return; + } + + /* Default to load image */ + e.stopPropagation(); + e.preventDefault(); + loadIdentity(identityId); } @@ -559,6 +610,7 @@ const App = () => { { selected.length !== 0 && <> + } diff --git a/ketrface/ketrface/db.py b/ketrface/ketrface/db.py index 17f02a3..02544ed 100644 --- a/ketrface/ketrface/db.py +++ b/ketrface/ketrface/db.py @@ -99,7 +99,8 @@ def load_faces(db_path ): FROM faces INNER JOIN photos ON (photos.duplicate == 0 OR photos.duplicate IS NULL) JOIN facedescriptors ON (faces.descriptorId=facedescriptors.id) - WHERE faces.identityId IS null + WHERE faces.identityId IS null + AND faces.classifiedBy != 'not-a-face' AND faces.photoId=photos.id ''') for row in res.fetchall(): diff --git a/server/routes/identities.js b/server/routes/identities.js index 528bf29..4f52255 100755 --- a/server/routes/identities.js +++ b/server/routes/identities.js @@ -134,7 +134,24 @@ router.put('/:id', async (req, res) => { return res.status(200).send(identity); }); -router.put("/faces/remove/:id", (req, res) => { + +const addFaceToIdentityDescriptors = (identity, face) => { +}; + +const removeFaceToIdentityDescriptors = (identity, face) => { +}; + +const writeIdentityDescriptors = async (identity) => { + await photoDB.sequelize.query( + 'UPDATE identities ' + + 'SET descriptors=:descriptors' + + 'WHERE id=:identityId', { + replacements: identity + } + ); +}; + +router.put("/faces/remove/:id", async (req, res) => { console.log(`PUT ${req.url}`) if (!req.user.maintainer) { console.warn(`${req.user.name} attempted to modify photos.`); @@ -150,23 +167,28 @@ router.put("/faces/remove/:id", (req, res) => { return res.status(400).send({ message: "No faces supplied." }); } - return photoDB.sequelize.query( - "UPDATE faces SET identityId=null " + - "WHERE id IN (:faceIds)", { - replacements: { - identityId: id, - faceIds: req.body.faces - } - }).then(() => { + try { + await photoDB.sequelize.query( + "UPDATE faces SET identityId=null " + + "WHERE id IN (:faceIds)", { + replacements: { + identityId: id, + faceIds: req.body.faces + } + }); const identity = { id: id, faces: req.body.faces }; + identity.faces = identity.faces.map(id => +id); + + updateIdentityDescriptors(identity); + return res.status(200).json(identity); - }).catch((error) => { + } catch (error) { console.error(error); return res.status(500).send({message: "Error processing request." }); - }); + }; }); router.put("/faces/add/:id", async (req, res) => { @@ -197,7 +219,11 @@ router.put("/faces/add/:id", async (req, res) => { id: id, faces: req.body.faces }; - return res.status(200).json([identity]); + identity.faces = identity.faces.map(id => +id); + + updateIdentityDescriptors(identity); + + return res.status(200).json(identity); } catch (error) { console.error(error); return res.status(500).send("Error processing request."); @@ -249,10 +275,16 @@ router.post("/", (req, res) => { }); function bufferToFloat32Array(buffer) { - return new Float64Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Float64Array.BYTES_PER_ELEMENT); + return new Float64Array(buffer.buffer, + buffer.byteOffset, + buffer.byteLength / Float64Array.BYTES_PER_ELEMENT); } function euclideanDistance(a, b) { + if (!a.buffer || !b.buffer) { + return -1; + } + let A = bufferToFloat32Array(a); let B = bufferToFloat32Array(b); let sum = 0; @@ -270,7 +302,7 @@ const getUnknownIdentity = async (faceCount) => { firstName: '', middleName: '', displayName: 'Unknown', - descriptors: [], + descriptors: new Float32Array(0), relatedFaces: [] }; const limit = faceCount @@ -293,7 +325,7 @@ const getUnknownIdentity = async (faceCount) => { unknownIdentity.relatedFaces.forEach(face => { face.identityId = -1; face.distance = face.faceConfidence; - face.descriptors = []; + face.descriptors = new Float32Array(0); delete face.faceConfidence; }); return unknownIdentity; @@ -361,10 +393,15 @@ router.get("/:id?", async (req, res) => { descriptors = descriptors.map(entry => entry.descriptors); identity.relatedFaces = relatedFaces.map((faceId, index) => { - const distance = euclideanDistance( - descriptors[index], - identity.descriptors - ); + let distance = 0; + if (descriptors[index] && identity.descriptors) { + distance = euclideanDistance( + descriptors[index], + identity.descriptors + ); + } else { + distance = -1; + } return { identityId: identity.id,