Continuing development...

Signed-off-by: James P. Ketrenos <james.p.ketrenos@intel.com>
This commit is contained in:
James P. Ketrenos 2023-01-22 14:48:41 -08:00
parent 99db385686
commit 40b3a0d819
4 changed files with 273 additions and 107 deletions

View File

@ -85,6 +85,13 @@ div {
background-position: 50% 50% !important; background-position: 50% 50% !important;
} }
.UnknownFace {
display: flex;
align-items: center;
font-size: 4rem;
font-weight: bold;
}
.IdentityForm { .IdentityForm {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -116,6 +123,8 @@ div {
box-sizing: border-box; box-sizing: border-box;
width: 8rem; width: 8rem;
height: 8rem; height: 8rem;
display: flex;
justify-content: center;
} }
.Cluster { .Cluster {

View File

@ -123,14 +123,12 @@ const onFaceMouseEnter = (e: any, face: FaceData) => {
const faceId = face.faceId; const faceId = face.faceId;
const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)]; const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)];
if (face.identity) { const identityId = face.identityId;
const identityId = face.identity.identityId; els.splice(0, 0,
els.splice(0, 0, ...document.querySelectorAll(
...document.querySelectorAll( `.Identities [data-identity-id="${identityId}"]`),
`.Identities [data-identity-id="${identityId}"]`), ...document.querySelectorAll(
...document.querySelectorAll( `.Photo [data-identity-id="${identityId}"]`));
`.Photo [data-identity-id="${identityId}"]`));
}
els.forEach(el => { els.forEach(el => {
el.classList.add('Active'); el.classList.add('Active');
@ -141,11 +139,9 @@ const onFaceMouseLeave = (e: any, face: FaceData) => {
const faceId = face.faceId; const faceId = face.faceId;
const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)]; const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)];
if (face.identity) { const identityId = face.identityId;
const identityId = face.identity.identityId; els.splice(0, 0,
els.splice(0, 0, ...document.querySelectorAll(`[data-identity-id="${identityId}"]`));
...document.querySelectorAll(`[data-identity-id="${identityId}"]`));
}
els.forEach(el => { els.forEach(el => {
el.classList.remove('Active'); el.classList.remove('Active');
@ -155,6 +151,14 @@ const onFaceMouseLeave = (e: any, face: FaceData) => {
const Face = ({ face, onFaceClick, title, ...rest }: any) => { const Face = ({ face, onFaceClick, title, ...rest }: any) => {
const faceId = face.faceId; const faceId = face.faceId;
const idPath = String(faceId % 100).padStart(2, '0'); const idPath = String(faceId % 100).padStart(2, '0');
const img = faceId === -1
? <div className='UnknownFace'>?</div>
: <img src={`${base}/../faces/${idPath}/${faceId}.jpg`}
style={{
objectFit: 'contain',
width: '100%',
height: '100%'
}} />;
return ( return (
<div <div
data-face-id={face.faceId} data-face-id={face.faceId}
@ -165,12 +169,7 @@ const Face = ({ face, onFaceClick, title, ...rest }: any) => {
onMouseLeave={(e) => { onFaceMouseLeave(e, face) }} onMouseLeave={(e) => { onFaceMouseLeave(e, face) }}
className='Face'> className='Face'>
<div className='Image'> <div className='Image'>
<img src={`${base}/../faces/${idPath}/${faceId}.jpg`} { img }
style={{
objectFit: 'contain',
width: '100%',
height: '100%'
}}/>
<div className='Title'>{title}</div> <div className='Title'>{title}</div>
</div> </div>
</div> </div>
@ -185,8 +184,6 @@ type ClusterProps = {
}; };
const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps) => { const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps) => {
console.log(identity);
const relatedFacesJSX = useMemo(() => { const relatedFacesJSX = useMemo(() => {
const faceClicked = async (e: any, face: FaceData) => { const faceClicked = async (e: any, face: FaceData) => {
if (!identity) { if (!identity) {
@ -267,13 +264,37 @@ const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps)
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filtered) body: JSON.stringify(filtered)
}); });
const data = await res.json(); const updated = await res.json();
setIdentity({ ...identity }); setIdentity({ ...identity });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}; };
const createIdentity = async () => {
try {
const validFields = [
'id', 'displayName', 'firstName', 'lastName', 'middleName'];
const filtered: any = Object.assign({}, identity);
for (let key in filtered) {
if (validFields.indexOf(key) == -1) {
delete filtered[key]
}
}
const res = await window.fetch(
`${base}/api/v1/identities/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filtered)
});
const created = await res.json();
setIdentity(created);
} catch (error) {
console.error(error);
}
};
if (identity === undefined) { if (identity === undefined) {
return (<div className='Cluster'> return (<div className='Cluster'>
Select identity to load. Select identity to load.
@ -300,6 +321,7 @@ const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps)
value={identity.displayName} value={identity.displayName}
onChange={displayNameChanged} /> onChange={displayNameChanged} />
</form> </form>
<Button onClick={createIdentity}>Create</Button>
<Button onClick={updateIdentity}>Update</Button> <Button onClick={updateIdentity}>Update</Button>
</div> </div>
<div>Faces: {identity.relatedFaces.length}</div> <div>Faces: {identity.relatedFaces.length}</div>
@ -313,10 +335,10 @@ const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps)
type FaceData = { type FaceData = {
faceId: number, faceId: number,
photoId: number, photoId: number,
lastName: string, /* lastName: string,
firstName: string, firstName: string,
middleName: string, middleName: string,
displayName: string, displayName: string,*/
identity: IdentityData, identity: IdentityData,
identityId: number, identityId: number,
distance: number, distance: number,
@ -500,11 +522,7 @@ const App = () => {
}; };
const onFaceClick = (e: any, face: FaceData) => { const onFaceClick = (e: any, face: FaceData) => {
if (!face.identity) { const identityId = face.identityId;
console.log(`Face ${face.faceId} does not have an Identity`);
return;
}
const identityId = face.identity.identityId;
const faceId = face.faceId; const faceId = face.faceId;
console.log(`onFaceClick`, { faceId, identityId}); console.log(`onFaceClick`, { faceId, identityId});
const faces = [ const faces = [
@ -516,7 +534,7 @@ const App = () => {
}; };
const identitiesOnFaceClick = (e: any, face: FaceData) => { const identitiesOnFaceClick = (e: any, face: FaceData) => {
const identityId = face.identity.identityId; const identityId = face.identityId;
loadIdentity(identityId); loadIdentity(identityId);
} }

View File

@ -1,6 +1,9 @@
# DEVELOPMENT -- use npm development server on port 3000 (entrypoint.sh) # DEVELOPMENT -- use npm development server on port 3000 (entrypoint.sh)
location /identities/api/v1/ { location /identities/api/v1/ {
rewrite ^/identities/api/v1/(.*)$ https://${host}/api/v1/$1 permanent; rewrite ^/identities/api/v1/(.*)$ /api/v1/$1 break;
proxy_pass https://localhost/;
proxy_redirect off;
proxy_set_header Host $host;
} }
location /identities { location /identities {

View File

@ -11,6 +11,109 @@ require("../db/photos").then(function(db) {
const router = express.Router(); const router = express.Router();
const addOrUpdateIdentity = async(id, {
displayName,
firstName,
lastName,
middleName
}, res) => {
if (displayName === undefined
|| firstName === undefined
|| lastName === undefined
|| middleName === undefined) {
res.status(400).send({ message: `Missing fields` });
return undefined;
}
const identity = {
displayName,
firstName,
lastName,
middleName,
id
};
if (id === -1 || !id) {
const [results, { lastId }] = await photoDB.sequelize.query(
`INSERT INTO identities ` +
'(displayName,firstName,lastName,middleName)' +
'VALUES(:displayName,:firstName,:lastName,:middleName)', {
replacements: identity
});
identity.id = lastId;
} else {
await photoDB.sequelize.query(
`UPDATE identities ` +
'SET ' +
'displayName=:displayName, ' +
'firstName=:firstName, ' +
'lastName=:lastName, ' +
'middleName=:middleName ' +
'WHERE id=:id', {
replacements: identity
});
}
return identity;
};
const populateRelatedFaces = async (identity, count) => {
let limit = '';
if (count) {
limit = ` LIMIT ${count} `;
}
/* If this is a new identity, no faces are being requested --
* just return the empty 'unknown face'.
*
* Otherwise, query the DB for 'count' faces */
if (count === undefined) {
identity.relatedFaces = await photoDB.sequelize.query(
"SELECT id AS faceId,photoId,faceConfidence " +
"FROM faces " +
"WHERE identityId=:identityId " +
limit, {
replacements: { identityId: identity.id },
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
} else {
identity.relatedFaces = [];
}
/* If there are no faces, return the 'unknown face' */
if (identity.relatedFaces.length === 0) {
identity.relatedFaces.push({
faceId: -1,
photoId: -1,
faceConfidence: 0
});
}
identity.relatedFaces.forEach(face => {
face.identityId = identity.id;
face.distance = face.faceConfidence;
face.descriptors = [];
delete face.faceConfidence;
});
}
router.post('/', async (req, res) => {
console.log(`POST ${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 identity = await addOrUpdateIdentity(-1, req.body, res);
if (!identity) {
return;
}
populateRelatedFaces(identity, 1);
return res.status(200).send(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) {
@ -23,45 +126,12 @@ router.put('/:id', async (req, res) => {
return res.status(400).send({ message: `Invalid identity id ${id}` }); return res.status(400).send({ message: `Invalid identity id ${id}` });
} }
const { const identity = await addOrUpdateIdentity(id, req.body, res);
displayName, if (!identity) {
firstName, return;
lastName,
middleName
} = req.body;
if (displayName === undefined
|| firstName === undefined
|| lastName === undefined
|| middleName === undefined) {
return res.status(400).send({ message: `Missing fields` });
} }
populateRelatedFaces(identity);
await photoDB.sequelize.query( return res.status(200).send(identity);
'UPDATE identities ' +
'SET ' +
'displayName=:displayName, ' +
'firstName=:firstName, ' +
'lastName=:lastName, ' +
'middleName=:middleName ' +
'WHERE id=:id', {
replacements: {
displayName,
firstName,
lastName,
middleName,
id
}
}
);
return res.status(200).json({
displayName,
firstName,
lastName,
middleName,
id
});
}); });
router.put("/faces/remove/:id", (req, res) => { router.put("/faces/remove/:id", (req, res) => {
@ -99,7 +169,7 @@ router.put("/faces/remove/:id", (req, res) => {
}); });
}); });
router.put("/faces/add/:id", (req, res) => { router.put("/faces/add/:id", async (req, res) => {
if (!req.user.maintainer) { if (!req.user.maintainer) {
console.warn(`${req.user.name} attempted to modify photos.`); console.warn(`${req.user.name} attempted to modify photos.`);
return res.status(401).send("Unauthorized to modify photos."); return res.status(401).send("Unauthorized to modify photos.");
@ -114,23 +184,24 @@ router.put("/faces/add/:id", (req, res) => {
return res.status(400).send("No faces supplied."); return res.status(400).send("No faces supplied.");
} }
return photoDB.sequelize.query( try {
"UPDATE faces SET identityId=:identityId " + await photoDB.sequelize.query(
"WHERE id IN (:faceIds)", { "UPDATE faces SET identityId=:identityId,classifiedBy='human' " +
replacements: { "WHERE id IN (:faceIds)", {
identityId: id, replacements: {
faceIds: req.body.faces identityId: id,
} faceIds: req.body.faces
}).then(() => { }
});
const identity = { const identity = {
id: id, id: id,
faces: req.body.faces faces: req.body.faces
}; };
return res.status(200).json([identity]); return res.status(200).json([identity]);
}).catch((error) => { } catch (error) {
console.error(error); console.error(error);
return res.status(500).send("Error processing request."); return res.status(500).send("Error processing request.");
}); };
}); });
router.post("/", (req, res) => { router.post("/", (req, res) => {
@ -192,6 +263,42 @@ function euclideanDistance(a, b) {
return Math.sqrt(sum); return Math.sqrt(sum);
} }
const getUnknownIdentity = async (faceCount) => {
const unknownIdentity = {
identityId: -1,
lastName: '',
firstName: '',
middleName: '',
displayName: 'Unknown',
descriptors: [],
relatedFaces: []
};
const limit = faceCount
? ` LIMIT ${faceCount} `
: ' ORDER BY faceConfidence DESC ';
unknownIdentity.relatedFaces = await photoDB.sequelize.query(
"SELECT id AS faceId,photoId,faceConfidence " +
"FROM faces WHERE identityId IS NULL AND classifiedBy != 'not-a-face' " +
limit, {
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
if (unknownIdentity.relatedFaces.length === 0) {
unknownIdentity.relatedFaces.push({
faceId: -1,
photoId: -1,
faceConfidence: 0
});
}
unknownIdentity.relatedFaces.forEach(face => {
face.identityId = -1;
face.distance = face.faceConfidence;
face.descriptors = [];
delete face.faceConfidence;
});
return unknownIdentity;
}
router.get("/:id?", async (req, res) => { router.get("/:id?", async (req, res) => {
console.log(`GET ${req.url}`); console.log(`GET ${req.url}`);
@ -204,6 +311,13 @@ router.get("/:id?", async (req, res) => {
} }
} }
/* If identityId requested is -1, this is the "Unknown" identity
* where all unmapped faces live. */
if (id === -1) {
const unknownIdentity = await getUnknownIdentity()
return res.status(200).json([ unknownIdentity ]);
}
const filter = id ? "WHERE identities.id=:id " : ""; const filter = id ? "WHERE identities.id=:id " : "";
const identities = await photoDB.sequelize.query("SELECT " + const identities = await photoDB.sequelize.query("SELECT " +
@ -212,14 +326,14 @@ router.get("/:id?", async (req, res) => {
"GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds," + "GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " + "GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " +
"FROM identities " + "FROM identities " +
"INNER JOIN faces ON identities.id=faces.identityId " + "LEFT JOIN faces ON identities.id=faces.identityId " +
filter + filter +
"GROUP BY identities.id", { "GROUP BY identities.id", {
replacements: { id }, replacements: { id },
type: photoDB.Sequelize.QueryTypes.SELECT, type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}); });
await Promise.map(identities, async (identity) => { await Promise.map(identities, async (identity) => {
[ 'firstName', 'middleName', 'lastName' ].forEach(key => { [ 'firstName', 'middleName', 'lastName' ].forEach(key => {
if (!identity[key]) { if (!identity[key]) {
@ -228,35 +342,49 @@ router.get("/:id?", async (req, res) => {
}); });
identity.identityId = identity.id; identity.identityId = identity.id;
const relatedFaces = identity.relatedFaceIds.split(","), if (!identity.relatedFaceIds) {
relatedFacePhotos = identity.relatedFacePhotoIds.split(","); identity.relatedFaces = [];
} else {
const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(",");
let descriptors = await photoDB.sequelize.query( let descriptors = await photoDB.sequelize.query(
`SELECT descriptors FROM facedescriptors WHERE id in (:ids)`, { `SELECT descriptors FROM facedescriptors WHERE id in (:ids)`, {
replacements: { replacements: {
ids: identity.relatedFaceDescriptorIds.split(',') ids: identity.relatedFaceDescriptorIds.split(',')
}, },
type: photoDB.Sequelize.QueryTypes.SELECT, type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
} }
);
descriptors = descriptors.map(entry => entry.descriptors);
identity.relatedFaces = relatedFaces.map((faceId, index) => {
const distance = euclideanDistance(
descriptors[index],
identity.descriptors
); );
descriptors = descriptors.map(entry => entry.descriptors);
return { identity.relatedFaces = relatedFaces.map((faceId, index) => {
const distance = euclideanDistance(
descriptors[index],
identity.descriptors
);
return {
identityId: identity.id,
faceId,
photoId: relatedFacePhotos[index],
distance
};
});
}
if (identity.relatedFaces.length === 0) {
identity.relatedFaces.push({
faceId: -1,
photoId: -1,
identityId: identity.id, identityId: identity.id,
faceId, distance: 0,
photoId: relatedFacePhotos[index], faceConfidence: 0
distance });
}; }
});
identity identity
.relatedFaces .relatedFaces
.sort((A, B) => { .sort((A, B) => {
@ -277,6 +405,14 @@ router.get("/:id?", async (req, res) => {
delete identity.relatedIdentityDescriptors; delete identity.relatedIdentityDescriptors;
}); });
/* If no ID was provided (so no 'filter') then this call is returning
* a list of all identities -- we create a fake identity for all
* unlabeled faces */
if (!filter) {
const unknownIdentity = await getUnknownIdentity(1)
identities.push(unknownIdentity);
}
return res.status(200).json(identities); return res.status(200).json(identities);
}); });