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-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",
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
|
@ -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] = '';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user