Face selection is working pretty well
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
c4a6b6dad4
commit
f1c1b79672
@ -49,10 +49,9 @@ div {
|
||||
user-select: none;
|
||||
overflow-y: scroll;
|
||||
overflow-x: clip;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 0.25rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(8.5rem, auto));
|
||||
grid-template-columns: repeat(auto-fill, minmax(4.25rem, auto));
|
||||
}
|
||||
|
||||
.Face {
|
||||
@ -73,14 +72,29 @@ div {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.PhotoPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Guess {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
.Image .FaceBox {
|
||||
border: 1px solid red;
|
||||
position: absolute;
|
||||
@ -94,9 +108,6 @@ div {
|
||||
.Image {
|
||||
display: flex;
|
||||
position: relative;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat no-repeat !important;
|
||||
background-position: 50% 50% !important;
|
||||
}
|
||||
|
||||
.PhotoPanel .FaceInfo {
|
||||
@ -112,6 +123,10 @@ div {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.Identities .UnknownFace {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.UnknownFace {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -158,6 +173,11 @@ div {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.Identities .Face .Image {
|
||||
min-width: 4rem;
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
.Face .Image {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
@ -186,6 +206,13 @@ div {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.Viewer .PhotoPanel img {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.Image img {
|
||||
object-fit: cover; /* contain */
|
||||
width: 100%;
|
||||
@ -195,7 +222,7 @@ div {
|
||||
.Cluster .Faces {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
@ -129,12 +129,7 @@ const Photo = ({ photoId, onFaceClick }: any) => {
|
||||
<div className="Image" ref={ref}>
|
||||
<img
|
||||
alt={image.filename}
|
||||
src={`${base}/../${image.path}thumbs/scaled/${image.filename}`.replace(/ /g, '%20')}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}} />
|
||||
src={`${base}/../${image.path}thumbs/scaled/${image.filename}`.replace(/ /g, '%20')}/>
|
||||
{ faces }
|
||||
</div>
|
||||
<div className="ImageInfo">{
|
||||
@ -326,20 +321,6 @@ const Cluster = ({
|
||||
});
|
||||
await res.json();
|
||||
setIdentity({ ...identity, ...values });
|
||||
setIdentities(
|
||||
[...identities]
|
||||
.sort((A: IdentityData, B: IdentityData) => {
|
||||
/* Sort the Unknown (-1) identity to the end */
|
||||
if (A.identityId === -1) {
|
||||
return +1;
|
||||
}
|
||||
if (B.identityId === -1) {
|
||||
return -1;
|
||||
}
|
||||
/* Otherwise sort alphabetically by displayName */
|
||||
return A.displayName.localeCompare(B.displayName);
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -361,20 +342,6 @@ const Cluster = ({
|
||||
});
|
||||
const created = await res.json();
|
||||
setIdentity(created);
|
||||
setIdentities(
|
||||
[created, ...identities]
|
||||
.sort((A: IdentityData, B: IdentityData) => {
|
||||
/* Sort the Unknown (-1) identity to the end */
|
||||
if (A.identityId === -1) {
|
||||
return +1;
|
||||
}
|
||||
if (B.identityId === -1) {
|
||||
return -1;
|
||||
}
|
||||
/* Otherwise sort alphabetically by displayName */
|
||||
return A.displayName.localeCompare(B.displayName);
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -537,6 +504,62 @@ const Button = ({ onClick, children }: any) => {
|
||||
</button>
|
||||
);
|
||||
};
|
||||
/* returns true if update to identities array occurred */
|
||||
const updateIdentityReferences = (
|
||||
identities: IdentityData[],
|
||||
identity: IdentityData) : boolean => {
|
||||
|
||||
if (identity.identityId === -1) {
|
||||
console.warn('Identity Unknown (-1) attempting to be updated');
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetIndex = identities.findIndex(
|
||||
x => x.identityId === identity.identityId);
|
||||
if (targetIndex === -1) {
|
||||
identities.push(identity);
|
||||
return true;
|
||||
}
|
||||
const target = identities[targetIndex];
|
||||
|
||||
/*
|
||||
IdentityData fields we check to make sure they are the same:
|
||||
|
||||
lastName: string,
|
||||
middleName: string,
|
||||
firstName: string,
|
||||
displayName: string,
|
||||
facesCount: number,
|
||||
faceId: number
|
||||
!identityId: number
|
||||
!relatedFaces: FaceData[],
|
||||
!descriptors: number[],
|
||||
*/
|
||||
|
||||
let same = true;
|
||||
|
||||
[ 'lastName', 'firstName', 'middleName',
|
||||
'displayName', 'faceId', 'facesCount', 'faceId' ]
|
||||
.forEach((field: string) => {
|
||||
same = same && (target as any)[field] === (identity as any)[field];
|
||||
});
|
||||
|
||||
if (same) {
|
||||
return false;
|
||||
}
|
||||
|
||||
identities[targetIndex] = {
|
||||
...identity,
|
||||
relatedFaces: target.relatedFaces
|
||||
};
|
||||
|
||||
/* relatedFaces is a list of references to identity */
|
||||
identity.relatedFaces.forEach(face => {
|
||||
face.identity = identity;
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const { identityId, faceId } = useParams();
|
||||
@ -574,32 +597,28 @@ const App = () => {
|
||||
/* If the identity changes, update its entry in the identities list
|
||||
* NOTE: Blocks update to 'Unknown' (-1) fake identity */
|
||||
useEffect(() => {
|
||||
if (!identity || identities.length === 0 || identity.identityId === -1) {
|
||||
if (identity.identityId === -1) {
|
||||
return;
|
||||
}
|
||||
for (let key in identities) {
|
||||
if (identities[key].identityId === identity.identityId) {
|
||||
let same = true;
|
||||
[ 'displayName', 'firstName', 'lastName', 'middleName' ]
|
||||
.forEach((field: string) => {
|
||||
same = same
|
||||
&& (identities[key] as any)[field] === (identity as any)[field];
|
||||
});
|
||||
if (!same) {
|
||||
console.log(`Updating `, identity, identities[key]);
|
||||
identities[key] = {
|
||||
...identity,
|
||||
relatedFaces: identities[key].relatedFaces
|
||||
};
|
||||
/* relatedFaces is a list of references to identity */
|
||||
identity.relatedFaces.forEach(face => {
|
||||
face.identity = identity;
|
||||
});
|
||||
setIdentities([...identities]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updateIdentityReferences(identities, identity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIdentities(
|
||||
[...identities]
|
||||
.sort((A: IdentityData, B: IdentityData) => {
|
||||
/* Sort the Unknown (-1) identity to the end */
|
||||
if (A.identityId === -1) {
|
||||
return +1;
|
||||
}
|
||||
if (B.identityId === -1) {
|
||||
return -1;
|
||||
}
|
||||
/* Otherwise sort alphabetically by displayName */
|
||||
return A.displayName.localeCompare(B.displayName);
|
||||
})
|
||||
);
|
||||
}, [identity, setIdentities, identities]);
|
||||
|
||||
/* If the identity changes, scroll it into view in the Identities list */
|
||||
@ -720,7 +739,7 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const markSelectedIncorrectIdentity = async () => {
|
||||
const removeFaceFromIdentity = async () => {
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
@ -735,9 +754,9 @@ const App = () => {
|
||||
|
||||
removeFacesFromIdentity(results.removed);
|
||||
deselectAll();
|
||||
if (identity.faceId !== results.faceId) {
|
||||
setIdentity({...identity, ...{ faceId: results.faceId }});
|
||||
setIdentities([...identities]);
|
||||
if (results.faceId !== undefined
|
||||
&& identity.faceId !== results.faceId) {
|
||||
setIdentity({...identity, faceId: results.faceId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -778,7 +797,6 @@ const App = () => {
|
||||
target.faceId = results.faceId;
|
||||
}
|
||||
deselectAll();
|
||||
setIdentities([...identities]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -798,7 +816,7 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const markSelectedNotFace = async () => {
|
||||
const updateFasAsNotFace = async () => {
|
||||
try {
|
||||
const res = await window.fetch(
|
||||
`${base}/api/v1/faces`, {
|
||||
@ -871,7 +889,8 @@ const App = () => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (identity.identityId !== identityId) {
|
||||
if (identity.identityId !== identityId
|
||||
|| identity.facesCount === 0) {
|
||||
[...document.querySelectorAll('.Cluster .Faces img')]
|
||||
.forEach((img: any) => {
|
||||
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
@ -901,8 +920,8 @@ const App = () => {
|
||||
<Button onClick={guessIdentity}>Guess</Button>
|
||||
</>}
|
||||
{ selected.length !== 0 && <>
|
||||
<Button onClick={markSelectedIncorrectIdentity}>Remove</Button>
|
||||
<Button onClick={markSelectedNotFace}>Not a face</Button>
|
||||
<Button onClick={removeFaceFromIdentity}>Remove</Button>
|
||||
<Button onClick={updateFasAsNotFace}>Not a face</Button>
|
||||
<Button onClick={changeSelectedIdentity}>Change Identity</Button>
|
||||
<Button onClick={deselectAll}>Deselect All</Button>
|
||||
</>}
|
||||
@ -913,18 +932,17 @@ const App = () => {
|
||||
</Panel>
|
||||
<PanelResizeHandle className="Resizer"/>
|
||||
<Panel>
|
||||
{image === 0 && <div style={{ margin: '1rem' }}>Select image to view</div>}
|
||||
{image !== 0 && <Photo onFaceClick={onFaceClick} photoId={image}/> }
|
||||
{guess !== undefined && guess.identity && <div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'}}>
|
||||
<Face
|
||||
face={guess.identity.relatedFaces[0]}
|
||||
onFaceClick={guessOnFaceClick}
|
||||
title={`${guess.identity.displayName} (${guess.distance})`}/>
|
||||
</div> }
|
||||
<div className="Viewer">
|
||||
{image === 0 && <div style={{ margin: '1rem' }}>Select image to view</div>}
|
||||
{image !== 0 && <Photo onFaceClick={onFaceClick} photoId={image}/> }
|
||||
{guess !== undefined && guess.identity && <div
|
||||
className="Guess">
|
||||
<Face
|
||||
face={guess.identity.relatedFaces[0]}
|
||||
onFaceClick={guessOnFaceClick}
|
||||
title={`${guess.identity.displayName} (${guess.distance})`}/>
|
||||
</div> }
|
||||
</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle className="Resizer" />
|
||||
<Panel defaultSize={8.5} minSize={8.5} className="IdentitiesList">
|
||||
@ -932,7 +950,7 @@ const App = () => {
|
||||
Loading...
|
||||
</div> }
|
||||
{ loaded && <Identities
|
||||
{... { onFaceClick: identitiesOnFaceClick, identities }}/>
|
||||
{...{ onFaceClick: identitiesOnFaceClick, identities }}/>
|
||||
}
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
|
@ -9,7 +9,7 @@ require("../db/photos").then(function(db) {
|
||||
photoDB = db;
|
||||
});
|
||||
|
||||
const MIN_DISTANCE_COMMIT = 0.0000001
|
||||
const MIN_DISTANCE_COMMIT = 0.0001
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@ -258,15 +258,15 @@ router.get("/faces/guess/:faceId", async (req, res) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.put("/faces/remove/:id", async (req, res) => {
|
||||
router.put("/faces/remove/:identityId", async (req, res) => {
|
||||
console.log(`PUT ${req.url}`)
|
||||
if (!req.user.maintainer) {
|
||||
console.warn(`${req.user.name} attempted to modify photos.`);
|
||||
return res.status(401).send({ message: "Unauthorized to modify photos." });
|
||||
}
|
||||
|
||||
const id = parseInt(req.params.id);
|
||||
if (id != req.params.id) {
|
||||
const identityId = parseInt(req.params.identityId);
|
||||
if (identityId != req.params.identityId) {
|
||||
return res.status(400).send({ message: "Invalid identity id." });
|
||||
}
|
||||
|
||||
@ -274,30 +274,35 @@ router.put("/faces/remove/:id", async (req, res) => {
|
||||
return res.status(400).send({ message: "No faces supplied." });
|
||||
}
|
||||
|
||||
/* Convert faces array to numbers and filter any non-numbers */
|
||||
let faceIds = req.body.faces
|
||||
.map(faceId => +faceId)
|
||||
.filter(faceId => !isNaN(faceId));
|
||||
|
||||
try {
|
||||
await photoDB.sequelize.query(
|
||||
"UPDATE faces SET identityId=null " +
|
||||
"WHERE id IN (:faceIds)", {
|
||||
replacements: {
|
||||
identityId: id,
|
||||
faceIds: req.body.faces
|
||||
identityId,
|
||||
faceIds
|
||||
}
|
||||
});
|
||||
const identity = {
|
||||
identityId: id,
|
||||
faces: req.body.faces
|
||||
identityId,
|
||||
removed: faceIds
|
||||
};
|
||||
identity.removed = identity.faces.map(id => +id);
|
||||
|
||||
/* If the primary faceId was removed, update the identity's faceId
|
||||
* to a new faceId */
|
||||
const faceIds = await photoDB.sequelize.query(`
|
||||
SELECT faceId FROM identities WHERE id=:identityId`, {
|
||||
const identityFaceIds = await photoDB.sequelize.query(`
|
||||
SELECT faceId AS identityFaceId FROM identities WHERE id=:identityId`, {
|
||||
replacements: identity,
|
||||
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||
raw: true
|
||||
});
|
||||
|
||||
if (identity.removed.indexOf(faceIds[0]) !== -1) {
|
||||
if (identity.removed.indexOf(identityFaceIds[0]) !== -1) {
|
||||
const newFaceId = await photoDB.sequelize.query(`
|
||||
SELECT faceId FROM faces WHERE identityId=:identityId
|
||||
ORDER BY distance ASC
|
||||
@ -343,7 +348,7 @@ router.put("/faces/add/:id", async (req, res) => {
|
||||
}
|
||||
|
||||
if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) {
|
||||
return res.status(400).send("No faces supplied.");
|
||||
return res.status(400).send({ message: "No faces supplied." });
|
||||
}
|
||||
|
||||
/* Convert faces array to numbers and filter any non-numbers */
|
||||
@ -442,8 +447,13 @@ router.put("/faces/add/:id", async (req, res) => {
|
||||
|
||||
/* Do not block on this call finishing -- update can occur
|
||||
* in the background */
|
||||
Promise.map([identity, ...tuples], identity => {
|
||||
updateIdentityFaces(identity);
|
||||
Promise.map([identity, ...tuples], (x, i) => {
|
||||
try {
|
||||
updateIdentityFaces(x);
|
||||
} catch (error) {
|
||||
console.log(i, x);
|
||||
throw error;
|
||||
}
|
||||
}, {
|
||||
concurrency: 1
|
||||
});
|
||||
@ -649,32 +659,37 @@ const updateIdentityFaces = async (identity) => {
|
||||
const t = await photoDB.sequelize.transaction();
|
||||
try {
|
||||
/* If the average position has not changed, then face distances should
|
||||
* not change either! */
|
||||
await Promise.map(faces, async (face) => {
|
||||
/* All the buffer are already arrays, so use the short-cut version */
|
||||
const distance = Number
|
||||
.parseFloat(face.updatedDistance)
|
||||
.toFixed(4);
|
||||
|
||||
if (Math.abs(face.updatedDistance - face.distance)
|
||||
> MIN_DISTANCE_COMMIT) {
|
||||
console.log(
|
||||
`Updating face ${face.id} to ${round(distance, 2)} ` +
|
||||
`(${distance - face.distance}) ` +
|
||||
`from identity ${identity.identityId} (${identity.displayName})`);
|
||||
face.distance = face.updatedDistance;
|
||||
delete face.updatedDistance;
|
||||
await photoDB.sequelize.query(
|
||||
'UPDATE faces SET distance=:distance WHERE id=:id', {
|
||||
replacements: face,
|
||||
transaction: t
|
||||
}
|
||||
);
|
||||
}
|
||||
}, {
|
||||
concurrency: 5
|
||||
});
|
||||
|
||||
* not change either!
|
||||
*
|
||||
* Do not update all the faces unless the centroid has moved a fair
|
||||
* amount */
|
||||
if (Math.abs(moved) > MIN_DISTANCE_COMMIT) {
|
||||
await Promise.map(faces, async (face) => {
|
||||
/* All the buffer are already arrays, so use the short-cut version */
|
||||
const distance = Number
|
||||
.parseFloat(face.updatedDistance)
|
||||
.toFixed(4);
|
||||
|
||||
if (Math.abs(face.updatedDistance - face.distance)
|
||||
> MIN_DISTANCE_COMMIT) {
|
||||
console.log(
|
||||
`Updating face ${face.id} to ${round(distance, 2)} ` +
|
||||
`(${distance - face.distance}) ` +
|
||||
`from identity ${identity.identityId} (${identity.displayName})`);
|
||||
face.distance = face.updatedDistance;
|
||||
delete face.updatedDistance;
|
||||
await photoDB.sequelize.query(
|
||||
'UPDATE faces SET distance=:distance WHERE id=:id', {
|
||||
replacements: face,
|
||||
transaction: t
|
||||
}
|
||||
);
|
||||
}
|
||||
}, {
|
||||
concurrency: 1
|
||||
});
|
||||
}
|
||||
|
||||
let sql = '';
|
||||
/* If there is a new closestId, then set the faceId field */
|
||||
if (closestId !== -1 && closestId !== identity.faceId) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user