Added infinite scrollers

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2023-01-24 15:50:55 -08:00
parent f8b33a65b8
commit d45600f6a7
6 changed files with 93 additions and 90 deletions

View File

@ -21,6 +21,7 @@
"react-resizable-panels": "^0.0.34", "react-resizable-panels": "^0.0.34",
"react-router-dom": "^6.6.2", "react-router-dom": "^6.6.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-virtuoso": "^4.0.4",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"web-vitals": "^2.1.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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@ -17,6 +17,7 @@
"react-resizable-panels": "^0.0.34", "react-resizable-panels": "^0.0.34",
"react-router-dom": "^6.6.2", "react-router-dom": "^6.6.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-virtuoso": "^4.0.4",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

View File

@ -148,8 +148,8 @@ div {
.Face .Image { .Face .Image {
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
width: 8rem; /*width: 8rem;
height: 8rem; height: 8rem;*/
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
@ -158,7 +158,7 @@ div {
user-select: none; user-select: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: scroll; /* overflow-y: scroll;*/
padding: 0.5rem; padding: 0.5rem;
height: 100%; height: 100%;
} }
@ -177,4 +177,7 @@ div {
display: grid; display: grid;
gap: 0.25rem; gap: 0.25rem;
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
width: 100%;
height: 100%;
flex-wrap: wrap;
} }

View File

@ -8,8 +8,9 @@ import {
Routes, Routes,
useParams useParams
} from "react-router-dom"; } from "react-router-dom";
import { VirtuosoGrid } from 'react-virtuoso'
import moment from 'moment'; import moment from 'moment';
import equal from "fast-deep-equal"; //import equal from "fast-deep-equal";
import './App.css'; import './App.css';
const base = process.env.PUBLIC_URL; /* /identities -- set in .env */ const base = process.env.PUBLIC_URL; /* /identities -- set in .env */
@ -191,7 +192,7 @@ const Face = ({ face, onFaceClick, title, ...rest }: any) => {
? <div className='UnknownFace'>?</div> ? <div className='UnknownFace'>?</div>
: <img src={`${base}/../faces/${idPath}/${faceId}.jpg`} : <img src={`${base}/../faces/${idPath}/${faceId}.jpg`}
style={{ style={{
objectFit: 'contain', objectFit: 'cover',//'contain',
width: '100%', width: '100%',
height: '100%' height: '100%'
}} />; }} />;
@ -225,67 +226,7 @@ const Cluster = ({
identity, setIdentity, identity, setIdentity,
identities, setIdentities, identities, setIdentities,
setImage, setSelected }: ClusterProps) => { 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 <div key={face.faceId}>
too many faces (${identity.relatedFaces.length - 1000} remaining)
</div>;
} else {
return;
}
}
return (
<div
key={face.faceId}
style={{
display: "flex",
justifyContent: 'center',
alignItems: 'center'}}>
<Face
face={face}
onFaceClick={faceClicked}
title={face.distance}/>
</div>
);
});
}, [identity, setImage, setSelected]);
const lastNameChanged = (e: any) => { const lastNameChanged = (e: any) => {
setIdentity({...identity, lastName: e.currentTarget.value }); setIdentity({...identity, lastName: e.currentTarget.value });
}; };
@ -299,6 +240,37 @@ const Cluster = ({
setIdentity({...identity, displayName: e.currentTarget.value }); 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 () => { const deleteIdentity = async () => {
try { try {
const res = await window.fetch( const res = await window.fetch(
@ -401,9 +373,17 @@ const Cluster = ({
</div> </div>
</div> </div>
<div>Faces: {identity.relatedFaces.length}</div> <div>Faces: {identity.relatedFaces.length}</div>
<div className="Faces"> <VirtuosoGrid
{ relatedFacesJSX } data={identity.relatedFaces}
</div>
listClassName='Faces'
itemContent={(index, face) => (
<Face
face={face}
onFaceClick={faceClicked}
title={face.distance} />
)}
/>
</div> </div>
); );
}; };
@ -650,7 +630,6 @@ const App = () => {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });
const faces = await res.json(); const faces = await res.json();
console.log(faces);
setGuess(faces[0]); setGuess(faces[0]);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -195,8 +195,8 @@ function init() {
}, },
classifiedBy: { classifiedBy: {
type: Sequelize.DataTypes.ENUM( type: Sequelize.DataTypes.ENUM(
'machine', 'machine', /* DBSCAN with VGG-Face */
'human', 'human', /* Human identified */
'not-a-face'), /* implies "human"; identityId=NULL */ 'not-a-face'), /* implies "human"; identityId=NULL */
defaultValue: 'machine', defaultValue: 'machine',
}, },

View File

@ -9,6 +9,8 @@ require("../db/photos").then(function(db) {
photoDB = db; photoDB = db;
}); });
const MIN_DISTANCE_COMMIT = 0.0000001
const router = express.Router(); const router = express.Router();
const upsertIdentity = async(id, { const upsertIdentity = async(id, {
@ -68,13 +70,14 @@ const populateRelatedFaces = async (identity, count) => {
* just return the empty 'unknown face'. * just return the empty 'unknown face'.
* *
* Otherwise, query the DB for 'count' faces */ * Otherwise, query the DB for 'count' faces */
if (count === undefined) { if (count !== undefined) {
identity.relatedFaces = await photoDB.sequelize.query( identity.relatedFaces = await photoDB.sequelize.query(
"SELECT id AS faceId,photoId,faceConfidence " + "SELECT id AS faceId,photoId,faceConfidence " +
"FROM faces " + "FROM faces " +
"WHERE identityId=:identityId " + "WHERE identityId=:identityId " +
"ORDER BY distance ASC" +
limit, { limit, {
replacements: { identityId: identity.identityId }, replacements: identity,
type: photoDB.Sequelize.QueryTypes.SELECT, type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}); });
@ -111,7 +114,7 @@ router.post('/', async (req, res) => {
if (!identity) { if (!identity) {
return; return;
} }
await updateIdentityFaces(identity); updateIdentityFaces(identity);
await populateRelatedFaces(identity, 1); await populateRelatedFaces(identity, 1);
return res.status(200).send(identity); return res.status(200).send(identity);
}); });
@ -189,8 +192,9 @@ router.get("/faces/guess/:faceId", async (req, res) => {
} }
try { try {
/* Look up the requested face... */
const faces = await photoDB.sequelize.query( const faces = await photoDB.sequelize.query(
"SELECT faces.*,faceDescriptors.* " + "SELECT faces.*,faceDescriptors.descriptors " +
"FROM faces,faceDescriptors " + "FROM faces,faceDescriptors " +
"WHERE faces.id=:faceId " + "WHERE faces.id=:faceId " +
"AND faceDescriptors.id=faces.descriptorId", { "AND faceDescriptors.id=faces.descriptorId", {
@ -199,6 +203,7 @@ router.get("/faces/guess/:faceId", async (req, res) => {
raw: true raw: true
}); });
/* Look up all the identities... */
const identities = await photoDB.sequelize.query( const identities = await photoDB.sequelize.query(
"SELECT * FROM identities", { "SELECT * FROM identities", {
type: photoDB.Sequelize.QueryTypes.SELECT, 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;}); identities.forEach(x => { x.identityId = x.id; delete x.id;});
faces.forEach((face) => { faces.forEach((face) => {
const currentIdentityId = face.identityId;
face.faceId = face.id; face.faceId = face.id;
delete face.id; delete face.id;
face.identityId = -1;
face.distance = -1;
face.identity = null; face.identity = null;
identities.forEach((identity) => { identities.forEach((identity) => {
if (!identity.descriptors) { if (!identity.descriptors
|| currentIdentityId === identity.identityId) {
return; return;
} }
@ -223,7 +229,7 @@ router.get("/faces/guess/:faceId", async (req, res) => {
identity.descriptors identity.descriptors
); );
if (face.identityId === -1) { if (face.identity === null) {
face.identityId = identity.identityId; face.identityId = identity.identityId;
face.identity = identity; face.identity = identity;
face.distance = distance; face.distance = distance;
@ -239,7 +245,6 @@ router.get("/faces/guess/:faceId", async (req, res) => {
}); });
/* Delete the VGG-Face descriptors and add relatedFaces[0] */ /* Delete the VGG-Face descriptors and add relatedFaces[0] */
const results = [];
await Promise.map(faces, async (face) => { await Promise.map(faces, async (face) => {
delete face.descriptors; delete face.descriptors;
if (face.identity.descriptors) { if (face.identity.descriptors) {
@ -290,7 +295,10 @@ router.put("/faces/remove/:id", async (req, res) => {
}; };
identity.faces = identity.faces.map(id => +id); 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); return res.status(200).json(identity);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -328,7 +336,9 @@ router.put("/faces/add/:id", async (req, res) => {
}; };
identity.faces = identity.faces.map(id => +id); 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); return res.status(200).json(identity);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -422,7 +432,7 @@ const updateIdentityFaces = async (identity) => {
if (!identity.descriptors) { if (!identity.descriptors) {
const results = await photoDB.sequelize.query( const results = await photoDB.sequelize.query(
"SELECT " + "SELECT " +
"descriptors,facesCount,faceId " + "descriptors,facesCount,faceId,displayName " +
"FROM identities " + "FROM identities " +
"WHERE id=:identityId", { "WHERE id=:identityId", {
replacements: identity, replacements: identity,
@ -513,7 +523,7 @@ const updateIdentityFaces = async (identity) => {
.parseFloat(euclideanDistanceArray(face.descriptors, average)) .parseFloat(euclideanDistanceArray(face.descriptors, average))
.toFixed(4); .toFixed(4);
if (Math.abs(distance - face.distance) > 0.0001) { if (Math.abs(distance - face.distance) > MIN_DISTANCE_COMMIT) {
console.log( console.log(
`Updating face ${face.id} to ${round(distance, 2)} ` + `Updating face ${face.id} to ${round(distance, 2)} ` +
`(${distance - face.distance}) ` + `(${distance - face.distance}) ` +
@ -526,7 +536,7 @@ const updateIdentityFaces = async (identity) => {
); );
} }
}, { }, {
concurrency: 1 concurrency: 5
}); });
let sql = ''; let sql = '';
@ -540,7 +550,7 @@ const updateIdentityFaces = async (identity) => {
/* If the centroid changed, update the identity descriptors to /* If the centroid changed, update the identity descriptors to
* the new average */ * the new average */
if (Math.abs(moved) > 0.0001) { if (Math.abs(moved) > MIN_DISTANCE_COMMIT) {
console.log( console.log(
`Updating identity ${identity.identityId} centroid ` + `Updating identity ${identity.identityId} centroid ` +
`(moved ${Number.parseFloat(moved).toFixed(4)}).`); `(moved ${Number.parseFloat(moved).toFixed(4)}).`);
@ -622,9 +632,6 @@ router.get("/:id?", async (req, res) => {
}); });
await Promise.map(identities, async (identity) => { await Promise.map(identities, async (identity) => {
console.log(`Updating ${identity.identityId}`);
await updateIdentityFaces(identity);
for (let field in identity) { for (let field in identity) {
if (field.match(/.*Name/) && identity[field] === null) { if (field.match(/.*Name/) && identity[field] === null) {
identity[field] = ''; identity[field] = '';