human/demo/facematch/facematch.js

262 lines
11 KiB
JavaScript
Raw Normal View History

2021-05-25 14:58:20 +02:00
// @ts-nocheck // typescript checks disabled as this is pure javascript
/**
* Human demo for browsers
*
* Demo for face descriptor analysis and face simmilarity analysis
*/
2021-06-14 14:16:10 +02:00
import Human from '../../dist/human.esm.js';
2021-03-12 00:26:04 +01:00
const userConfig = {
backend: 'wasm',
async: false,
warmup: 'none',
2021-09-29 14:02:23 +02:00
cacheSimilarity: 0,
2021-03-12 00:26:04 +01:00
debug: true,
2021-06-14 14:16:10 +02:00
modelBasePath: '../../models/',
2021-08-31 19:00:06 +02:00
// wasmPath: 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm@3.9.0/dist/',
2021-03-12 00:26:04 +01:00
face: {
enabled: true,
2021-09-29 14:02:23 +02:00
detector: { rotation: true, return: true, maxDetected: 20 },
2021-03-12 00:26:04 +01:00
mesh: { enabled: true },
2021-04-13 17:05:52 +02:00
embedding: { enabled: false },
2021-10-01 17:40:57 +02:00
iris: { enabled: true },
emotion: { enabled: true },
2021-03-21 19:18:51 +01:00
description: { enabled: true },
2021-03-12 00:26:04 +01:00
},
hand: { enabled: false },
2021-10-01 17:40:57 +02:00
gesture: { enabled: true },
2021-03-12 00:26:04 +01:00
body: { enabled: false },
2021-06-05 23:51:46 +02:00
filter: { enabled: true },
segmentation: { enabled: false },
2021-03-12 00:26:04 +01:00
};
2021-03-12 18:54:08 +01:00
const human = new Human(userConfig); // new instance of human
const all = []; // array that will hold all detected faces
let db = []; // array that holds all known faces
2021-03-12 00:26:04 +01:00
2021-09-29 14:02:23 +02:00
const minScore = 0.4;
2021-03-24 16:08:49 +01:00
2021-03-12 00:26:04 +01:00
function log(...msg) {
const dt = new Date();
const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`;
// eslint-disable-next-line no-console
console.log(ts, ...msg);
}
2021-10-01 17:40:57 +02:00
function title(msg) {
document.getElementById('title').innerHTML = msg;
}
2021-09-29 14:02:23 +02:00
async function loadFaceMatchDB() {
2021-03-24 16:08:49 +01:00
// download db with known faces
try {
2021-06-14 14:16:10 +02:00
let res = await fetch('/demo/facematch/faces.json');
if (!res || !res.ok) res = await fetch('/human/demo/facematch/faces.json');
2021-03-24 16:08:49 +01:00
db = (res && res.ok) ? await res.json() : [];
2021-09-29 14:02:23 +02:00
log('Loaded Faces DB:', db);
2021-03-24 16:08:49 +01:00
} catch (err) {
log('Could not load faces database', err);
}
}
2021-09-29 14:02:23 +02:00
async function SelectFaceCanvas(face) {
2021-03-12 18:54:08 +01:00
// if we have face image tensor, enhance it and display it
2021-09-29 14:02:23 +02:00
let embedding;
2021-10-01 17:40:57 +02:00
document.getElementById('orig').style.filter = 'blur(16px)';
2021-03-12 18:54:08 +01:00
if (face.tensor) {
2021-10-01 17:40:57 +02:00
title('Sorting Faces by Similarity');
2021-03-12 18:54:08 +01:00
const enhanced = human.enhance(face);
if (enhanced) {
const c = document.getElementById('orig');
2021-09-29 14:02:23 +02:00
const squeeze = human.tf.squeeze(enhanced);
const normalize = human.tf.div(squeeze, 255);
await human.tf.browser.toPixels(normalize, c);
2021-07-29 22:06:03 +02:00
human.tf.dispose(enhanced);
human.tf.dispose(squeeze);
2021-09-29 14:02:23 +02:00
human.tf.dispose(normalize);
2021-03-21 19:18:51 +01:00
const ctx = c.getContext('2d');
ctx.font = 'small-caps 0.4rem "Lato"';
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
2021-03-12 18:54:08 +01:00
}
const arr = db.map((rec) => rec.embedding);
const res = await human.match(face.embedding, arr);
log('Match:', db[res.index].name);
2021-10-01 17:40:57 +02:00
const emotion = face.emotion[0] ? `${Math.round(100 * face.emotion[0].score)}% ${face.emotion[0].emotion}` : 'N/A';
2021-09-29 14:02:23 +02:00
document.getElementById('desc').innerHTML = `
2021-10-01 17:40:57 +02:00
source: ${face.fileName}<br>
match: ${Math.round(1000 * res.similarity) / 10}% ${db[res.index].name}<br>
score: ${Math.round(100 * face.boxScore)}% detection ${Math.round(100 * face.faceScore)}% analysis<br>
age: ${face.age} years<br>
gender: ${Math.round(100 * face.genderScore)}% ${face.gender}<br>
emotion: ${emotion}<br>
2021-09-29 14:02:23 +02:00
`;
embedding = face.embedding.map((a) => parseFloat(a.toFixed(4)));
navigator.clipboard.writeText(`{"name":"unknown", "source":"${face.fileName}", "embedding":[${embedding}]},`);
2021-03-12 18:54:08 +01:00
}
2021-03-12 04:04:44 +01:00
2021-03-12 18:54:08 +01:00
// loop through all canvases that contain faces
2021-03-12 00:26:04 +01:00
const canvases = document.getElementsByClassName('face');
2021-09-29 14:02:23 +02:00
let time = 0;
2021-03-12 00:26:04 +01:00
for (const canvas of canvases) {
2021-03-21 19:18:51 +01:00
// calculate similarity from selected face to current one in the loop
const current = all[canvas.tag.sample][canvas.tag.face];
const similarity = human.similarity(face.embedding, current.embedding);
2021-10-01 17:40:57 +02:00
canvas.tag.similarity = similarity;
// get best match
2021-03-23 20:24:58 +01:00
// draw the canvas
2021-03-21 19:18:51 +01:00
await human.tf.browser.toPixels(current.tensor, canvas);
2021-03-12 00:26:04 +01:00
const ctx = canvas.getContext('2d');
ctx.font = 'small-caps 1rem "Lato"';
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
2021-03-21 19:18:51 +01:00
ctx.fillText(`${(100 * similarity).toFixed(1)}%`, 3, 23);
2021-03-12 00:26:04 +01:00
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
2021-03-21 19:18:51 +01:00
ctx.fillText(`${(100 * similarity).toFixed(1)}%`, 4, 24);
ctx.font = 'small-caps 0.8rem "Lato"';
2021-06-01 14:59:09 +02:00
ctx.fillText(`${current.age}y ${(100 * (current.genderScore || 0)).toFixed(1)}% ${current.gender}`, 4, canvas.height - 6);
2021-03-23 20:24:58 +01:00
// identify person
2021-03-24 16:08:49 +01:00
ctx.font = 'small-caps 1rem "Lato"';
2021-09-29 14:02:23 +02:00
const start = performance.now();
const arr = db.map((rec) => rec.embedding);
const res = await human.match(face.embedding, arr);
2021-09-29 14:02:23 +02:00
time += (performance.now() - start);
if (res.similarity > minScore) ctx.fillText(`DB: ${(100 * res.similarity).toFixed(1)}% ${db[res.index].name}`, 4, canvas.height - 30);
2021-03-12 00:26:04 +01:00
}
2021-03-12 18:54:08 +01:00
2021-09-29 14:02:23 +02:00
log('Analyzed:', 'Face:', canvases.length, 'DB:', db.length, 'Time:', time);
2021-03-21 19:18:51 +01:00
// sort all faces by similarity
2021-03-12 00:26:04 +01:00
const sorted = document.getElementById('faces');
[...sorted.children]
2021-10-01 17:40:57 +02:00
.sort((a, b) => parseFloat(b.tag.similarity) - parseFloat(a.tag.similarity))
2021-03-12 00:26:04 +01:00
.forEach((canvas) => sorted.appendChild(canvas));
2021-10-01 17:40:57 +02:00
document.getElementById('orig').style.filter = 'blur(0)';
title('Selected Face');
2021-03-12 00:26:04 +01:00
}
2021-09-29 14:02:23 +02:00
async function AddFaceCanvas(index, res, fileName) {
2021-03-12 00:26:04 +01:00
all[index] = res.face;
2021-09-29 14:02:23 +02:00
let ok = false;
2021-03-12 00:26:04 +01:00
for (const i in res.face) {
2021-09-29 14:02:23 +02:00
if (res.face[i].mesh.length === 0) continue;
ok = true;
all[index][i].fileName = fileName;
2021-03-12 00:26:04 +01:00
const canvas = document.createElement('canvas');
2021-09-29 14:02:23 +02:00
canvas.tag = { sample: index, face: i, source: fileName };
2021-03-12 00:26:04 +01:00
canvas.width = 200;
canvas.height = 200;
canvas.className = 'face';
2021-10-01 17:40:57 +02:00
const emotion = res.face[i].emotion[0] ? `${Math.round(100 * res.face[i].emotion[0].score)}% ${res.face[i].emotion[0].emotion}` : 'N/A';
canvas.title = `
source: ${res.face[i].fileName}
score: ${Math.round(100 * res.face[i].boxScore)}% detection ${Math.round(100 * res.face[i].faceScore)}% analysis
age: ${res.face[i].age} years
gender: ${Math.round(100 * res.face[i].genderScore)}% ${res.face[i].gender}
emotion: ${emotion}
`.replace(/ /g, ' ');
2021-03-12 18:54:08 +01:00
// mouse click on any face canvas triggers analysis
2021-03-12 00:26:04 +01:00
canvas.addEventListener('click', (evt) => {
2021-09-29 14:02:23 +02:00
log('Select:', 'Image:', evt.target.tag.sample, 'Face:', evt.target.tag.face, 'Source:', evt.target.tag.source, all[evt.target.tag.sample][evt.target.tag.face]);
SelectFaceCanvas(all[evt.target.tag.sample][evt.target.tag.face]);
2021-03-12 00:26:04 +01:00
});
2021-03-12 18:54:08 +01:00
// if we actually got face image tensor, draw canvas with that face
if (res.face[i].tensor) {
2021-03-21 19:18:51 +01:00
await human.tf.browser.toPixels(res.face[i].tensor, canvas);
2021-03-12 18:54:08 +01:00
document.getElementById('faces').appendChild(canvas);
2021-03-21 19:18:51 +01:00
const ctx = canvas.getContext('2d');
ctx.font = 'small-caps 0.8rem "Lato"';
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
2021-06-01 14:59:09 +02:00
ctx.fillText(`${res.face[i].age}y ${(100 * (res.face[i].genderScore || 0)).toFixed(1)}% ${res.face[i].gender}`, 4, canvas.height - 6);
const arr = db.map((rec) => rec.embedding);
const result = await human.match(res.face[i].embedding, arr);
2021-03-24 16:08:49 +01:00
ctx.font = 'small-caps 1rem "Lato"';
if (result.similarity && res.similarity > minScore) ctx.fillText(`${(100 * result.similarity).toFixed(1)}% ${db[result.index].name}`, 4, canvas.height - 30);
2021-03-12 18:54:08 +01:00
}
2021-03-12 00:26:04 +01:00
}
2021-09-29 14:02:23 +02:00
return ok;
2021-03-12 00:26:04 +01:00
}
2021-10-01 17:40:57 +02:00
async function AddImageElement(index, image, length) {
const faces = all.reduce((prev, curr) => prev += curr.length, 0);
title(`Analyzing Input Images<br> ${Math.round(100 * index / length)}% [${index} / ${length}]<br>Found ${faces} Faces`);
2021-03-12 00:26:04 +01:00
return new Promise((resolve) => {
2021-03-13 17:26:53 +01:00
const img = new Image(128, 128);
2021-03-12 18:54:08 +01:00
img.onload = () => { // must wait until image is loaded
2021-06-14 14:16:10 +02:00
human.detect(img, userConfig).then(async (res) => {
2021-09-29 14:02:23 +02:00
const ok = await AddFaceCanvas(index, res, image); // then wait until image is analyzed
// log('Add image:', index + 1, image, 'faces:', res.face.length);
if (ok) document.getElementById('images').appendChild(img); // and finally we can add it
2021-03-13 17:26:53 +01:00
resolve(true);
});
2021-03-12 00:26:04 +01:00
};
2021-04-02 14:15:39 +02:00
img.onerror = () => {
log('Add image error:', index + 1, image);
resolve(false);
};
img.title = image;
img.src = encodeURI(image);
2021-03-12 00:26:04 +01:00
});
}
2021-09-29 14:02:23 +02:00
async function createFaceMatchDB() {
log('Creating Faces DB...');
for (const image of all) {
for (const face of image) db.push({ name: 'unknown', source: face.fileName, embedding: face.embedding });
}
log(db);
}
2021-03-12 00:26:04 +01:00
async function main() {
// pre-load human models
2021-03-12 00:26:04 +01:00
await human.load();
2021-10-01 17:40:57 +02:00
title('Loading Face Match Database');
2021-03-24 16:08:49 +01:00
let images = [];
let dir = [];
2021-04-02 14:37:35 +02:00
// load face descriptor database
2021-09-29 14:02:23 +02:00
await loadFaceMatchDB();
2021-04-02 14:37:35 +02:00
// enumerate all sample images in /assets
2021-10-01 17:40:57 +02:00
title('Enumerating Input Images');
2021-09-29 14:02:23 +02:00
const res = await fetch('/samples/in');
2021-03-24 16:08:49 +01:00
dir = (res && res.ok) ? await res.json() : [];
images = images.concat(dir.filter((img) => (img.endsWith('.jpg') && img.includes('sample'))));
2021-04-02 14:05:19 +02:00
// could not dynamically enumerate images so using static list
if (images.length === 0) {
images = [
2021-09-29 14:02:23 +02:00
'ai-body.jpg', 'ai-upper.jpg',
'person-carolina.jpg', 'person-celeste.jpg', 'person-leila1.jpg', 'person-leila2.jpg', 'person-lexi.jpg', 'person-linda.jpg', 'person-nicole.jpg', 'person-tasia.jpg',
'person-tetiana.jpg', 'person-vlado1.jpg', 'person-vlado5.jpg', 'person-vlado.jpg', 'person-christina.jpg', 'person-lauren.jpg',
'group-1.jpg', 'group-2.jpg', 'group-3.jpg', 'group-4.jpg', 'group-5.jpg', 'group-6.jpg', 'group-7.jpg',
'daz3d-brianna.jpg', 'daz3d-chiyo.jpg', 'daz3d-cody.jpg', 'daz3d-drew-01.jpg', 'daz3d-drew-02.jpg', 'daz3d-ella-01.jpg', 'daz3d-ella-02.jpg', 'daz3d-gillian.jpg',
'daz3d-hye-01.jpg', 'daz3d-hye-02.jpg', 'daz3d-kaia.jpg', 'daz3d-karen.jpg', 'daz3d-kiaria-01.jpg', 'daz3d-kiaria-02.jpg', 'daz3d-lilah-01.jpg', 'daz3d-lilah-02.jpg',
'daz3d-lilah-03.jpg', 'daz3d-lila.jpg', 'daz3d-lindsey.jpg', 'daz3d-megah.jpg', 'daz3d-selina-01.jpg', 'daz3d-selina-02.jpg', 'daz3d-snow.jpg',
'daz3d-sunshine.jpg', 'daz3d-taia.jpg', 'daz3d-tuesday-01.jpg', 'daz3d-tuesday-02.jpg', 'daz3d-tuesday-03.jpg', 'daz3d-zoe.jpg', 'daz3d-ginnifer.jpg',
'daz3d-_emotions01.jpg', 'daz3d-_emotions02.jpg', 'daz3d-_emotions03.jpg', 'daz3d-_emotions04.jpg', 'daz3d-_emotions05.jpg',
2021-04-02 14:05:19 +02:00
];
2021-04-02 14:37:35 +02:00
// add prefix for gitpages
2021-09-29 14:02:23 +02:00
images = images.map((a) => `/human/samples/in/${a}`);
log('Adding static image list:', images);
} else {
log('Discovered images:', images);
2021-04-02 14:05:19 +02:00
}
2021-04-02 14:09:06 +02:00
// download and analyze all images
2021-10-01 17:40:57 +02:00
for (let i = 0; i < images.length; i++) await AddImageElement(i, images[i], images.length);
2021-04-02 14:09:06 +02:00
// print stats
2021-03-13 17:26:53 +01:00
const num = all.reduce((prev, cur) => prev += cur.length, 0);
log('Extracted faces:', num, 'from images:', all.length);
log(human.tf.engine().memory());
// if we didn't download db, generate it from current faces
2021-09-29 14:02:23 +02:00
if (!db || db.length === 0) await createFaceMatchDB();
2021-10-01 17:40:57 +02:00
title('');
2021-03-12 04:04:44 +01:00
log('Ready');
2021-03-12 00:26:04 +01:00
}
window.onload = main;