update facematch demo

pull/193/head
Vladimir Mandic 2021-09-29 08:02:23 -04:00
parent 533bbf9286
commit 14225c89f9
5 changed files with 2028 additions and 162 deletions

View File

@ -12,12 +12,13 @@ const userConfig = {
backend: 'wasm', backend: 'wasm',
async: false, async: false,
warmup: 'none', warmup: 'none',
cacheSimilarity: 0,
debug: true, debug: true,
modelBasePath: '../../models/', modelBasePath: '../../models/',
// wasmPath: 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm@3.9.0/dist/', // wasmPath: 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm@3.9.0/dist/',
face: { face: {
enabled: true, enabled: true,
detector: { rotation: true, return: true }, detector: { rotation: true, return: true, maxDetected: 20 },
mesh: { enabled: true }, mesh: { enabled: true },
embedding: { enabled: false }, embedding: { enabled: false },
iris: { enabled: false }, iris: { enabled: false },
@ -36,8 +37,7 @@ const human = new Human(userConfig); // new instance of human
const all = []; // array that will hold all detected faces const all = []; // array that will hold all detected faces
let db = []; // array that holds all known faces let db = []; // array that holds all known faces
const minScore = 0.6; const minScore = 0.4;
const minConfidence = 0.8;
function log(...msg) { function log(...msg) {
const dt = new Date(); const dt = new Date();
@ -46,45 +46,48 @@ function log(...msg) {
console.log(ts, ...msg); console.log(ts, ...msg);
} }
async function getFaceDB() { async function loadFaceMatchDB() {
// download db with known faces // download db with known faces
try { try {
let res = await fetch('/demo/facematch/faces.json'); let res = await fetch('/demo/facematch/faces.json');
if (!res || !res.ok) res = await fetch('/human/demo/facematch/faces.json'); if (!res || !res.ok) res = await fetch('/human/demo/facematch/faces.json');
db = (res && res.ok) ? await res.json() : []; db = (res && res.ok) ? await res.json() : [];
for (const rec of db) { log('Loaded Faces DB:', db);
rec.embedding = rec.embedding.map((a) => parseFloat(a.toFixed(4)));
}
} catch (err) { } catch (err) {
log('Could not load faces database', err); log('Could not load faces database', err);
} }
} }
async function analyze(face) { async function SelectFaceCanvas(face) {
// refresh faces database
await getFaceDB();
// if we have face image tensor, enhance it and display it // if we have face image tensor, enhance it and display it
let embedding;
if (face.tensor) { if (face.tensor) {
const enhanced = human.enhance(face); const enhanced = human.enhance(face);
const desc = document.getElementById('desc');
desc.innerText = `{"name":"unknown", "source":"${face.fileName}", "embedding":[${face.embedding}]},`;
const embedding = face.embedding.map((a) => parseFloat(a.toFixed(4)));
navigator.clipboard.writeText(`{"name":"unknown", "source":"${face.fileName}", "embedding":[${embedding}]},`);
if (enhanced) { if (enhanced) {
const c = document.getElementById('orig'); const c = document.getElementById('orig');
const squeeze = human.tf.div(human.tf.squeeze(enhanced), 255); const squeeze = human.tf.squeeze(enhanced);
await human.tf.browser.toPixels(squeeze, c); const normalize = human.tf.div(squeeze, 255);
await human.tf.browser.toPixels(normalize, c);
human.tf.dispose(enhanced); human.tf.dispose(enhanced);
human.tf.dispose(squeeze); human.tf.dispose(squeeze);
human.tf.dispose(normalize);
const ctx = c.getContext('2d'); const ctx = c.getContext('2d');
ctx.font = 'small-caps 0.4rem "Lato"'; ctx.font = 'small-caps 0.4rem "Lato"';
ctx.fillStyle = 'rgba(255, 255, 255, 1)'; ctx.fillStyle = 'rgba(255, 255, 255, 1)';
} }
const person = await human.match(face.embedding, db);
log('Match:', person);
document.getElementById('desc').innerHTML = `
${face.fileName}<br>
Match: ${Math.round(1000 * person.similarity) / 10}% ${person.name}
`;
embedding = face.embedding.map((a) => parseFloat(a.toFixed(4)));
navigator.clipboard.writeText(`{"name":"unknown", "source":"${face.fileName}", "embedding":[${embedding}]},`);
} }
// loop through all canvases that contain faces // loop through all canvases that contain faces
const canvases = document.getElementsByClassName('face'); const canvases = document.getElementsByClassName('face');
let time = 0;
for (const canvas of canvases) { for (const canvas of canvases) {
// calculate similarity from selected face to current one in the loop // calculate similarity from selected face to current one in the loop
const current = all[canvas.tag.sample][canvas.tag.face]; const current = all[canvas.tag.sample][canvas.tag.face];
@ -103,10 +106,13 @@ async function analyze(face) {
ctx.fillText(`${current.age}y ${(100 * (current.genderScore || 0)).toFixed(1)}% ${current.gender}`, 4, canvas.height - 6); ctx.fillText(`${current.age}y ${(100 * (current.genderScore || 0)).toFixed(1)}% ${current.gender}`, 4, canvas.height - 6);
// identify person // identify person
ctx.font = 'small-caps 1rem "Lato"'; ctx.font = 'small-caps 1rem "Lato"';
const start = performance.now();
const person = await human.match(current.embedding, db); const person = await human.match(current.embedding, db);
if (person.similarity && person.similarity > minScore && current.confidence > minConfidence) ctx.fillText(`${(100 * person.similarity).toFixed(1)}% ${person.name}`, 4, canvas.height - 30); time += (performance.now() - start);
if (person.similarity && person.similarity > minScore) ctx.fillText(`DB: ${(100 * person.similarity).toFixed(1)}% ${person.name}`, 4, canvas.height - 30);
} }
log('Analyzed:', 'Face:', canvases.length, 'DB:', db.length, 'Time:', time);
// sort all faces by similarity // sort all faces by similarity
const sorted = document.getElementById('faces'); const sorted = document.getElementById('faces');
[...sorted.children] [...sorted.children]
@ -114,19 +120,22 @@ async function analyze(face) {
.forEach((canvas) => sorted.appendChild(canvas)); .forEach((canvas) => sorted.appendChild(canvas));
} }
async function faces(index, res, fileName) { async function AddFaceCanvas(index, res, fileName) {
all[index] = res.face; all[index] = res.face;
let ok = false;
for (const i in res.face) { for (const i in res.face) {
if (res.face[i].mesh.length === 0) continue;
ok = true;
all[index][i].fileName = fileName; all[index][i].fileName = fileName;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.tag = { sample: index, face: i }; canvas.tag = { sample: index, face: i, source: fileName };
canvas.width = 200; canvas.width = 200;
canvas.height = 200; canvas.height = 200;
canvas.className = 'face'; canvas.className = 'face';
// mouse click on any face canvas triggers analysis // mouse click on any face canvas triggers analysis
canvas.addEventListener('click', (evt) => { canvas.addEventListener('click', (evt) => {
log('Select:', 'Image:', evt.target.tag.sample, 'Face:', evt.target.tag.face, all[evt.target.tag.sample][evt.target.tag.face]); 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]);
analyze(all[evt.target.tag.sample][evt.target.tag.face]); SelectFaceCanvas(all[evt.target.tag.sample][evt.target.tag.face]);
}); });
// if we actually got face image tensor, draw canvas with that face // if we actually got face image tensor, draw canvas with that face
if (res.face[i].tensor) { if (res.face[i].tensor) {
@ -138,19 +147,20 @@ async function faces(index, res, fileName) {
ctx.fillText(`${res.face[i].age}y ${(100 * (res.face[i].genderScore || 0)).toFixed(1)}% ${res.face[i].gender}`, 4, canvas.height - 6); ctx.fillText(`${res.face[i].age}y ${(100 * (res.face[i].genderScore || 0)).toFixed(1)}% ${res.face[i].gender}`, 4, canvas.height - 6);
const person = await human.match(res.face[i].embedding, db); const person = await human.match(res.face[i].embedding, db);
ctx.font = 'small-caps 1rem "Lato"'; ctx.font = 'small-caps 1rem "Lato"';
if (person.similarity && person.similarity > minScore && res.face[i].confidence > minConfidence) ctx.fillText(`${(100 * person.similarity).toFixed(1)}% ${person.name}`, 4, canvas.height - 30); if (person.similarity && person.similarity > minScore) ctx.fillText(`${(100 * person.similarity).toFixed(1)}% ${person.name}`, 4, canvas.height - 30);
} }
} }
return ok;
} }
async function process(index, image) { async function AddImageElement(index, image) {
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image(128, 128); const img = new Image(128, 128);
img.onload = () => { // must wait until image is loaded img.onload = () => { // must wait until image is loaded
human.detect(img, userConfig).then(async (res) => { human.detect(img, userConfig).then(async (res) => {
await faces(index, res, image); // then wait until image is analyzed const ok = await AddFaceCanvas(index, res, image); // then wait until image is analyzed
log('Add image:', index + 1, image, 'faces:', res.face.length); // log('Add image:', index + 1, image, 'faces:', res.face.length);
document.getElementById('images').appendChild(img); // and finally we can add it if (ok) document.getElementById('images').appendChild(img); // and finally we can add it
resolve(true); resolve(true);
}); });
}; };
@ -163,7 +173,7 @@ async function process(index, image) {
}); });
} }
async function createDB() { async function createFaceMatchDB() {
log('Creating Faces DB...'); log('Creating Faces DB...');
for (const image of all) { for (const image of all) {
for (const face of image) db.push({ name: 'unknown', source: face.fileName, embedding: face.embedding }); for (const face of image) db.push({ name: 'unknown', source: face.fileName, embedding: face.embedding });
@ -172,53 +182,41 @@ async function createDB() {
} }
async function main() { async function main() {
/*
window.addEventListener('unhandledrejection', (evt) => {
// eslint-disable-next-line no-console
console.error(evt.reason || evt);
document.getElementById('list').innerHTML = evt?.reason?.message || evt?.reason || evt;
evt.preventDefault();
});
*/
// pre-load human models // pre-load human models
await human.load(); await human.load();
let images = []; let images = [];
let dir = []; let dir = [];
// load face descriptor database // load face descriptor database
await getFaceDB(); await loadFaceMatchDB();
// enumerate all sample images in /assets // enumerate all sample images in /assets
const res = await fetch('/samples/groups'); const res = await fetch('/samples/in');
dir = (res && res.ok) ? await res.json() : []; dir = (res && res.ok) ? await res.json() : [];
images = images.concat(dir.filter((img) => (img.endsWith('.jpg') && img.includes('sample')))); images = images.concat(dir.filter((img) => (img.endsWith('.jpg') && img.includes('sample'))));
// could not dynamically enumerate images so using static list // could not dynamically enumerate images so using static list
if (images.length === 0) { if (images.length === 0) {
images = [ images = [
'groups/group1.jpg', 'ai-body.jpg', 'ai-upper.jpg',
'groups/group2.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',
'groups/group3.jpg', 'person-tetiana.jpg', 'person-vlado1.jpg', 'person-vlado5.jpg', 'person-vlado.jpg', 'person-christina.jpg', 'person-lauren.jpg',
'groups/group4.jpg', 'group-1.jpg', 'group-2.jpg', 'group-3.jpg', 'group-4.jpg', 'group-5.jpg', 'group-6.jpg', 'group-7.jpg',
'groups/group5.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',
'groups/group6.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',
'groups/group7.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',
'groups/group8.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',
'groups/group9.jpg', 'daz3d-_emotions01.jpg', 'daz3d-_emotions02.jpg', 'daz3d-_emotions03.jpg', 'daz3d-_emotions04.jpg', 'daz3d-_emotions05.jpg',
'groups/group10.jpg',
'groups/group11.jpg',
'groups/group12.jpg',
'groups/group13.jpg',
'groups/group14.jpg',
]; ];
// add prefix for gitpages // add prefix for gitpages
images = images.map((a) => `/human/samples/${a}`); images = images.map((a) => `/human/samples/in/${a}`);
log('Adding static image list:', images.length, 'images'); log('Adding static image list:', images);
} else {
log('Disoovered images:', images);
} }
// download and analyze all images // download and analyze all images
for (let i = 0; i < images.length; i++) await process(i, images[i]); for (let i = 0; i < images.length; i++) await AddImageElement(i, images[i]);
// print stats // print stats
const num = all.reduce((prev, cur) => prev += cur.length, 0); const num = all.reduce((prev, cur) => prev += cur.length, 0);
@ -226,8 +224,7 @@ async function main() {
log(human.tf.engine().memory()); log(human.tf.engine().memory());
// if we didn't download db, generate it from current faces // if we didn't download db, generate it from current faces
if (!db || db.length === 0) await createDB(); if (!db || db.length === 0) await createFaceMatchDB();
else log('Loaded Faces DB:', db.length);
log('Ready'); log('Ready');
} }

File diff suppressed because one or more lines are too long

View File

@ -27,13 +27,13 @@
<div> <div>
Selected Face<br> Selected Face<br>
<canvas id="orig" style="width: 200px; height: 200px; padding: 20px"></canvas> <canvas id="orig" style="width: 200px; height: 200px; padding: 20px"></canvas>
<div id="desc" style="font-size: 0.8rem;"></div>
</div> </div>
<div style="width: 20px"></div> <div style="width: 20px"></div>
<div> <div>
Sample Images<br> Sample Images<br>
<div id="images" style="display: flex; flex-wrap: wrap; width: 85vw"></div> <div id="images" style="display: flex; flex-wrap: wrap; width: 85vw"></div>
</div> </div>
<span id="desc" style="visibility: hidden; font-size: 0.4rem;"></span><br>
</div> </div>
<div id="list" style="height: 10px"></div> <div id="list" style="height: 10px"></div>
Extracted Faces - click on a face to sort by similarity and get a known face match:<br> Extracted Faces - click on a face to sort by similarity and get a known face match:<br>

View File

@ -43,7 +43,7 @@
const samples = [ const samples = [
'ai-body.jpg', 'ai-upper.jpg', '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-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', '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', '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-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-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',

File diff suppressed because it is too large Load Diff