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;
}
.UnknownFace {
display: flex;
align-items: center;
font-size: 4rem;
font-weight: bold;
}
.IdentityForm {
display: grid;
grid-template-columns: 1fr 1fr;
@ -116,6 +123,8 @@ div {
box-sizing: border-box;
width: 8rem;
height: 8rem;
display: flex;
justify-content: center;
}
.Cluster {

View File

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

View File

@ -1,6 +1,9 @@
# DEVELOPMENT -- use npm development server on port 3000 (entrypoint.sh)
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 {

View File

@ -11,6 +11,109 @@ require("../db/photos").then(function(db) {
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) => {
console.log(`PUT ${req.url}`)
if (!req.user.maintainer) {
@ -23,45 +126,12 @@ router.put('/:id', async (req, res) => {
return res.status(400).send({ message: `Invalid identity id ${id}` });
}
const {
displayName,
firstName,
lastName,
middleName
} = req.body;
if (displayName === undefined
|| firstName === undefined
|| lastName === undefined
|| middleName === undefined) {
return res.status(400).send({ message: `Missing fields` });
const identity = await addOrUpdateIdentity(id, req.body, res);
if (!identity) {
return;
}
await photoDB.sequelize.query(
'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
});
populateRelatedFaces(identity);
return res.status(200).send(identity);
});
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) {
console.warn(`${req.user.name} attempted 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 photoDB.sequelize.query(
"UPDATE faces SET identityId=:identityId " +
try {
await photoDB.sequelize.query(
"UPDATE faces SET identityId=:identityId,classifiedBy='human' " +
"WHERE id IN (:faceIds)", {
replacements: {
identityId: id,
faceIds: req.body.faces
}
}).then(() => {
});
const identity = {
id: id,
faces: req.body.faces
};
return res.status(200).json([identity]);
}).catch((error) => {
} catch (error) {
console.error(error);
return res.status(500).send("Error processing request.");
});
};
});
router.post("/", (req, res) => {
@ -192,6 +263,42 @@ function euclideanDistance(a, b) {
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) => {
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 identities = await photoDB.sequelize.query("SELECT " +
@ -212,7 +326,7 @@ router.get("/:id?", async (req, res) => {
"GROUP_CONCAT(faces.descriptorId) AS relatedFaceDescriptorIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds " +
"FROM identities " +
"INNER JOIN faces ON identities.id=faces.identityId " +
"LEFT JOIN faces ON identities.id=faces.identityId " +
filter +
"GROUP BY identities.id", {
replacements: { id },
@ -228,6 +342,9 @@ router.get("/:id?", async (req, res) => {
});
identity.identityId = identity.id;
if (!identity.relatedFaceIds) {
identity.relatedFaces = [];
} else {
const relatedFaces = identity.relatedFaceIds.split(","),
relatedFacePhotos = identity.relatedFacePhotoIds.split(",");
@ -256,6 +373,17 @@ router.get("/:id?", async (req, res) => {
distance
};
});
}
if (identity.relatedFaces.length === 0) {
identity.relatedFaces.push({
faceId: -1,
photoId: -1,
identityId: identity.id,
distance: 0,
faceConfidence: 0
});
}
identity
.relatedFaces
@ -277,6 +405,14 @@ router.get("/:id?", async (req, res) => {
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);
});