diff --git a/client/package-lock.json b/client/package-lock.json
index f0949c9..c2cb21e 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -21,6 +21,7 @@
"react-resizable-panels": "^0.0.34",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1",
+ "react-virtuoso": "^4.0.4",
"typescript": "^4.9.4",
"web-vitals": "^2.1.4"
},
@@ -16774,6 +16775,18 @@
}
}
},
+ "node_modules/react-virtuoso": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.0.4.tgz",
+ "integrity": "sha512-X+qHVnDCNFrG4l7CZN7ai9U7ulVZsTKNLVSjOXmN7kVjYwiN2kJVRYXvPBYsRpZbzRPZLJe7/mZMQG0IBfYJbw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16 || >=17 || >= 18",
+ "react-dom": ">=16 || >=17 || >= 18"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/client/package.json b/client/package.json
index cc51be0..a7f27e0 100644
--- a/client/package.json
+++ b/client/package.json
@@ -17,6 +17,7 @@
"react-resizable-panels": "^0.0.34",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1",
+ "react-virtuoso": "^4.0.4",
"typescript": "^4.9.4",
"web-vitals": "^2.1.4"
},
diff --git a/client/src/App.css b/client/src/App.css
index 4bb3688..6b38d0b 100644
--- a/client/src/App.css
+++ b/client/src/App.css
@@ -148,8 +148,8 @@ div {
.Face .Image {
position: relative;
box-sizing: border-box;
- width: 8rem;
- height: 8rem;
+ /*width: 8rem;
+ height: 8rem;*/
display: flex;
justify-content: center;
}
@@ -158,7 +158,7 @@ div {
user-select: none;
display: flex;
flex-direction: column;
- overflow-y: scroll;
+/* overflow-y: scroll;*/
padding: 0.5rem;
height: 100%;
}
@@ -177,4 +177,7 @@ div {
display: grid;
gap: 0.25rem;
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
+ width: 100%;
+ height: 100%;
+ flex-wrap: wrap;
}
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
index b847cb0..249128c 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -8,8 +8,9 @@ import {
Routes,
useParams
} from "react-router-dom";
+import { VirtuosoGrid } from 'react-virtuoso'
import moment from 'moment';
-import equal from "fast-deep-equal";
+//import equal from "fast-deep-equal";
import './App.css';
const base = process.env.PUBLIC_URL; /* /identities -- set in .env */
@@ -191,7 +192,7 @@ const Face = ({ face, onFaceClick, title, ...rest }: any) => {
?
?
:
;
@@ -225,67 +226,7 @@ const Cluster = ({
identity, setIdentity,
identities, setIdentities,
setImage, setSelected }: ClusterProps) => {
- const relatedFacesJSX = useMemo(() => {
- const faceClicked = async (e: any, face: FaceData) => {
- if (!identity) {
- return;
- }
- const el = e.currentTarget;
-
- /* Control -- select / deselect single item */
- if (e.ctrlKey) {
- const cluster = document.querySelector('.Cluster');
- el.classList.toggle('Selected');
- if (!cluster) {
- return;
- }
-
- const selected = [...cluster.querySelectorAll('.Selected')]
- .map((face: any) => face.getAttribute('data-face-id'));
- setSelected(selected);
- return;
- }
-
- /* Shift -- select groups */
- if (e.shiftKey) {
- return;
- }
-
- /* Default to load image */
- e.stopPropagation();
- e.preventDefault();
- setImage(face.photoId);
- }
- if (identity === undefined) {
- return <>>;
- }
-
- return identity.relatedFaces.map((face: FaceData, i: number) => {
- if (i >= 1000) {
- if (i === 1000) {
- return
- too many faces (${identity.relatedFaces.length - 1000} remaining)
-
;
- } else {
- return;
- }
- }
- return (
-
-
-
- );
- });
- }, [identity, setImage, setSelected]);
-
+
const lastNameChanged = (e: any) => {
setIdentity({...identity, lastName: e.currentTarget.value });
};
@@ -299,6 +240,37 @@ const Cluster = ({
setIdentity({...identity, displayName: e.currentTarget.value });
};
+ const faceClicked = async (e: any, face: FaceData) => {
+ if (!identity) {
+ return;
+ }
+ const el = e.currentTarget;
+
+ /* Control -- select / deselect single item */
+ if (e.ctrlKey) {
+ const cluster = document.querySelector('.Cluster');
+ el.classList.toggle('Selected');
+ if (!cluster) {
+ return;
+ }
+
+ const selected = [...cluster.querySelectorAll('.Selected')]
+ .map((face: any) => face.getAttribute('data-face-id'));
+ setSelected(selected);
+ return;
+ }
+
+ /* Shift -- select groups */
+ if (e.shiftKey) {
+ return;
+ }
+
+ /* Default to load image */
+ e.stopPropagation();
+ e.preventDefault();
+ setImage(face.photoId);
+ };
+
const deleteIdentity = async () => {
try {
const res = await window.fetch(
@@ -401,9 +373,17 @@ const Cluster = ({
Faces: {identity.relatedFaces.length}
-
- { relatedFacesJSX }
-
+ (
+
+ )}
+ />
);
};
@@ -650,7 +630,6 @@ const App = () => {
headers: { 'Content-Type': 'application/json' },
});
const faces = await res.json();
- console.log(faces);
setGuess(faces[0]);
} catch (error) {
console.error(error);
diff --git a/server/db/photos.js b/server/db/photos.js
index c11e390..0658612 100755
--- a/server/db/photos.js
+++ b/server/db/photos.js
@@ -195,8 +195,8 @@ function init() {
},
classifiedBy: {
type: Sequelize.DataTypes.ENUM(
- 'machine',
- 'human',
+ 'machine', /* DBSCAN with VGG-Face */
+ 'human', /* Human identified */
'not-a-face'), /* implies "human"; identityId=NULL */
defaultValue: 'machine',
},
diff --git a/server/routes/identities.js b/server/routes/identities.js
index e34cc68..9f24839 100755
--- a/server/routes/identities.js
+++ b/server/routes/identities.js
@@ -9,6 +9,8 @@ require("../db/photos").then(function(db) {
photoDB = db;
});
+const MIN_DISTANCE_COMMIT = 0.0000001
+
const router = express.Router();
const upsertIdentity = async(id, {
@@ -68,13 +70,14 @@ const populateRelatedFaces = async (identity, count) => {
* just return the empty 'unknown face'.
*
* Otherwise, query the DB for 'count' faces */
- if (count === undefined) {
+ if (count !== undefined) {
identity.relatedFaces = await photoDB.sequelize.query(
"SELECT id AS faceId,photoId,faceConfidence " +
"FROM faces " +
"WHERE identityId=:identityId " +
+ "ORDER BY distance ASC" +
limit, {
- replacements: { identityId: identity.identityId },
+ replacements: identity,
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
@@ -111,7 +114,7 @@ router.post('/', async (req, res) => {
if (!identity) {
return;
}
- await updateIdentityFaces(identity);
+ updateIdentityFaces(identity);
await populateRelatedFaces(identity, 1);
return res.status(200).send(identity);
});
@@ -189,8 +192,9 @@ router.get("/faces/guess/:faceId", async (req, res) => {
}
try {
+ /* Look up the requested face... */
const faces = await photoDB.sequelize.query(
- "SELECT faces.*,faceDescriptors.* " +
+ "SELECT faces.*,faceDescriptors.descriptors " +
"FROM faces,faceDescriptors " +
"WHERE faces.id=:faceId " +
"AND faceDescriptors.id=faces.descriptorId", {
@@ -199,6 +203,7 @@ router.get("/faces/guess/:faceId", async (req, res) => {
raw: true
});
+ /* Look up all the identities... */
const identities = await photoDB.sequelize.query(
"SELECT * FROM identities", {
type: photoDB.Sequelize.QueryTypes.SELECT,
@@ -207,14 +212,15 @@ router.get("/faces/guess/:faceId", async (req, res) => {
identities.forEach(x => { x.identityId = x.id; delete x.id;});
faces.forEach((face) => {
+ const currentIdentityId = face.identityId;
face.faceId = face.id;
delete face.id;
- face.identityId = -1;
- face.distance = -1;
face.identity = null;
+
identities.forEach((identity) => {
- if (!identity.descriptors) {
+ if (!identity.descriptors
+ || currentIdentityId === identity.identityId) {
return;
}
@@ -223,7 +229,7 @@ router.get("/faces/guess/:faceId", async (req, res) => {
identity.descriptors
);
- if (face.identityId === -1) {
+ if (face.identity === null) {
face.identityId = identity.identityId;
face.identity = identity;
face.distance = distance;
@@ -239,7 +245,6 @@ router.get("/faces/guess/:faceId", async (req, res) => {
});
/* Delete the VGG-Face descriptors and add relatedFaces[0] */
- const results = [];
await Promise.map(faces, async (face) => {
delete face.descriptors;
if (face.identity.descriptors) {
@@ -290,7 +295,10 @@ router.put("/faces/remove/:id", async (req, res) => {
};
identity.faces = identity.faces.map(id => +id);
- await updateIdentityFaces(identity);
+ /* Do not block on this call finishing -- update can occur
+ * in the background */
+ updateIdentityFaces(identity);
+
return res.status(200).json(identity);
} catch (error) {
console.error(error);
@@ -328,7 +336,9 @@ router.put("/faces/add/:id", async (req, res) => {
};
identity.faces = identity.faces.map(id => +id);
- await updateIdentityFaces(identity);
+ /* Do not block on this call finishing -- update can occur
+ * in the background */
+ updateIdentityFaces(identity);
return res.status(200).json(identity);
} catch (error) {
console.error(error);
@@ -422,7 +432,7 @@ const updateIdentityFaces = async (identity) => {
if (!identity.descriptors) {
const results = await photoDB.sequelize.query(
"SELECT " +
- "descriptors,facesCount,faceId " +
+ "descriptors,facesCount,faceId,displayName " +
"FROM identities " +
"WHERE id=:identityId", {
replacements: identity,
@@ -513,7 +523,7 @@ const updateIdentityFaces = async (identity) => {
.parseFloat(euclideanDistanceArray(face.descriptors, average))
.toFixed(4);
- if (Math.abs(distance - face.distance) > 0.0001) {
+ if (Math.abs(distance - face.distance) > MIN_DISTANCE_COMMIT) {
console.log(
`Updating face ${face.id} to ${round(distance, 2)} ` +
`(${distance - face.distance}) ` +
@@ -526,7 +536,7 @@ const updateIdentityFaces = async (identity) => {
);
}
}, {
- concurrency: 1
+ concurrency: 5
});
let sql = '';
@@ -540,7 +550,7 @@ const updateIdentityFaces = async (identity) => {
/* If the centroid changed, update the identity descriptors to
* the new average */
- if (Math.abs(moved) > 0.0001) {
+ if (Math.abs(moved) > MIN_DISTANCE_COMMIT) {
console.log(
`Updating identity ${identity.identityId} centroid ` +
`(moved ${Number.parseFloat(moved).toFixed(4)}).`);
@@ -622,9 +632,6 @@ router.get("/:id?", async (req, res) => {
});
await Promise.map(identities, async (identity) => {
- console.log(`Updating ${identity.identityId}`);
- await updateIdentityFaces(identity);
-
for (let field in identity) {
if (field.match(/.*Name/) && identity[field] === null) {
identity[field] = '';