Added infinite scrollers
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
f8b33a65b8
commit
d45600f6a7
13
client/package-lock.json
generated
13
client/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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] = '';
|
||||
|
Loading…
x
Reference in New Issue
Block a user