Identity averages are now working!
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
58a2baddde
commit
f8b33a65b8
9
client/package-lock.json
generated
9
client/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"moment": "^2.29.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-resizable-panels": "^0.0.34",
|
"react-resizable-panels": "^0.0.34",
|
||||||
@ -14346,6 +14347,14 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.29.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
|
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"moment": "^2.29.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-resizable-panels": "^0.0.34",
|
"react-resizable-panels": "^0.0.34",
|
||||||
|
@ -35,6 +35,15 @@ div {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.Identities {
|
.Identities {
|
||||||
display: grid;
|
display: grid;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -64,27 +73,45 @@ div {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.PhotoPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.Image .FaceBox {
|
.Image .FaceBox {
|
||||||
border: 1px solid red;
|
border: 1px solid red;
|
||||||
/* border-radius: 0.25rem;*/
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Image .FaceBox:hover {
|
.Image .FaceBox:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
box-shadow: 0px 0px 5px black;
|
box-shadow: 0px 0px 10px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Image {
|
.Image {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background-size: contain !important;
|
background-size: contain !important;
|
||||||
background-repeat: no-repeat no-repeat !important;
|
background-repeat: no-repeat no-repeat !important;
|
||||||
background-position: 50% 50% !important;
|
background-position: 50% 50% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.PhotoPanel .FaceInfo {
|
||||||
|
padding: 0.25rem;
|
||||||
|
background-color: #444;
|
||||||
|
color: white;
|
||||||
|
margin-top: 0.25rem;}
|
||||||
|
|
||||||
|
.PhotoPanel .ImageInfo {
|
||||||
|
padding: 0.25rem;
|
||||||
|
background-color: #222;
|
||||||
|
color: white;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.UnknownFace {
|
.UnknownFace {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -8,24 +8,32 @@ import {
|
|||||||
Routes,
|
Routes,
|
||||||
useParams
|
useParams
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
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 */
|
||||||
|
|
||||||
const makeFaceBoxes = (photo: any, dimensions: any, onFaceClick: any): any => {
|
const makeFaceBoxes = (photo: any,
|
||||||
|
dimensions: any,
|
||||||
|
onFaceClick: any,
|
||||||
|
onFaceEnter: any,
|
||||||
|
onFaceLeave: any,
|
||||||
|
): any => {
|
||||||
const faces: FaceData[] = photo.faces;
|
const faces: FaceData[] = photo.faces;
|
||||||
|
|
||||||
let width: number, height: number, offsetLeft = 0, offsetTop = 0;
|
let width: number, height: number, offsetLeft = 0, offsetTop = 0;
|
||||||
|
|
||||||
/* If photo is wider than viewport, it will be 100% width and < 100% height */
|
/* If photo is wider than viewport, it will be 100% width and < 100% height */
|
||||||
if (photo.width / photo.height > dimensions.width / dimensions.height) {
|
if (photo.width / photo.height > dimensions.width / dimensions.height) {
|
||||||
|
console.log('Landscape');
|
||||||
width = dimensions.width;
|
width = dimensions.width;
|
||||||
height = dimensions.height * photo.height / photo.width *
|
height = dimensions.height * photo.height / photo.width *
|
||||||
dimensions.width / dimensions.height;
|
dimensions.width / dimensions.height;
|
||||||
offsetLeft = 0;
|
offsetLeft = 0;
|
||||||
offsetTop = (dimensions.height - height) * 0.5;
|
offsetTop = (dimensions.height - height) * 0.5;
|
||||||
} else {
|
} else {
|
||||||
|
console.log('Portrait');
|
||||||
width = dimensions.width * photo.width / photo.height *
|
width = dimensions.width * photo.width / photo.height *
|
||||||
dimensions.height / dimensions.width;
|
dimensions.height / dimensions.width;
|
||||||
height = dimensions.height;
|
height = dimensions.height;
|
||||||
@ -48,8 +56,8 @@ const makeFaceBoxes = (photo: any, dimensions: any, onFaceClick: any): any => {
|
|||||||
height: Math.floor((face.bottom - face.top) * height) + "px"
|
height: Math.floor((face.bottom - face.top) * height) + "px"
|
||||||
}}
|
}}
|
||||||
onClick={(e) => { onFaceClick(e, face) }}
|
onClick={(e) => { onFaceClick(e, face) }}
|
||||||
onMouseEnter={(e) => { onFaceMouseEnter(e, face) }}
|
onMouseEnter={(e) => { onFaceEnter(e, face) }}
|
||||||
onMouseLeave={(e) => { onFaceMouseLeave(e, face) }}
|
onMouseLeave={(e) => { onFaceLeave(e, face) }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@ -57,14 +65,26 @@ const makeFaceBoxes = (photo: any, dimensions: any, onFaceClick: any): any => {
|
|||||||
|
|
||||||
const Photo = ({ photoId, onFaceClick }: any) => {
|
const Photo = ({ photoId, onFaceClick }: any) => {
|
||||||
const [image, setImage] = useState<any>(undefined);
|
const [image, setImage] = useState<any>(undefined);
|
||||||
|
const [faceInfo, setFaceInfo] = useState<string>('');
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [dimensions, setDimensions] = React.useState({width: 0, height: 0});
|
const [dimensions, setDimensions] = React.useState({width: 0, height: 0});
|
||||||
|
|
||||||
|
const onFaceEnter = (e: any, face: FaceData) => {
|
||||||
|
onFaceMouseEnter(e, face);
|
||||||
|
setFaceInfo(face.identity ? face.identity.displayName : 'Unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFaceLeave = (e: any, face: FaceData) => {
|
||||||
|
setFaceInfo('');
|
||||||
|
onFaceMouseLeave(e, face);
|
||||||
|
}
|
||||||
|
|
||||||
const faces = useMemo(() => {
|
const faces = useMemo(() => {
|
||||||
if (image === undefined || dimensions.height === 0) {
|
if (image === undefined || dimensions.height === 0) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
return makeFaceBoxes(image, dimensions, onFaceClick);
|
return makeFaceBoxes(image, dimensions,
|
||||||
|
onFaceClick, onFaceEnter, onFaceLeave);
|
||||||
}, [image, dimensions, onFaceClick]);
|
}, [image, dimensions, onFaceClick]);
|
||||||
|
|
||||||
const checkResize = useCallback(() => {
|
const checkResize = useCallback(() => {
|
||||||
@ -78,7 +98,7 @@ const Photo = ({ photoId, onFaceClick }: any) => {
|
|||||||
setDimensions({
|
setDimensions({
|
||||||
height: el.clientHeight,
|
height: el.clientHeight,
|
||||||
width: el.clientWidth
|
width: el.clientWidth
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [setDimensions, dimensions]);
|
}, [setDimensions, dimensions]);
|
||||||
|
|
||||||
@ -106,15 +126,25 @@ const Photo = ({ photoId, onFaceClick }: any) => {
|
|||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div className="Image" ref={ref}>
|
return (<div className="PhotoPanel">
|
||||||
<img
|
<div className="Image" ref={ref}>
|
||||||
src={`${base}/../${image.path}thumbs/scaled/${image.filename}`.replace(/ /g, '%20')}
|
<img
|
||||||
style={{
|
src={`${base}/../${image.path}thumbs/scaled/${image.filename}`.replace(/ /g, '%20')}
|
||||||
objectFit: 'contain',
|
style={{
|
||||||
width: '100%',
|
objectFit: 'contain',
|
||||||
height: '100%'
|
width: '100%',
|
||||||
}} />
|
height: '100%'
|
||||||
{ faces }
|
}} />
|
||||||
|
{ faces }
|
||||||
|
</div>
|
||||||
|
<div className="ImageInfo">{
|
||||||
|
moment(image.taken)
|
||||||
|
.format('MMMM Do YYYY, h:mm:ss a')
|
||||||
|
}, {
|
||||||
|
moment(image.taken)
|
||||||
|
.fromNow()
|
||||||
|
}.</div>
|
||||||
|
<div className="FaceInfo">{ faceInfo ? faceInfo : 'Hover over face for information.'}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -132,7 +162,10 @@ const onFaceMouseEnter = (e: any, face: FaceData) => {
|
|||||||
|
|
||||||
els.forEach(el => {
|
els.forEach(el => {
|
||||||
el.classList.add('Active');
|
el.classList.add('Active');
|
||||||
})
|
});
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFaceMouseLeave = (e: any, face: FaceData) => {
|
const onFaceMouseLeave = (e: any, face: FaceData) => {
|
||||||
@ -145,7 +178,10 @@ const onFaceMouseLeave = (e: any, face: FaceData) => {
|
|||||||
|
|
||||||
els.forEach(el => {
|
els.forEach(el => {
|
||||||
el.classList.remove('Active');
|
el.classList.remove('Active');
|
||||||
})
|
});
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const Face = ({ face, onFaceClick, title, ...rest }: any) => {
|
const Face = ({ face, onFaceClick, title, ...rest }: any) => {
|
||||||
@ -180,10 +216,15 @@ type ClusterProps = {
|
|||||||
identity: IdentityData,
|
identity: IdentityData,
|
||||||
setImage(image: number): void,
|
setImage(image: number): void,
|
||||||
setSelected(selected: number[]): void,
|
setSelected(selected: number[]): void,
|
||||||
setIdentity(identity: IdentityData): void
|
setIdentity(identity: IdentityData | undefined): void
|
||||||
|
identities: IdentityData[],
|
||||||
|
setIdentities(identiteis: IdentityData[]): void
|
||||||
};
|
};
|
||||||
|
|
||||||
const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps) => {
|
const Cluster = ({
|
||||||
|
identity, setIdentity,
|
||||||
|
identities, setIdentities,
|
||||||
|
setImage, setSelected }: ClusterProps) => {
|
||||||
const relatedFacesJSX = useMemo(() => {
|
const relatedFacesJSX = useMemo(() => {
|
||||||
const faceClicked = async (e: any, face: FaceData) => {
|
const faceClicked = async (e: any, face: FaceData) => {
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@ -218,7 +259,17 @@ const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps)
|
|||||||
if (identity === undefined) {
|
if (identity === undefined) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
return identity.relatedFaces.map((face: FaceData) => {
|
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={face.faceId}
|
key={face.faceId}
|
||||||
@ -248,6 +299,27 @@ const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps)
|
|||||||
setIdentity({...identity, displayName: e.currentTarget.value });
|
setIdentity({...identity, displayName: e.currentTarget.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteIdentity = async () => {
|
||||||
|
try {
|
||||||
|
const res = await window.fetch(
|
||||||
|
`${base}/api/v1/identities/${identity.identityId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
const updated = await res.json();
|
||||||
|
const index = identities
|
||||||
|
.findIndex((item: IdentityData) =>
|
||||||
|
item.identityId === identity.identityId);
|
||||||
|
if (index !== -1) {
|
||||||
|
identities.splice(index, 1);
|
||||||
|
}
|
||||||
|
setIdentity(undefined);
|
||||||
|
setIdentities([...identities]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateIdentity = async () => {
|
const updateIdentity = async () => {
|
||||||
try {
|
try {
|
||||||
const validFields = [
|
const validFields = [
|
||||||
@ -289,6 +361,7 @@ const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps)
|
|||||||
});
|
});
|
||||||
const created = await res.json();
|
const created = await res.json();
|
||||||
setIdentity(created);
|
setIdentity(created);
|
||||||
|
setIdentities([identity, ...identities]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -321,8 +394,11 @@ const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps)
|
|||||||
value={identity.displayName}
|
value={identity.displayName}
|
||||||
onChange={displayNameChanged} />
|
onChange={displayNameChanged} />
|
||||||
</form>
|
</form>
|
||||||
|
<div className="Actions">
|
||||||
<Button onClick={createIdentity}>Create</Button>
|
<Button onClick={createIdentity}>Create</Button>
|
||||||
<Button onClick={updateIdentity}>Update</Button>
|
<Button onClick={updateIdentity}>Update</Button>
|
||||||
|
<Button onClick={deleteIdentity}>Delete</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>Faces: {identity.relatedFaces.length}</div>
|
<div>Faces: {identity.relatedFaces.length}</div>
|
||||||
<div className="Faces">
|
<div className="Faces">
|
||||||
@ -356,7 +432,9 @@ type IdentityData = {
|
|||||||
descriptors: number[],
|
descriptors: number[],
|
||||||
identityId: number
|
identityId: number
|
||||||
displayName: string,
|
displayName: string,
|
||||||
relatedFaces: FaceData[]
|
relatedFaces: FaceData[],
|
||||||
|
facesCount: number,
|
||||||
|
faceId: number
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IdentitiesProps {
|
interface IdentitiesProps {
|
||||||
@ -379,7 +457,7 @@ const Identities = ({ identities, onFaceClick } : IdentitiesProps) => {
|
|||||||
<Face
|
<Face
|
||||||
face={face}
|
face={face}
|
||||||
onFaceClick={onFaceClick}
|
onFaceClick={onFaceClick}
|
||||||
title={identity.displayName}/>
|
title={`${identity.displayName} (${identity.facesCount})`}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -406,7 +484,8 @@ const App = () => {
|
|||||||
const [selectedIdentities, setSelectedIdentities] = useState<number[]>([]);
|
const [selectedIdentities, setSelectedIdentities] = useState<number[]>([]);
|
||||||
const [identity, setIdentity] = useState<IdentityData | undefined>(undefined);
|
const [identity, setIdentity] = useState<IdentityData | undefined>(undefined);
|
||||||
const [image, setImage] = useState<number>(0);
|
const [image, setImage] = useState<number>(0);
|
||||||
const { loading, data } = useApi(
|
const [guess, setGuess] = useState<FaceData|undefined>(undefined);
|
||||||
|
const { loading, data } = useApi( /* TODO: Switch away from using useApi */
|
||||||
`${base}/api/v1/identities`
|
`${base}/api/v1/identities`
|
||||||
);
|
);
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
@ -467,6 +546,9 @@ const App = () => {
|
|||||||
face.identity = identity;
|
face.identity = identity;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
data.sort((A: IdentityData, B: IdentityData) => {
|
||||||
|
return A.displayName.localeCompare(B.displayName);
|
||||||
|
});
|
||||||
setIdentities(data as IdentityData[]);
|
setIdentities(data as IdentityData[]);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@ -485,6 +567,42 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mergeIdentity = async () => {
|
||||||
|
if (selectedIdentities.length === 0) {
|
||||||
|
window.alert('You need to select an identity first (CTRL+CLICK)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!identity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let res = await window.fetch(
|
||||||
|
`${base}/api/v1/identities/faces/add/${selectedIdentities[0]}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ faces: identity.relatedFaces
|
||||||
|
.map(face => face.faceId) })
|
||||||
|
});
|
||||||
|
await res.json();
|
||||||
|
res = await window.fetch(
|
||||||
|
`${base}/api/v1/identities/${identity.identityId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
const updated = await res.json();
|
||||||
|
const index = identities
|
||||||
|
.findIndex((item: IdentityData) =>
|
||||||
|
item.identityId === identity.identityId);
|
||||||
|
if (index !== -1) {
|
||||||
|
identities.splice(index, 1);
|
||||||
|
}
|
||||||
|
setIdentity(undefined);
|
||||||
|
setIdentities([...identities]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const markSelectedIncorrectIdentity = async () => {
|
const markSelectedIncorrectIdentity = async () => {
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
return;
|
return;
|
||||||
@ -505,7 +623,6 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeSelectedIdentity = async () => {
|
const changeSelectedIdentity = async () => {
|
||||||
|
|
||||||
if (selectedIdentities.length === 0) {
|
if (selectedIdentities.length === 0) {
|
||||||
window.alert('You need to select an identity first (CTRL+CLICK)');
|
window.alert('You need to select an identity first (CTRL+CLICK)');
|
||||||
return;
|
return;
|
||||||
@ -525,6 +642,21 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const guessIdentity = async () => {
|
||||||
|
try {
|
||||||
|
const res = await window.fetch(
|
||||||
|
`${base}/api/v1/identities/faces/guess/${selected[0]}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
const faces = await res.json();
|
||||||
|
console.log(faces);
|
||||||
|
setGuess(faces[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const markSelectedNotFace = async () => {
|
const markSelectedNotFace = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch(
|
const res = await window.fetch(
|
||||||
@ -537,12 +669,23 @@ const App = () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
removeFacesFromIdentities(data);
|
removeFacesFromIdentities(selected);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deselectAll = () => {
|
||||||
|
const cluster = document.querySelector('.Cluster');
|
||||||
|
if (!cluster) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[...cluster.querySelectorAll('.Selected')].forEach(item => {
|
||||||
|
item.classList.remove('Selected')
|
||||||
|
});
|
||||||
|
setSelected([]);
|
||||||
|
};
|
||||||
|
|
||||||
const onFaceClick = (e: any, face: FaceData) => {
|
const onFaceClick = (e: any, face: FaceData) => {
|
||||||
const identityId = face.identityId;
|
const identityId = face.identityId;
|
||||||
const faceId = face.faceId;
|
const faceId = face.faceId;
|
||||||
@ -555,6 +698,9 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const guessOnFaceClick = (e: any, face: FaceData) => {
|
||||||
|
};
|
||||||
|
|
||||||
const identitiesOnFaceClick = (e: any, face: FaceData) => {
|
const identitiesOnFaceClick = (e: any, face: FaceData) => {
|
||||||
const identitiesEl = document.querySelector('.Identities');
|
const identitiesEl = document.querySelector('.Identities');
|
||||||
if (!identitiesEl) {
|
if (!identitiesEl) {
|
||||||
@ -600,6 +746,8 @@ const App = () => {
|
|||||||
<Cluster {...{
|
<Cluster {...{
|
||||||
identity,
|
identity,
|
||||||
setIdentity,
|
setIdentity,
|
||||||
|
identities,
|
||||||
|
setIdentities,
|
||||||
setImage,
|
setImage,
|
||||||
setSelected
|
setSelected
|
||||||
}} />}
|
}} />}
|
||||||
@ -607,10 +755,17 @@ const App = () => {
|
|||||||
Select identity to edit
|
Select identity to edit
|
||||||
</div>}
|
</div>}
|
||||||
<div className="Actions">
|
<div className="Actions">
|
||||||
|
{selected.length === 1 && <>
|
||||||
|
<Button onClick={guessIdentity}>Guess</Button>
|
||||||
|
</>}
|
||||||
{ selected.length !== 0 && <>
|
{ selected.length !== 0 && <>
|
||||||
<Button onClick={markSelectedIncorrectIdentity}>Remove</Button>
|
<Button onClick={markSelectedIncorrectIdentity}>Remove</Button>
|
||||||
<Button onClick={markSelectedNotFace}>Not a face</Button>
|
<Button onClick={markSelectedNotFace}>Not a face</Button>
|
||||||
<Button onClick={changeSelectedIdentity}>Change Identity</Button>
|
<Button onClick={changeSelectedIdentity}>Change Identity</Button>
|
||||||
|
<Button onClick={deselectAll}>Deselect All</Button>
|
||||||
|
</>}
|
||||||
|
{selectedIdentities.length !== 0 && <>
|
||||||
|
<Button onClick={mergeIdentity}>Merge</Button>
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
@ -618,6 +773,16 @@ const App = () => {
|
|||||||
<Panel>
|
<Panel>
|
||||||
{image === 0 && <div style={{ margin: '1rem' }}>Select image to view</div>}
|
{image === 0 && <div style={{ margin: '1rem' }}>Select image to view</div>}
|
||||||
{image !== 0 && <Photo onFaceClick={onFaceClick} photoId={image}/> }
|
{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> }
|
||||||
</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">
|
||||||
|
@ -119,6 +119,7 @@ function init() {
|
|||||||
key: 'id',
|
key: 'id',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
facesCount: Sequelize.INTEGER,
|
||||||
descriptors: Sequelize.BLOB /* average of all faces mapped to this */
|
descriptors: Sequelize.BLOB /* average of all faces mapped to this */
|
||||||
}, {
|
}, {
|
||||||
timestamps: false
|
timestamps: false
|
||||||
|
@ -31,7 +31,7 @@ const upsertIdentity = async(id, {
|
|||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
middleName,
|
middleName,
|
||||||
id
|
identityId: id
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id === -1 || !id) {
|
if (id === -1 || !id) {
|
||||||
@ -41,7 +41,7 @@ const upsertIdentity = async(id, {
|
|||||||
'VALUES(:displayName,:firstName,:lastName,:middleName)', {
|
'VALUES(:displayName,:firstName,:lastName,:middleName)', {
|
||||||
replacements: identity
|
replacements: identity
|
||||||
});
|
});
|
||||||
identity.id = lastId;
|
identity.identityId = lastId;
|
||||||
} else {
|
} else {
|
||||||
await photoDB.sequelize.query(
|
await photoDB.sequelize.query(
|
||||||
`UPDATE identities ` +
|
`UPDATE identities ` +
|
||||||
@ -50,7 +50,7 @@ const upsertIdentity = async(id, {
|
|||||||
'firstName=:firstName, ' +
|
'firstName=:firstName, ' +
|
||||||
'lastName=:lastName, ' +
|
'lastName=:lastName, ' +
|
||||||
'middleName=:middleName ' +
|
'middleName=:middleName ' +
|
||||||
'WHERE id=:id', {
|
'WHERE id=:identityId', {
|
||||||
replacements: identity
|
replacements: identity
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -74,7 +74,7 @@ const populateRelatedFaces = async (identity, count) => {
|
|||||||
"FROM faces " +
|
"FROM faces " +
|
||||||
"WHERE identityId=:identityId " +
|
"WHERE identityId=:identityId " +
|
||||||
limit, {
|
limit, {
|
||||||
replacements: { identityId: identity.id },
|
replacements: { identityId: identity.identityId },
|
||||||
type: photoDB.Sequelize.QueryTypes.SELECT,
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
raw: true
|
raw: true
|
||||||
});
|
});
|
||||||
@ -92,13 +92,14 @@ const populateRelatedFaces = async (identity, count) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
identity.relatedFaces.forEach(face => {
|
identity.relatedFaces.forEach(face => {
|
||||||
face.identityId = identity.id;
|
face.identityId = identity.identityId;
|
||||||
face.distance = face.faceConfidence;
|
face.distance = face.faceConfidence;
|
||||||
face.descriptors = [];
|
face.descriptors = [];
|
||||||
delete face.faceConfidence;
|
delete face.faceConfidence;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Create new identity */
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
console.log(`POST ${req.url}`)
|
console.log(`POST ${req.url}`)
|
||||||
if (!req.user.maintainer) {
|
if (!req.user.maintainer) {
|
||||||
@ -110,10 +111,12 @@ router.post('/', async (req, res) => {
|
|||||||
if (!identity) {
|
if (!identity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
populateRelatedFaces(identity, 1);
|
await updateIdentityFaces(identity);
|
||||||
|
await populateRelatedFaces(identity, 1);
|
||||||
return res.status(200).send(identity);
|
return res.status(200).send(identity);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Update identity */
|
||||||
router.put('/:id', async (req, res) => {
|
router.put('/:id', async (req, res) => {
|
||||||
console.log(`PUT ${req.url}`)
|
console.log(`PUT ${req.url}`)
|
||||||
if (!req.user.maintainer) {
|
if (!req.user.maintainer) {
|
||||||
@ -130,7 +133,8 @@ router.put('/:id', async (req, res) => {
|
|||||||
if (!identity) {
|
if (!identity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
populateRelatedFaces(identity);
|
await updateIdentityFaces(identity);
|
||||||
|
await populateRelatedFaces(identity);
|
||||||
return res.status(200).send(identity);
|
return res.status(200).send(identity);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,7 +160,7 @@ router.delete('/:id', async (req, res) => {
|
|||||||
|
|
||||||
await photoDB.sequelize.query(
|
await photoDB.sequelize.query(
|
||||||
'DELETE FROM identities ' +
|
'DELETE FROM identities ' +
|
||||||
'WHERE identityId=:id', {
|
'WHERE id=:id', {
|
||||||
replacements: { id }
|
replacements: { id }
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,11 +168,6 @@ router.delete('/:id', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const addFaceToIdentityDescriptors = (identity, face) => {
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFaceToIdentityDescriptors = (identity, face) => {
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeIdentityDescriptors = async (identity) => {
|
const writeIdentityDescriptors = async (identity) => {
|
||||||
await photoDB.sequelize.query(
|
await photoDB.sequelize.query(
|
||||||
@ -180,6 +179,86 @@ const writeIdentityDescriptors = async (identity) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* Given a faceId, find the closest defined identity and return
|
||||||
|
* it as a guess -- does not modify the DB */
|
||||||
|
router.get("/faces/guess/:faceId", async (req, res) => {
|
||||||
|
const faceId = parseInt(req.params.faceId);
|
||||||
|
if (faceId != req.params.faceId) {
|
||||||
|
return res.status(400).send({ message: "Invalid identity id." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const faces = await photoDB.sequelize.query(
|
||||||
|
"SELECT faces.*,faceDescriptors.* " +
|
||||||
|
"FROM faces,faceDescriptors " +
|
||||||
|
"WHERE faces.id=:faceId " +
|
||||||
|
"AND faceDescriptors.id=faces.descriptorId", {
|
||||||
|
replacements: { faceId },
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const identities = await photoDB.sequelize.query(
|
||||||
|
"SELECT * FROM identities", {
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
identities.forEach(x => { x.identityId = x.id; delete x.id;});
|
||||||
|
|
||||||
|
faces.forEach((face) => {
|
||||||
|
face.faceId = face.id;
|
||||||
|
delete face.id;
|
||||||
|
face.identityId = -1;
|
||||||
|
face.distance = -1;
|
||||||
|
face.identity = null;
|
||||||
|
|
||||||
|
identities.forEach((identity) => {
|
||||||
|
if (!identity.descriptors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = euclideanDistance(
|
||||||
|
face.descriptors,
|
||||||
|
identity.descriptors
|
||||||
|
);
|
||||||
|
|
||||||
|
if (face.identityId === -1) {
|
||||||
|
face.identityId = identity.identityId;
|
||||||
|
face.identity = identity;
|
||||||
|
face.distance = distance;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance < face.distance) {
|
||||||
|
face.identityId = identity.identityId;
|
||||||
|
face.identity = identity;
|
||||||
|
face.distance = distance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Delete the VGG-Face descriptors and add relatedFaces[0] */
|
||||||
|
const results = [];
|
||||||
|
await Promise.map(faces, async (face) => {
|
||||||
|
delete face.descriptors;
|
||||||
|
if (face.identity.descriptors) {
|
||||||
|
delete face.identity.descriptors;
|
||||||
|
}
|
||||||
|
if (!face.identity.relatedFaces) {
|
||||||
|
await populateRelatedFaces(face.identity, 1);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
concurrency: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(faces);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).send("Error processing request.");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
router.put("/faces/remove/:id", async (req, res) => {
|
router.put("/faces/remove/:id", async (req, res) => {
|
||||||
console.log(`PUT ${req.url}`)
|
console.log(`PUT ${req.url}`)
|
||||||
if (!req.user.maintainer) {
|
if (!req.user.maintainer) {
|
||||||
@ -206,13 +285,12 @@ router.put("/faces/remove/:id", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const identity = {
|
const identity = {
|
||||||
id: id,
|
identityId: id,
|
||||||
faces: req.body.faces
|
faces: req.body.faces
|
||||||
};
|
};
|
||||||
identity.faces = identity.faces.map(id => +id);
|
identity.faces = identity.faces.map(id => +id);
|
||||||
|
|
||||||
updateIdentityDescriptors(identity);
|
await updateIdentityFaces(identity);
|
||||||
|
|
||||||
return res.status(200).json(identity);
|
return res.status(200).json(identity);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -245,13 +323,12 @@ router.put("/faces/add/:id", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const identity = {
|
const identity = {
|
||||||
id: id,
|
identityId: id,
|
||||||
faces: req.body.faces
|
faces: req.body.faces
|
||||||
};
|
};
|
||||||
identity.faces = identity.faces.map(id => +id);
|
identity.faces = identity.faces.map(id => +id);
|
||||||
|
|
||||||
updateIdentityDescriptors(identity);
|
await updateIdentityFaces(identity);
|
||||||
|
|
||||||
return res.status(200).json(identity);
|
return res.status(200).json(identity);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -259,49 +336,6 @@ router.put("/faces/add/:id", async (req, res) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
|
||||||
if (!req.user.maintainer) {
|
|
||||||
console.warn(`${req.user.name} attempted to modify photos.`);
|
|
||||||
return res.status(401).send("Unauthorized to modify photos.");
|
|
||||||
}
|
|
||||||
const identity = {
|
|
||||||
lastName: req.body.lastName || "",
|
|
||||||
firstName: req.body.firstName || "",
|
|
||||||
middleName: req.body.middleName || ""
|
|
||||||
};
|
|
||||||
identity.name = req.body.name || (identity.firstName + " " + identity.lastName);
|
|
||||||
|
|
||||||
let fields = [];
|
|
||||||
for (let key in identity) {
|
|
||||||
fields.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(req.body.faces) || req.body.faces.length == 0) {
|
|
||||||
return res.status(400).send("No faces supplied.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return photoDB.sequelize.query("INSERT INTO identities " +
|
|
||||||
"(" + fields.join(",") + ") " +
|
|
||||||
"VALUES(:" + fields.join(",:") + ")", {
|
|
||||||
replacements: identity,
|
|
||||||
}).then(([ results, metadata ]) => {
|
|
||||||
identity.id = metadata.lastID;
|
|
||||||
return photoDB.sequelize.query(
|
|
||||||
"UPDATE faces SET identityId=:identityId " +
|
|
||||||
"WHERE id IN (:faceIds)", {
|
|
||||||
replacements: {
|
|
||||||
identityId: identity.id,
|
|
||||||
faceIds: req.body.faces
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
identity.faces = req.body.faces;
|
|
||||||
return res.status(200).json([identity]);
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
return res.status(500).send("Error processing request.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function bufferToFloat32Array(buffer) {
|
function bufferToFloat32Array(buffer) {
|
||||||
return new Float64Array(buffer.buffer,
|
return new Float64Array(buffer.buffer,
|
||||||
@ -309,19 +343,24 @@ function bufferToFloat32Array(buffer) {
|
|||||||
buffer.byteLength / Float64Array.BYTES_PER_ELEMENT);
|
buffer.byteLength / Float64Array.BYTES_PER_ELEMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function euclideanDistanceArray(a, b) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
let delta = a[i] - b[i];
|
||||||
|
sum += delta * delta;
|
||||||
|
}
|
||||||
|
return Math.sqrt(sum);
|
||||||
|
}
|
||||||
|
|
||||||
function euclideanDistance(a, b) {
|
function euclideanDistance(a, b) {
|
||||||
if (!a.buffer || !b.buffer) {
|
if (!a.buffer || !b.buffer) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let A = bufferToFloat32Array(a);
|
return euclideanDistanceArray(
|
||||||
let B = bufferToFloat32Array(b);
|
bufferToFloat32Array(a),
|
||||||
let sum = 0;
|
bufferToFloat32Array(b)
|
||||||
for (let i = 0; i < A.length; i++) {
|
);
|
||||||
let delta = A[i] - B[i];
|
|
||||||
sum += delta * delta;
|
|
||||||
}
|
|
||||||
return Math.sqrt(sum);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUnknownIdentity = async (faceCount) => {
|
const getUnknownIdentity = async (faceCount) => {
|
||||||
@ -332,10 +371,11 @@ const getUnknownIdentity = async (faceCount) => {
|
|||||||
middleName: '',
|
middleName: '',
|
||||||
displayName: 'Unknown',
|
displayName: 'Unknown',
|
||||||
descriptors: new Float32Array(0),
|
descriptors: new Float32Array(0),
|
||||||
relatedFaces: []
|
relatedFaces: [],
|
||||||
|
facesCount: 0
|
||||||
};
|
};
|
||||||
const limit = faceCount
|
const limit = faceCount
|
||||||
? ` LIMIT ${faceCount} `
|
? ` ORDER BY RANDOM() LIMIT ${faceCount} `
|
||||||
: ' ORDER BY faceConfidence DESC ';
|
: ' ORDER BY faceConfidence DESC ';
|
||||||
unknownIdentity.relatedFaces = await photoDB.sequelize.query(
|
unknownIdentity.relatedFaces = await photoDB.sequelize.query(
|
||||||
"SELECT id AS faceId,photoId,faceConfidence " +
|
"SELECT id AS faceId,photoId,faceConfidence " +
|
||||||
@ -360,6 +400,13 @@ const getUnknownIdentity = async (faceCount) => {
|
|||||||
return unknownIdentity;
|
return unknownIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const round = (x, precision) => {
|
||||||
|
if (precision === undefined) {
|
||||||
|
precision = 2;
|
||||||
|
}
|
||||||
|
return Number.parseFloat(x).toFixed(precision);
|
||||||
|
};
|
||||||
|
|
||||||
/* Compute the identity's centroid descriptor from all faces
|
/* Compute the identity's centroid descriptor from all faces
|
||||||
* and determine closest face to that centroid. If either of
|
* and determine closest face to that centroid. If either of
|
||||||
* those values have changed, update the identity.
|
* those values have changed, update the identity.
|
||||||
@ -369,8 +416,27 @@ const getUnknownIdentity = async (faceCount) => {
|
|||||||
*/
|
*/
|
||||||
const updateIdentityFaces = async (identity) => {
|
const updateIdentityFaces = async (identity) => {
|
||||||
if (!identity.identityId) {
|
if (!identity.identityId) {
|
||||||
identity.identityId = identity.id;
|
throw Error(`identityId is not set.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!identity.descriptors) {
|
||||||
|
const results = await photoDB.sequelize.query(
|
||||||
|
"SELECT " +
|
||||||
|
"descriptors,facesCount,faceId " +
|
||||||
|
"FROM identities " +
|
||||||
|
"WHERE id=:identityId", {
|
||||||
|
replacements: identity,
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
Object.assign(identity, results[0]);
|
||||||
|
/* New identities do not have descriptors set */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity.descriptors) {
|
||||||
|
identity.descriptors = bufferToFloat32Array(identity.descriptors);
|
||||||
|
}
|
||||||
|
|
||||||
const faces = await photoDB.sequelize.query(
|
const faces = await photoDB.sequelize.query(
|
||||||
"SELECT " +
|
"SELECT " +
|
||||||
"faces.*,faceDescriptors.* " +
|
"faces.*,faceDescriptors.* " +
|
||||||
@ -383,73 +449,132 @@ const updateIdentityFaces = async (identity) => {
|
|||||||
raw: true
|
raw: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (faces.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let average = undefined,
|
let average = undefined,
|
||||||
closestId = -1,
|
closestId = -1,
|
||||||
closestDistance = -1,
|
closestDistance = -1;
|
||||||
count = 0;
|
|
||||||
|
/* First find the average centroid of all faces */
|
||||||
faces.forEach((face) => {
|
faces.forEach((face) => {
|
||||||
if (!identity.descriptors) {
|
/* Convert the descriptors from buffer to array so they can be
|
||||||
|
* modified */
|
||||||
|
face.descriptors = bufferToFloat32Array(face.descriptors);
|
||||||
|
|
||||||
|
/* First face starts the average sum */
|
||||||
|
if (average === undefined) {
|
||||||
|
average = face.descriptors.slice();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!face.descriptors) {
|
|
||||||
return;
|
/* Add this face descriptor into the average descriptor */
|
||||||
}
|
for (let i = 0; i < face.descriptors.length; i++) {
|
||||||
face.distance = euclideanDistance(
|
average[i] = average[i] + face.descriptors[i];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Divide sum of descriptors to create average centroid */
|
||||||
|
for (let i = 0; i < average.length; i++) {
|
||||||
|
average[i] = average[i] / faces.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now compute the distance from each face to the new centroid */
|
||||||
|
faces.forEach((face) => {
|
||||||
|
let distance;
|
||||||
|
|
||||||
|
distance = euclideanDistanceArray(
|
||||||
face.descriptors,
|
face.descriptors,
|
||||||
identity.descriptors
|
average
|
||||||
);
|
);
|
||||||
face.descriptors = bufferToFloat32Array(face.descriptors).map(x => x * x);
|
|
||||||
|
|
||||||
if (closestId === -1) {
|
if (closestId === -1 || face.distance < closestDistance) {
|
||||||
closestId = face.id;
|
closestDistance = distance;
|
||||||
closestDistance = face.distance;
|
|
||||||
average = descriptors;
|
|
||||||
count = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptors.forEach((x, i) => {
|
|
||||||
average[i] += x;
|
|
||||||
});
|
|
||||||
count++;
|
|
||||||
|
|
||||||
if (face.distance < closestDistance) {
|
|
||||||
closestDistance = face.distance;
|
|
||||||
closestId = face.id;
|
closestId = face.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let same = true;
|
/* Determine if the centroid for this identity has moved
|
||||||
if (average) {
|
* and for each relatedFace, update its distance to the centroid */
|
||||||
average = average.map(x => x / count);
|
if (!identity.descriptors) {
|
||||||
same = bufferToFloat32Array(identity.descriptors)
|
console.log(`Identity ${identity.identityId} has no descriptors`);
|
||||||
.find((x, i) => average[i] === x) === undefined;
|
|
||||||
await Promise(faces, async (face) => {
|
|
||||||
const distance = euclideanDistanceArray(face.descriptors, average);
|
|
||||||
if (distance !== face.distance) {
|
|
||||||
await photoDB.sequelize.query(
|
|
||||||
'UPDATE faces SET distance=:distance WHERE id=:faceId', {
|
|
||||||
replacements: face
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
let moved = (identity.descriptors === null ? 1 : 0)
|
||||||
|
|| Number
|
||||||
|
.parseFloat(euclideanDistanceArray(identity.descriptors, average))
|
||||||
|
.toFixed(4);
|
||||||
|
|
||||||
|
/* 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(euclideanDistanceArray(face.descriptors, average))
|
||||||
|
.toFixed(4);
|
||||||
|
|
||||||
|
if (Math.abs(distance - face.distance) > 0.0001) {
|
||||||
|
console.log(
|
||||||
|
`Updating face ${face.id} to ${round(distance, 2)} ` +
|
||||||
|
`(${distance - face.distance}) ` +
|
||||||
|
`from identity ${identity.identityId} (${identity.displayName})`);
|
||||||
|
face.distance = distance;
|
||||||
|
await photoDB.sequelize.query(
|
||||||
|
'UPDATE faces SET distance=:distance WHERE id=:id', {
|
||||||
|
replacements: face
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
concurrency: 1
|
||||||
|
});
|
||||||
|
|
||||||
let sql = '';
|
let sql = '';
|
||||||
|
/* If there is a new closestId, then set the faceId field */
|
||||||
if (closestId !== -1 && closestId !== identity.faceId) {
|
if (closestId !== -1 && closestId !== identity.faceId) {
|
||||||
|
console.log(
|
||||||
|
`Updating identity ${identity.identityId} closest face to ${closestId}`);
|
||||||
sql = `${sql} faceId=:faceId`;
|
sql = `${sql} faceId=:faceId`;
|
||||||
|
identity.faceId = closestId;
|
||||||
}
|
}
|
||||||
if (!same) {
|
|
||||||
|
/* If the centroid changed, update the identity descriptors to
|
||||||
|
* the new average */
|
||||||
|
if (Math.abs(moved) > 0.0001) {
|
||||||
|
console.log(
|
||||||
|
`Updating identity ${identity.identityId} centroid ` +
|
||||||
|
`(moved ${Number.parseFloat(moved).toFixed(4)}).`);
|
||||||
if (sql !== '') {
|
if (sql !== '') {
|
||||||
sql = `${sql}, `;
|
sql = `${sql}, `;
|
||||||
}
|
}
|
||||||
sql = `${sql} descriptors=:descriptors`;
|
sql = `${sql} descriptors=:descriptors`;
|
||||||
identity.descriptors = average;
|
// this: identity.descriptors = average;
|
||||||
|
// gives: Invalid value Float64Array(2622)
|
||||||
|
//
|
||||||
|
// this: identity.descriptors = new Blob(average);
|
||||||
|
// gives: Invalid value Blob { size: 54008, type: '' }
|
||||||
|
//
|
||||||
|
// this: identity.descriptors = Buffer.from(average);
|
||||||
|
// gives: all zeroes
|
||||||
|
//
|
||||||
|
// this: identity.descriptors = Buffer.from(average.buffer);
|
||||||
|
// gives: IT WORKS!!!
|
||||||
|
identity.descriptors = Buffer.from(average.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* If the number of faces changed, update the facesCount */
|
||||||
|
if (identity.facesCount !== faces.length) {
|
||||||
|
if (sql !== '') {
|
||||||
|
sql = `${sql}, `;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Updating identity ${identity.identityId} face count to ${faces.length}`);
|
||||||
|
identity.facesCount = faces.length;
|
||||||
|
sql = `${sql} facesCount=${faces.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If any of the above required changes, actually commit to the DB */
|
||||||
if (sql !== '') {
|
if (sql !== '') {
|
||||||
identity.faceId = closestId;
|
|
||||||
await photoDB.sequelize.query(
|
await photoDB.sequelize.query(
|
||||||
`UPDATE identities SET ${sql} ` +
|
`UPDATE identities SET ${sql} ` +
|
||||||
`WHERE id=:identityId`, {
|
`WHERE id=:identityId`, {
|
||||||
@ -487,7 +612,8 @@ router.get("/:id?", async (req, res) => {
|
|||||||
"identities.lastName," +
|
"identities.lastName," +
|
||||||
"identities.middleName," +
|
"identities.middleName," +
|
||||||
"identities.displayName," +
|
"identities.displayName," +
|
||||||
"identities.faceId " +
|
"identities.faceId," +
|
||||||
|
"identities.facesCount " +
|
||||||
"FROM identities " +
|
"FROM identities " +
|
||||||
filter, {
|
filter, {
|
||||||
replacements: { id },
|
replacements: { id },
|
||||||
@ -496,6 +622,9 @@ 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] = '';
|
||||||
@ -526,6 +655,10 @@ router.get("/:id?", async (req, res) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* If this identity has at least one face associated with it,
|
||||||
|
* and it does not yet have the 'closest' face assigned, update
|
||||||
|
* the identity statistics.
|
||||||
|
*/
|
||||||
if (identity.relatedFaces.length !== 0
|
if (identity.relatedFaces.length !== 0
|
||||||
&& (!identity.faceId || identity.faceId === -1)) {
|
&& (!identity.faceId || identity.faceId === -1)) {
|
||||||
await updateIdentityFaces(identity);
|
await updateIdentityFaces(identity);
|
||||||
@ -541,6 +674,8 @@ router.get("/:id?", async (req, res) => {
|
|||||||
faceConfidence: 0
|
faceConfidence: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
concurrency: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
/* If no ID was provided then this call is returning
|
/* If no ID was provided then this call is returning
|
||||||
|
Loading…
x
Reference in New Issue
Block a user