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-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",

View File

@ -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"
},

View File

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

View File

@ -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) => {
? <div className='UnknownFace'>?</div>
: <img src={`${base}/../faces/${idPath}/${faceId}.jpg`}
style={{
objectFit: 'contain',
objectFit: 'cover',//'contain',
width: '100%',
height: '100%'
}} />;
@ -225,66 +226,6 @@ 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 <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) => {
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 = ({
</div>
</div>
<div>Faces: {identity.relatedFaces.length}</div>
<div className="Faces">
{ relatedFacesJSX }
</div>
<VirtuosoGrid
data={identity.relatedFaces}
listClassName='Faces'
itemContent={(index, face) => (
<Face
face={face}
onFaceClick={faceClicked}
title={face.distance} />
)}
/>
</div>
);
};
@ -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);

View File

@ -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',
},

View File

@ -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] = '';