Add/Remove while updating faces is *mostly* working -- it doesn't update the faceId in Identities though.
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
b86c12fc92
commit
c4a6b6dad4
@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useApi } from './useApi';
|
|
||||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router,
|
BrowserRouter as Router,
|
||||||
@ -224,20 +223,42 @@ const Cluster = ({
|
|||||||
setSelected,
|
setSelected,
|
||||||
setImage,
|
setImage,
|
||||||
}: ClusterProps) => {
|
}: ClusterProps) => {
|
||||||
|
const [lastName, setLastName] = useState<string>(identity.lastName);
|
||||||
|
const [firstName, setFirstName] = useState<string>(identity.firstName);
|
||||||
|
const [middleName, setMiddleName] = useState<string>(identity.middleName);
|
||||||
|
const [displayName, setDisplayName] = useState<string>(identity.displayName);
|
||||||
|
const [updated, setUpdated] = useState<boolean>(false);
|
||||||
|
|
||||||
const lastNameChanged = (e: any) => {
|
const lastNameChanged = (e: any) => {
|
||||||
setIdentity({...identity, lastName: e.currentTarget.value });
|
setLastName(e.currentTarget.value);
|
||||||
};
|
};
|
||||||
const firstNameChanged = (e: any) => {
|
const firstNameChanged = (e: any) => {
|
||||||
setIdentity({...identity, firstName: e.currentTarget.value });
|
setFirstName(e.currentTarget.value);
|
||||||
};
|
};
|
||||||
const middleNameChanged = (e: any) => {
|
const middleNameChanged = (e: any) => {
|
||||||
setIdentity({...identity, middleName: e.currentTarget.value });
|
setMiddleName(e.currentTarget.value);
|
||||||
};
|
};
|
||||||
const displayNameChanged = (e: any) => {
|
const displayNameChanged = (e: any) => {
|
||||||
setIdentity({...identity, displayName: e.currentTarget.value });
|
setDisplayName(e.currentTarget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* If the user edits the identity, set the "updated" flag */
|
||||||
|
useEffect(() => {
|
||||||
|
setUpdated(lastName !== identity.lastName
|
||||||
|
|| firstName !== identity.firstName
|
||||||
|
|| middleName !== identity.middleName
|
||||||
|
|| displayName !== identity.displayName
|
||||||
|
);
|
||||||
|
}, [setUpdated, identity, lastName, firstName, middleName, displayName]);
|
||||||
|
|
||||||
|
/* If the identity changes, update all the fields */
|
||||||
|
useEffect(() => {
|
||||||
|
setLastName(identity.lastName);
|
||||||
|
setFirstName(identity.firstName);
|
||||||
|
setMiddleName(identity.middleName);
|
||||||
|
setDisplayName(identity.displayName);
|
||||||
|
}, [identity]);
|
||||||
|
|
||||||
const faceClicked = useCallback((e: any, face: FaceData) => {
|
const faceClicked = useCallback((e: any, face: FaceData) => {
|
||||||
const el = e.currentTarget;
|
const el = e.currentTarget;
|
||||||
|
|
||||||
@ -262,6 +283,9 @@ const Cluster = ({
|
|||||||
}, [setSelected, setImage]);
|
}, [setSelected, setImage]);
|
||||||
|
|
||||||
const deleteIdentity = async () => {
|
const deleteIdentity = async () => {
|
||||||
|
if (!identity || identity.identityId === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch(
|
const res = await window.fetch(
|
||||||
`${base}/api/v1/identities/${identity.identityId}`, {
|
`${base}/api/v1/identities/${identity.identityId}`, {
|
||||||
@ -283,23 +307,39 @@ const Cluster = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateIdentity = async () => {
|
const updateIdentity = async () => {
|
||||||
|
if (!identity || identity.identityId === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const validFields = [
|
const values = {
|
||||||
'id', 'displayName', 'firstName', 'lastName', 'middleName'];
|
lastName,
|
||||||
const filtered: any = Object.assign({}, identity);
|
firstName,
|
||||||
for (let key in filtered) {
|
middleName,
|
||||||
if (validFields.indexOf(key) === -1) {
|
displayName
|
||||||
delete filtered[key]
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
const res = await window.fetch(
|
const res = await window.fetch(
|
||||||
`${base}/api/v1/identities/${identity.identityId}`, {
|
`${base}/api/v1/identities/${identity.identityId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(filtered)
|
body: JSON.stringify(values)
|
||||||
});
|
});
|
||||||
await res.json();
|
await res.json();
|
||||||
setIdentity({ ...identity });
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -307,23 +347,34 @@ const Cluster = ({
|
|||||||
|
|
||||||
const createIdentity = async () => {
|
const createIdentity = async () => {
|
||||||
try {
|
try {
|
||||||
const validFields = [
|
const values = {
|
||||||
'id', 'displayName', 'firstName', 'lastName', 'middleName'];
|
lastName,
|
||||||
const filtered: any = Object.assign({}, identity);
|
firstName,
|
||||||
for (let key in filtered) {
|
middleName,
|
||||||
if (validFields.indexOf(key) === -1) {
|
displayName
|
||||||
delete filtered[key]
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
const res = await window.fetch(
|
const res = await window.fetch(
|
||||||
`${base}/api/v1/identities/`, {
|
`${base}/api/v1/identities/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(filtered)
|
body: JSON.stringify(values)
|
||||||
});
|
});
|
||||||
const created = await res.json();
|
const created = await res.json();
|
||||||
setIdentity(created);
|
setIdentity(created);
|
||||||
setIdentities([identity, ...identities]);
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -342,31 +393,32 @@ const Cluster = ({
|
|||||||
<form className="IdentityForm">
|
<form className="IdentityForm">
|
||||||
<div>Last name:</div>
|
<div>Last name:</div>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value={identity.lastName}
|
value={lastName}
|
||||||
onChange={lastNameChanged}/>
|
onChange={lastNameChanged}/>
|
||||||
<div>First name:</div>
|
<div>First name:</div>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value={identity.firstName}
|
value={firstName}
|
||||||
onChange={firstNameChanged} />
|
onChange={firstNameChanged} />
|
||||||
<div>Middle name:</div><input type="text"
|
<div>Middle name:</div><input type="text"
|
||||||
value={identity.middleName}
|
value={middleName}
|
||||||
onChange={middleNameChanged} />
|
onChange={middleNameChanged} />
|
||||||
<div>Display name:</div>
|
<div>Display name:</div>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value={identity.displayName}
|
value={displayName}
|
||||||
onChange={displayNameChanged} />
|
onChange={displayNameChanged} />
|
||||||
</form>
|
</form>
|
||||||
<Face
|
<Face
|
||||||
face={identity.relatedFaces.length
|
face={{
|
||||||
? identity.relatedFaces[0]
|
identityId: identity.identityId,
|
||||||
: UnknownFace}
|
faceId: identity.faceId
|
||||||
|
}}
|
||||||
onFaceClick={() => {}}
|
onFaceClick={() => {}}
|
||||||
title={`${identity.displayName} (${identity.facesCount})`} />
|
title={`${displayName} (${identity.facesCount})`} />
|
||||||
</div>
|
</div>
|
||||||
<div className="Actions">
|
<div className="Actions">
|
||||||
<Button onClick={createIdentity}>Create</Button>
|
<Button onClick={createIdentity}>Create</Button>
|
||||||
{ identity.identityId !== -1 && <>
|
{ identity.identityId !== -1 && <>
|
||||||
<Button onClick={updateIdentity}>Update</Button>
|
{ updated && <Button onClick={updateIdentity}>Update</Button> }
|
||||||
<Button onClick={deleteIdentity}>Delete</Button>
|
<Button onClick={deleteIdentity}>Delete</Button>
|
||||||
</> }
|
</> }
|
||||||
</div>
|
</div>
|
||||||
@ -447,27 +499,33 @@ interface IdentitiesProps {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Identities = ({ identities, onFaceClick } : IdentitiesProps) => {
|
const Identities = ({ identities, onFaceClick } : IdentitiesProps) => {
|
||||||
const identitiesJSX = identities.map((identity) => {
|
const [jsx, setJsx] = useState<any[]>([]);
|
||||||
const face = identity.relatedFaces[0];
|
useEffect(() => {
|
||||||
return (
|
setJsx(identities.map((identity) => {
|
||||||
<div
|
const face = {
|
||||||
key={face.faceId}
|
faceId: identity.faceId,
|
||||||
style={{
|
identityId: identity.identityId
|
||||||
display: "flex",
|
};
|
||||||
justifyContent: 'center',
|
return (
|
||||||
alignItems: 'center'
|
<div
|
||||||
}}>
|
key={identity.identityId}
|
||||||
<Face
|
style={{
|
||||||
face={face}
|
display: "flex",
|
||||||
onFaceClick={onFaceClick}
|
justifyContent: 'center',
|
||||||
title={`${identity.displayName} (${identity.facesCount})`}/>
|
alignItems: 'center'
|
||||||
</div>
|
}}>
|
||||||
);
|
<Face
|
||||||
});
|
face={face}
|
||||||
|
onFaceClick={onFaceClick}
|
||||||
|
title={`${identity.displayName} (${identity.facesCount})`}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}, [identities, onFaceClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='Identities'>
|
<div className='Identities'>
|
||||||
{ identitiesJSX }
|
{ jsx }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -481,15 +539,13 @@ const Button = ({ onClick, children }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [identities, setIdentities] = useState<IdentityData[]>([]);
|
|
||||||
const { identityId, faceId } = useParams();
|
const { identityId, faceId } = useParams();
|
||||||
|
const [identities, setIdentities] = useState<IdentityData[]>([]);
|
||||||
const [selectedIdentities, setSelectedIdentities] = useState<number[]>([]);
|
const [selectedIdentities, setSelectedIdentities] = useState<number[]>([]);
|
||||||
const [identity, setIdentity] = useState<IdentityData>(EmptyIdentity);
|
const [identity, setIdentity] = useState<IdentityData>(EmptyIdentity);
|
||||||
const [image, setImage] = useState<number>(0);
|
const [image, setImage] = useState<number>(0);
|
||||||
const [guess, setGuess] = useState<FaceData|undefined>(undefined);
|
const [guess, setGuess] = useState<FaceData|undefined>(undefined);
|
||||||
const { loading, data } = useApi( /* TODO: Switch away from using useApi */
|
const [loaded, setLoaded] = useState<boolean>(false);
|
||||||
`${base}/api/v1/identities`
|
|
||||||
);
|
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
|
|
||||||
/* If 'selected' changes, clear any selected face which is not in the
|
/* If 'selected' changes, clear any selected face which is not in the
|
||||||
@ -515,9 +571,10 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* If the identity changes, update its entry in the identities list */
|
/* If the identity changes, update its entry in the identities list
|
||||||
|
* NOTE: Blocks update to 'Unknown' (-1) fake identity */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!identity || identities.length === 0) {
|
if (!identity || identities.length === 0 || identity.identityId === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let key in identities) {
|
for (let key in identities) {
|
||||||
@ -525,9 +582,11 @@ const App = () => {
|
|||||||
let same = true;
|
let same = true;
|
||||||
[ 'displayName', 'firstName', 'lastName', 'middleName' ]
|
[ 'displayName', 'firstName', 'lastName', 'middleName' ]
|
||||||
.forEach((field: string) => {
|
.forEach((field: string) => {
|
||||||
same = same && (identities[key] as any)[field] === (identity as any)[field];
|
same = same
|
||||||
|
&& (identities[key] as any)[field] === (identity as any)[field];
|
||||||
});
|
});
|
||||||
if (!same) {
|
if (!same) {
|
||||||
|
console.log(`Updating `, identity, identities[key]);
|
||||||
identities[key] = {
|
identities[key] = {
|
||||||
...identity,
|
...identity,
|
||||||
relatedFaces: identities[key].relatedFaces
|
relatedFaces: identities[key].relatedFaces
|
||||||
@ -543,6 +602,22 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}, [identity, setIdentities, identities]);
|
}, [identity, setIdentities, identities]);
|
||||||
|
|
||||||
|
/* If the identity changes, scroll it into view in the Identities list */
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedIdentities.length !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = document.querySelector(
|
||||||
|
`.Identities [data-identity-id="${identity.identityId}"]`);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "nearest"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [identity, selectedIdentities]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (identityId !== undefined && !isNaN(+identityId)) {
|
if (identityId !== undefined && !isNaN(+identityId)) {
|
||||||
loadIdentity(+identityId);
|
loadIdentity(+identityId);
|
||||||
@ -555,20 +630,38 @@ const App = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && data.length) {
|
if (identities.length !== 0 || loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadIdentities = async () => {
|
||||||
|
const res = await window.fetch(`${base}/api/v1/identities`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
data.forEach((identity: IdentityData) => {
|
data.forEach((identity: IdentityData) => {
|
||||||
identity.relatedFaces.forEach(face => {
|
identity.relatedFaces.forEach(face => {
|
||||||
face.identity = identity;
|
face.identity = identity;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
data.sort((A: IdentityData, B: IdentityData) => {
|
data.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);
|
return A.displayName.localeCompare(B.displayName);
|
||||||
});
|
});
|
||||||
|
setLoaded(true);
|
||||||
setIdentities(data as IdentityData[]);
|
setIdentities(data as IdentityData[]);
|
||||||
}
|
}
|
||||||
}, [data]);
|
loadIdentities();
|
||||||
|
}, [identities, setIdentities, setLoaded, loaded]);
|
||||||
|
|
||||||
const removeFacesFromIdentities = (faceIds: number[]) => {
|
const removeFacesFromIdentity = (faceIds: number[]) => {
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -578,6 +671,7 @@ const App = () => {
|
|||||||
identity.relatedFaces = identity.relatedFaces.filter(
|
identity.relatedFaces = identity.relatedFaces.filter(
|
||||||
(face: FaceData) => faceIds.indexOf(face.faceId) === -1);
|
(face: FaceData) => faceIds.indexOf(face.faceId) === -1);
|
||||||
if (pre !== identity.relatedFaces.length) {
|
if (pre !== identity.relatedFaces.length) {
|
||||||
|
identity.facesCount = identity.relatedFaces.length;
|
||||||
setIdentity({ ...identity })
|
setIdentity({ ...identity })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -587,7 +681,7 @@ const App = () => {
|
|||||||
window.alert('You need to select an identity first (CTRL+CLICK)');
|
window.alert('You need to select an identity first (CTRL+CLICK)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!identity) {
|
if (!identity || identity.identityId === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -598,18 +692,26 @@ const App = () => {
|
|||||||
body: JSON.stringify({ faces: identity.relatedFaces
|
body: JSON.stringify({ faces: identity.relatedFaces
|
||||||
.map(face => face.faceId) })
|
.map(face => face.faceId) })
|
||||||
});
|
});
|
||||||
await res.json();
|
const result = await res.json();
|
||||||
res = await window.fetch(
|
res = await window.fetch(
|
||||||
`${base}/api/v1/identities/${identity.identityId}`, {
|
`${base}/api/v1/identities/${identity.identityId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
await res.json();
|
await res.json();
|
||||||
const index = identities
|
/* Delete the identity from the list of identities */
|
||||||
|
const deleted = identities
|
||||||
.findIndex((item: IdentityData) =>
|
.findIndex((item: IdentityData) =>
|
||||||
item.identityId === identity.identityId);
|
item.identityId === identity.identityId);
|
||||||
if (index !== -1) {
|
if (deleted !== -1) {
|
||||||
identities.splice(index, 1);
|
identities.splice(deleted, 1);
|
||||||
|
}
|
||||||
|
/* Update the faces count on the target identity */
|
||||||
|
const target = identities
|
||||||
|
.find((item: IdentityData) =>
|
||||||
|
item.identityId === selectedIdentities[0]);
|
||||||
|
if (target) {
|
||||||
|
target.facesCount += result.added.length;
|
||||||
}
|
}
|
||||||
setIdentity(EmptyIdentity);
|
setIdentity(EmptyIdentity);
|
||||||
setIdentities([...identities]);
|
setIdentities([...identities]);
|
||||||
@ -629,10 +731,14 @@ const App = () => {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ faces: selected })
|
body: JSON.stringify({ faces: selected })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const results = await res.json();
|
||||||
|
|
||||||
removeFacesFromIdentities(data.faces);
|
removeFacesFromIdentity(results.removed);
|
||||||
deselectAll();
|
deselectAll();
|
||||||
|
if (identity.faceId !== results.faceId) {
|
||||||
|
setIdentity({...identity, ...{ faceId: results.faceId }});
|
||||||
|
setIdentities([...identities]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -650,10 +756,29 @@ const App = () => {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ faces: selected })
|
body: JSON.stringify({ faces: selected })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const results = await res.json();
|
||||||
|
|
||||||
removeFacesFromIdentities(data.faces);
|
removeFacesFromIdentity(results.added);
|
||||||
|
/* If the identity faceId was removed from the identity, select
|
||||||
|
* the next relatedFace */
|
||||||
|
if (results.added.indexOf(identity.faceId) !== -1) {
|
||||||
|
if (identity.relatedFaces.length === 0) {
|
||||||
|
identity.faceId = -1;
|
||||||
|
} else {
|
||||||
|
identity.faceId = identity.relatedFaces[0].faceId;
|
||||||
|
}
|
||||||
|
setIdentity({...identity});
|
||||||
|
}
|
||||||
|
/* Update the faces count on the target identity */
|
||||||
|
const target = identities
|
||||||
|
.find((item: IdentityData) =>
|
||||||
|
item.identityId === selectedIdentities[0]);
|
||||||
|
if (target) {
|
||||||
|
target.facesCount += results.added.length;
|
||||||
|
target.faceId = results.faceId;
|
||||||
|
}
|
||||||
deselectAll();
|
deselectAll();
|
||||||
|
setIdentities([...identities]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -685,7 +810,7 @@ const App = () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
await res.json();
|
await res.json();
|
||||||
removeFacesFromIdentities(selected);
|
removeFacesFromIdentity(selected);
|
||||||
deselectAll();
|
deselectAll();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -707,7 +832,11 @@ const App = () => {
|
|||||||
...document.querySelectorAll(`.Identities [data-identity-id="${identityId}"]`),
|
...document.querySelectorAll(`.Identities [data-identity-id="${identityId}"]`),
|
||||||
...document.querySelectorAll(`.Cluster [data-face-id="${faceId}"]`)];
|
...document.querySelectorAll(`.Cluster [data-face-id="${faceId}"]`)];
|
||||||
faces.forEach((el: any) => {
|
faces.forEach((el: any) => {
|
||||||
el.scrollIntoView();
|
el.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "nearest"
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -742,12 +871,14 @@ const App = () => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
[...document.querySelectorAll('.Cluster .Faces img')]
|
if (identity.identityId !== identityId) {
|
||||||
.forEach((img: any) => {
|
[...document.querySelectorAll('.Cluster .Faces img')]
|
||||||
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
.forEach((img: any) => {
|
||||||
});
|
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
|
});
|
||||||
loadIdentity(identityId);
|
|
||||||
|
loadIdentity(identityId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -756,19 +887,15 @@ const App = () => {
|
|||||||
<PanelGroup className="Explorer"
|
<PanelGroup className="Explorer"
|
||||||
autoSaveId="persistence" direction="horizontal">
|
autoSaveId="persistence" direction="horizontal">
|
||||||
<Panel defaultSize={50} className="ClusterEditor">
|
<Panel defaultSize={50} className="ClusterEditor">
|
||||||
{loading && <div style={{ margin: '1rem' }}>Loading...</div>}
|
<Cluster {...{
|
||||||
<Cluster {...{
|
identity,
|
||||||
identity,
|
setIdentity,
|
||||||
setIdentity,
|
identities,
|
||||||
identities,
|
setIdentities,
|
||||||
setIdentities,
|
setImage,
|
||||||
setImage,
|
selected,
|
||||||
selected,
|
setSelected,
|
||||||
setSelected,
|
}} />
|
||||||
}} />
|
|
||||||
{!loading && identity === undefined && <div className="Cluster">
|
|
||||||
Select identity to edit
|
|
||||||
</div>}
|
|
||||||
<div className="Actions">
|
<div className="Actions">
|
||||||
{ selected.length === 1 && <>
|
{ selected.length === 1 && <>
|
||||||
<Button onClick={guessIdentity}>Guess</Button>
|
<Button onClick={guessIdentity}>Guess</Button>
|
||||||
@ -801,7 +928,10 @@ const App = () => {
|
|||||||
</Panel>
|
</Panel>
|
||||||
<PanelResizeHandle className="Resizer" />
|
<PanelResizeHandle className="Resizer" />
|
||||||
<Panel defaultSize={8.5} minSize={8.5} className="IdentitiesList">
|
<Panel defaultSize={8.5} minSize={8.5} className="IdentitiesList">
|
||||||
{ !loading && <Identities
|
{ !loaded && <div style={{ margin: '1rem' }}>
|
||||||
|
Loading...
|
||||||
|
</div> }
|
||||||
|
{ loaded && <Identities
|
||||||
{... { onFaceClick: identitiesOnFaceClick, identities }}/>
|
{... { onFaceClick: identitiesOnFaceClick, identities }}/>
|
||||||
}
|
}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
@ -28,22 +28,27 @@ const upsertIdentity = async(id, {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identity = {
|
/* Create identity structure based on UnknownIdentity to set
|
||||||
displayName,
|
* default values for new identities */
|
||||||
firstName,
|
const identity = {
|
||||||
lastName,
|
...UnknownIdentity,
|
||||||
middleName,
|
...{
|
||||||
identityId: id
|
displayName,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
middleName,
|
||||||
|
identityId: id
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id === -1 || !id) {
|
if (id === -1 || !id) {
|
||||||
const [results, { lastId }] = await photoDB.sequelize.query(
|
const [, { lastID }] = await photoDB.sequelize.query(
|
||||||
`INSERT INTO identities ` +
|
`INSERT INTO identities ` +
|
||||||
'(displayName,firstName,lastName,middleName)' +
|
'(displayName,firstName,lastName,middleName)' +
|
||||||
'VALUES(:displayName,:firstName,:lastName,:middleName)', {
|
'VALUES(:displayName,:firstName,:lastName,:middleName)', {
|
||||||
replacements: identity
|
replacements: identity
|
||||||
});
|
});
|
||||||
identity.identityId = lastId;
|
identity.identityId = lastID;
|
||||||
console.log('Created identity: ', identity)
|
console.log('Created identity: ', identity)
|
||||||
} else {
|
} else {
|
||||||
await photoDB.sequelize.query(
|
await photoDB.sequelize.query(
|
||||||
@ -282,8 +287,39 @@ router.put("/faces/remove/:id", async (req, res) => {
|
|||||||
identityId: id,
|
identityId: id,
|
||||||
faces: req.body.faces
|
faces: req.body.faces
|
||||||
};
|
};
|
||||||
identity.faces = identity.faces.map(id => +id);
|
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`, {
|
||||||
|
replacements: identity,
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (identity.removed.indexOf(faceIds[0]) !== -1) {
|
||||||
|
const newFaceId = await photoDB.sequelize.query(`
|
||||||
|
SELECT faceId FROM faces WHERE identityId=:identityId
|
||||||
|
ORDER BY distance ASC
|
||||||
|
LIMIT 1`, {
|
||||||
|
replacements: identity,
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (newFaceId.length === 0) {
|
||||||
|
identity.faceId = -1;
|
||||||
|
} else {
|
||||||
|
identity.faceId = newFaceId[0];
|
||||||
|
}
|
||||||
|
await photoDB.sequelize.query(`
|
||||||
|
UPDATE identities
|
||||||
|
SET faceId=${identity.faceId}
|
||||||
|
WHERE id=${identity.identityId}
|
||||||
|
`);
|
||||||
|
console.log(
|
||||||
|
`New faceId for ${identity.identityId} set to ${identity.faceId}.`);
|
||||||
|
}
|
||||||
/* Do not block on this call finishing -- update can occur
|
/* Do not block on this call finishing -- update can occur
|
||||||
* in the background */
|
* in the background */
|
||||||
updateIdentityFaces(identity);
|
updateIdentityFaces(identity);
|
||||||
@ -301,8 +337,8 @@ router.put("/faces/add/:id", async (req, res) => {
|
|||||||
return res.status(401).send("Unauthorized to modify photos.");
|
return res.status(401).send("Unauthorized to modify photos.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = parseInt(req.params.id);
|
const identityId = parseInt(req.params.id);
|
||||||
if (id != req.params.id) {
|
if (identityId != req.params.id) {
|
||||||
return res.status(400).send("Invalid identity id.");
|
return res.status(400).send("Invalid identity id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,24 +346,107 @@ router.put("/faces/add/:id", async (req, res) => {
|
|||||||
return res.status(400).send("No faces supplied.");
|
return res.status(400).send("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));
|
||||||
|
|
||||||
|
/* See which identities currently have these faces (if any) */
|
||||||
|
let tuples = await photoDB.sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
faces.identityId,
|
||||||
|
faces.id AS faceId
|
||||||
|
FROM
|
||||||
|
faces
|
||||||
|
WHERE faces.id IN (:faceIds)`, {
|
||||||
|
replacements: {
|
||||||
|
identityId,
|
||||||
|
faceIds
|
||||||
|
},
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
/* Filter out any faces which are already owned by this identity */
|
||||||
|
tuples = tuples
|
||||||
|
.filter(tuple => tuple.identityId !== identityId);
|
||||||
|
if (tuples.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: 'No faceIds provided not owned by identity.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Obtain faceId from all referenced identities (src and dsts) */
|
||||||
|
const identityIds = [
|
||||||
|
identityId,
|
||||||
|
...tuples
|
||||||
|
.filter(tuple =>
|
||||||
|
tuple.identityId !== null
|
||||||
|
&& tuple.identityId != -1)
|
||||||
|
.map(tuple => tuple.identityId)
|
||||||
|
];
|
||||||
|
|
||||||
|
let identities = await photoDB.sequelize.query(`
|
||||||
|
SELECT id AS identityId, faceId AS identityFaceId
|
||||||
|
FROM identities
|
||||||
|
WHERE id IN (:identityIds)`, {
|
||||||
|
replacements: {
|
||||||
|
identityIds
|
||||||
|
},
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Find the src identity */
|
||||||
|
const identity = identities.find(tuple => tuple.identityId === identityId);
|
||||||
|
|
||||||
|
/* Merge the identities and the tuples, first filtering out the src
|
||||||
|
* identity */
|
||||||
|
tuples = tuples
|
||||||
|
.filter(tuple => tuple.identityId !== identityId)
|
||||||
|
.map(tuple => {
|
||||||
|
return {
|
||||||
|
faceId: tuple.faceId,
|
||||||
|
identityId: tuple.identityId,
|
||||||
|
identityFaceId: (tuple.identityId === null
|
||||||
|
|| tuple.identityId === -1)
|
||||||
|
? -1
|
||||||
|
: identities
|
||||||
|
.find(identity => tuple.identityId === identity.identityId)
|
||||||
|
.identityFaceId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({ dst: identity, src: tuples });
|
||||||
|
console.log(`Need new faceId: `,
|
||||||
|
tuples.filter(tuple => tuple.faceId === tuple.identityFaceId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await photoDB.sequelize.query(
|
await photoDB.sequelize.query(
|
||||||
"UPDATE faces SET identityId=:identityId,classifiedBy='human' " +
|
"UPDATE faces SET identityId=:identityId,classifiedBy='human' " +
|
||||||
"WHERE id IN (:faceIds)", {
|
"WHERE id IN (:faceIds)", {
|
||||||
replacements: {
|
replacements: {
|
||||||
identityId: id,
|
identityId,
|
||||||
faceIds: req.body.faces
|
faceIds
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const identity = {
|
|
||||||
identityId: id,
|
identity.added = faceIds;
|
||||||
faces: req.body.faces
|
identity.faceId = identity.identityFaceId;
|
||||||
};
|
delete identity.identityFaceId;
|
||||||
identity.faces = identity.faces.map(id => +id);
|
|
||||||
|
if (identity.faceId === -1 || identity.faceId === null) {
|
||||||
|
identity.faceId = faceIds[0];
|
||||||
|
}
|
||||||
|
|
||||||
/* Do not block on this call finishing -- update can occur
|
/* Do not block on this call finishing -- update can occur
|
||||||
* in the background */
|
* in the background */
|
||||||
updateIdentityFaces(identity);
|
Promise.map([identity, ...tuples], identity => {
|
||||||
|
updateIdentityFaces(identity);
|
||||||
|
}, {
|
||||||
|
concurrency: 1
|
||||||
|
});
|
||||||
return res.status(200).json(identity);
|
return res.status(200).json(identity);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -335,7 +454,6 @@ router.put("/faces/add/:id", async (req, res) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function bufferToFloat32Array(buffer) {
|
function bufferToFloat32Array(buffer) {
|
||||||
return new Float64Array(buffer.buffer,
|
return new Float64Array(buffer.buffer,
|
||||||
buffer.byteOffset,
|
buffer.byteOffset,
|
||||||
@ -362,39 +480,63 @@ function euclideanDistance(a, b) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UnknownFace = {
|
||||||
|
faceId: -1,
|
||||||
|
identityId: -1,
|
||||||
|
photoId: -1,
|
||||||
|
distance: 0,
|
||||||
|
faceConfidence: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnknownIdentity = {
|
||||||
|
identityId: -1,
|
||||||
|
lastName: '',
|
||||||
|
firstName: '',
|
||||||
|
middleName: '',
|
||||||
|
displayName: 'Unknown',
|
||||||
|
descriptors: new Float32Array(0),
|
||||||
|
relatedFaces: [ UnknownFace ],
|
||||||
|
facesCount: 0,
|
||||||
|
faceId: -1
|
||||||
|
};
|
||||||
|
|
||||||
const getUnknownIdentity = async (faceCount) => {
|
const getUnknownIdentity = async (faceCount) => {
|
||||||
const unknownIdentity = {
|
const unknownIdentity = { ...UnknownIdentity };
|
||||||
identityId: -1,
|
|
||||||
lastName: '',
|
|
||||||
firstName: '',
|
|
||||||
middleName: '',
|
|
||||||
displayName: 'Unknown',
|
|
||||||
descriptors: new Float32Array(0),
|
|
||||||
relatedFaces: [],
|
|
||||||
facesCount: 0
|
|
||||||
};
|
|
||||||
const limit = faceCount
|
const limit = faceCount
|
||||||
? ` ORDER BY RANDOM() LIMIT ${faceCount} `
|
? ` ORDER BY RANDOM() LIMIT ${faceCount} `
|
||||||
: ' ORDER BY faceConfidence DESC ';
|
: ' ORDER BY faceConfidence DESC ';
|
||||||
unknownIdentity.relatedFaces = await photoDB.sequelize.query(
|
const sql = `
|
||||||
"SELECT id AS faceId,photoId,faceConfidence " +
|
SELECT
|
||||||
"FROM faces WHERE identityId IS NULL AND classifiedBy != 'not-a-face' " +
|
faces.id AS faceId,
|
||||||
limit, {
|
faces.photoId,
|
||||||
|
faces.faceConfidence,
|
||||||
|
total.facesCount
|
||||||
|
FROM
|
||||||
|
faces,
|
||||||
|
(SELECT COUNT(total.id) AS facesCount
|
||||||
|
FROM
|
||||||
|
faces AS total
|
||||||
|
WHERE
|
||||||
|
total.identityId IS NULL
|
||||||
|
AND total.classifiedBy != 'not-a-face') AS total
|
||||||
|
WHERE
|
||||||
|
faces.identityId IS NULL
|
||||||
|
AND faces.classifiedBy != 'not-a-face'
|
||||||
|
${ limit }`;
|
||||||
|
|
||||||
|
unknownIdentity.relatedFaces = await photoDB.sequelize.query(sql, {
|
||||||
type: photoDB.Sequelize.QueryTypes.SELECT,
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
raw: true
|
raw: true
|
||||||
});
|
});
|
||||||
if (unknownIdentity.relatedFaces.length === 0) {
|
if (unknownIdentity.relatedFaces.length !== 0) {
|
||||||
unknownIdentity.relatedFaces.push({
|
unknownIdentity.facesCount = unknownIdentity.relatedFaces[0].facesCount;
|
||||||
faceId: -1,
|
|
||||||
photoId: -1,
|
|
||||||
faceConfidence: 0
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
unknownIdentity.relatedFaces.forEach(face => {
|
unknownIdentity.relatedFaces.forEach(face => {
|
||||||
face.identityId = -1;
|
face.identityId = -1;
|
||||||
face.distance = face.faceConfidence;
|
face.distance = face.faceConfidence;
|
||||||
face.descriptors = new Float32Array(0);
|
face.descriptors = new Float32Array(0);
|
||||||
delete face.faceConfidence;
|
delete face.faceConfidence;
|
||||||
|
delete face.facesCount;
|
||||||
});
|
});
|
||||||
return unknownIdentity;
|
return unknownIdentity;
|
||||||
}
|
}
|
||||||
@ -452,9 +594,7 @@ const updateIdentityFaces = async (identity) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let average = undefined,
|
let average = undefined;
|
||||||
closestId = -1,
|
|
||||||
closestDistance = -1;
|
|
||||||
|
|
||||||
/* First find the average centroid of all faces */
|
/* First find the average centroid of all faces */
|
||||||
faces.forEach((face) => {
|
faces.forEach((face) => {
|
||||||
@ -479,17 +619,18 @@ const updateIdentityFaces = async (identity) => {
|
|||||||
average[i] = average[i] / faces.length;
|
average[i] = average[i] / faces.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let closestId = -1,
|
||||||
|
closestDistance = -1;
|
||||||
|
|
||||||
/* Now compute the distance from each face to the new centroid */
|
/* Now compute the distance from each face to the new centroid */
|
||||||
faces.forEach((face) => {
|
faces.forEach((face) => {
|
||||||
let distance;
|
face.updatedDistance = euclideanDistanceArray(
|
||||||
|
|
||||||
distance = euclideanDistanceArray(
|
|
||||||
face.descriptors,
|
face.descriptors,
|
||||||
average
|
average
|
||||||
);
|
);
|
||||||
|
|
||||||
if (closestId === -1 || face.distance < closestDistance) {
|
if (closestId === -1 || face.updatedDistance < closestDistance) {
|
||||||
closestDistance = distance;
|
closestDistance = face.updatedDistance;
|
||||||
closestId = face.id;
|
closestId = face.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -499,6 +640,7 @@ const updateIdentityFaces = async (identity) => {
|
|||||||
if (!identity.descriptors) {
|
if (!identity.descriptors) {
|
||||||
console.log(`Identity ${identity.identityId} has no descriptors`);
|
console.log(`Identity ${identity.identityId} has no descriptors`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let moved = (identity.descriptors === null ? 1 : 0)
|
let moved = (identity.descriptors === null ? 1 : 0)
|
||||||
|| Number
|
|| Number
|
||||||
.parseFloat(euclideanDistanceArray(identity.descriptors, average))
|
.parseFloat(euclideanDistanceArray(identity.descriptors, average))
|
||||||
@ -511,15 +653,17 @@ const updateIdentityFaces = async (identity) => {
|
|||||||
await Promise.map(faces, async (face) => {
|
await Promise.map(faces, async (face) => {
|
||||||
/* All the buffer are already arrays, so use the short-cut version */
|
/* All the buffer are already arrays, so use the short-cut version */
|
||||||
const distance = Number
|
const distance = Number
|
||||||
.parseFloat(euclideanDistanceArray(face.descriptors, average))
|
.parseFloat(face.updatedDistance)
|
||||||
.toFixed(4);
|
.toFixed(4);
|
||||||
|
|
||||||
if (Math.abs(distance - face.distance) > MIN_DISTANCE_COMMIT) {
|
if (Math.abs(face.updatedDistance - 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}) ` +
|
||||||
`from identity ${identity.identityId} (${identity.displayName})`);
|
`from identity ${identity.identityId} (${identity.displayName})`);
|
||||||
face.distance = distance;
|
face.distance = face.updatedDistance;
|
||||||
|
delete face.updatedDistance;
|
||||||
await photoDB.sequelize.query(
|
await photoDB.sequelize.query(
|
||||||
'UPDATE faces SET distance=:distance WHERE id=:id', {
|
'UPDATE faces SET distance=:distance WHERE id=:id', {
|
||||||
replacements: face,
|
replacements: face,
|
||||||
@ -677,11 +821,8 @@ router.get("/:id?", async (req, res) => {
|
|||||||
/* If there were no faces, then add a 'Unknown' face */
|
/* If there were no faces, then add a 'Unknown' face */
|
||||||
if (identity.relatedFaces.length === 0) {
|
if (identity.relatedFaces.length === 0) {
|
||||||
identity.relatedFaces.push({
|
identity.relatedFaces.push({
|
||||||
faceId: -1,
|
...UnknownFace,
|
||||||
photoId: -1,
|
...{ identityId: identity.identityId }
|
||||||
identityId: identity.identityId,
|
|
||||||
distance: 0,
|
|
||||||
faceConfidence: 0
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user