mirror of https://github.com/vladmandic/human
updated facematch demo
parent
56d01dd3c7
commit
ae21e412a1
|
@ -21,12 +21,12 @@ const userConfig = {
|
||||||
detector: { rotation: true, return: true, maxDetected: 20 },
|
detector: { rotation: true, return: true, maxDetected: 20 },
|
||||||
mesh: { enabled: true },
|
mesh: { enabled: true },
|
||||||
embedding: { enabled: false },
|
embedding: { enabled: false },
|
||||||
iris: { enabled: false },
|
iris: { enabled: true },
|
||||||
emotion: { enabled: true },
|
emotion: { enabled: true },
|
||||||
description: { enabled: true },
|
description: { enabled: true },
|
||||||
},
|
},
|
||||||
hand: { enabled: false },
|
hand: { enabled: false },
|
||||||
gesture: { enabled: false },
|
gesture: { enabled: true },
|
||||||
body: { enabled: false },
|
body: { enabled: false },
|
||||||
filter: { enabled: true },
|
filter: { enabled: true },
|
||||||
segmentation: { enabled: false },
|
segmentation: { enabled: false },
|
||||||
|
@ -46,6 +46,10 @@ function log(...msg) {
|
||||||
console.log(ts, ...msg);
|
console.log(ts, ...msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function title(msg) {
|
||||||
|
document.getElementById('title').innerHTML = msg;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadFaceMatchDB() {
|
async function loadFaceMatchDB() {
|
||||||
// download db with known faces
|
// download db with known faces
|
||||||
try {
|
try {
|
||||||
|
@ -61,7 +65,9 @@ async function loadFaceMatchDB() {
|
||||||
async function SelectFaceCanvas(face) {
|
async function SelectFaceCanvas(face) {
|
||||||
// if we have face image tensor, enhance it and display it
|
// if we have face image tensor, enhance it and display it
|
||||||
let embedding;
|
let embedding;
|
||||||
|
document.getElementById('orig').style.filter = 'blur(16px)';
|
||||||
if (face.tensor) {
|
if (face.tensor) {
|
||||||
|
title('Sorting Faces by Similarity');
|
||||||
const enhanced = human.enhance(face);
|
const enhanced = human.enhance(face);
|
||||||
if (enhanced) {
|
if (enhanced) {
|
||||||
const c = document.getElementById('orig');
|
const c = document.getElementById('orig');
|
||||||
|
@ -78,9 +84,14 @@ async function SelectFaceCanvas(face) {
|
||||||
const arr = db.map((rec) => rec.embedding);
|
const arr = db.map((rec) => rec.embedding);
|
||||||
const res = await human.match(face.embedding, arr);
|
const res = await human.match(face.embedding, arr);
|
||||||
log('Match:', db[res.index].name);
|
log('Match:', db[res.index].name);
|
||||||
|
const emotion = face.emotion[0] ? `${Math.round(100 * face.emotion[0].score)}% ${face.emotion[0].emotion}` : 'N/A';
|
||||||
document.getElementById('desc').innerHTML = `
|
document.getElementById('desc').innerHTML = `
|
||||||
${face.fileName}<br>
|
source: ${face.fileName}<br>
|
||||||
Match: ${Math.round(1000 * res.similarity) / 10}% ${db[res.index].name}
|
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>
|
||||||
`;
|
`;
|
||||||
embedding = face.embedding.map((a) => parseFloat(a.toFixed(4)));
|
embedding = face.embedding.map((a) => parseFloat(a.toFixed(4)));
|
||||||
navigator.clipboard.writeText(`{"name":"unknown", "source":"${face.fileName}", "embedding":[${embedding}]},`);
|
navigator.clipboard.writeText(`{"name":"unknown", "source":"${face.fileName}", "embedding":[${embedding}]},`);
|
||||||
|
@ -93,9 +104,9 @@ async function SelectFaceCanvas(face) {
|
||||||
// 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];
|
||||||
const similarity = human.similarity(face.embedding, current.embedding);
|
const similarity = human.similarity(face.embedding, current.embedding);
|
||||||
|
canvas.tag.similarity = similarity;
|
||||||
// get best match
|
// get best match
|
||||||
// draw the canvas
|
// draw the canvas
|
||||||
canvas.title = similarity;
|
|
||||||
await human.tf.browser.toPixels(current.tensor, canvas);
|
await human.tf.browser.toPixels(current.tensor, canvas);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.font = 'small-caps 1rem "Lato"';
|
ctx.font = 'small-caps 1rem "Lato"';
|
||||||
|
@ -118,8 +129,10 @@ async function SelectFaceCanvas(face) {
|
||||||
// sort all faces by similarity
|
// sort all faces by similarity
|
||||||
const sorted = document.getElementById('faces');
|
const sorted = document.getElementById('faces');
|
||||||
[...sorted.children]
|
[...sorted.children]
|
||||||
.sort((a, b) => parseFloat(b.title) - parseFloat(a.title))
|
.sort((a, b) => parseFloat(b.tag.similarity) - parseFloat(a.tag.similarity))
|
||||||
.forEach((canvas) => sorted.appendChild(canvas));
|
.forEach((canvas) => sorted.appendChild(canvas));
|
||||||
|
document.getElementById('orig').style.filter = 'blur(0)';
|
||||||
|
title('Selected Face');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AddFaceCanvas(index, res, fileName) {
|
async function AddFaceCanvas(index, res, fileName) {
|
||||||
|
@ -134,6 +147,14 @@ async function AddFaceCanvas(index, res, fileName) {
|
||||||
canvas.width = 200;
|
canvas.width = 200;
|
||||||
canvas.height = 200;
|
canvas.height = 200;
|
||||||
canvas.className = 'face';
|
canvas.className = 'face';
|
||||||
|
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, ' ');
|
||||||
// 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, 'Source:', evt.target.tag.source, 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]);
|
||||||
|
@ -156,7 +177,9 @@ async function AddFaceCanvas(index, res, fileName) {
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AddImageElement(index, image) {
|
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`);
|
||||||
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
|
||||||
|
@ -188,12 +211,14 @@ async function main() {
|
||||||
// pre-load human models
|
// pre-load human models
|
||||||
await human.load();
|
await human.load();
|
||||||
|
|
||||||
|
title('Loading Face Match Database');
|
||||||
let images = [];
|
let images = [];
|
||||||
let dir = [];
|
let dir = [];
|
||||||
// load face descriptor database
|
// load face descriptor database
|
||||||
await loadFaceMatchDB();
|
await loadFaceMatchDB();
|
||||||
|
|
||||||
// enumerate all sample images in /assets
|
// enumerate all sample images in /assets
|
||||||
|
title('Enumerating Input Images');
|
||||||
const res = await fetch('/samples/in');
|
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'))));
|
||||||
|
@ -219,7 +244,7 @@ async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// download and analyze all images
|
// download and analyze all images
|
||||||
for (let i = 0; i < images.length; i++) await AddImageElement(i, images[i]);
|
for (let i = 0; i < images.length; i++) await AddImageElement(i, images[i], images.length);
|
||||||
|
|
||||||
// print stats
|
// print stats
|
||||||
const num = all.reduce((prev, cur) => prev += cur.length, 0);
|
const num = all.reduce((prev, cur) => prev += cur.length, 0);
|
||||||
|
@ -229,6 +254,7 @@ async function main() {
|
||||||
// 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 createFaceMatchDB();
|
if (!db || db.length === 0) await createFaceMatchDB();
|
||||||
|
|
||||||
|
title('');
|
||||||
log('Ready');
|
log('Ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,30 +14,36 @@
|
||||||
<link rel="apple-touch-icon" href="../../assets/icon.png">
|
<link rel="apple-touch-icon" href="../../assets/icon.png">
|
||||||
<script src="./facematch.js" type="module"></script>
|
<script src="./facematch.js" type="module"></script>
|
||||||
<style>
|
<style>
|
||||||
|
img { object-fit: contain; }
|
||||||
@font-face { font-family: 'Lato'; font-display: swap; font-style: normal; font-weight: 100; src: local('Lato'), url('../../assets/lato-light.woff2') }
|
@font-face { font-family: 'Lato'; font-display: swap; font-style: normal; font-weight: 100; src: local('Lato'), url('../../assets/lato-light.woff2') }
|
||||||
html { font-family: 'Lato', 'Segoe UI'; font-size: 24px; font-variant: small-caps; }
|
html { font-family: 'Lato', 'Segoe UI'; font-size: 24px; font-variant: small-caps; }
|
||||||
body { margin: 0; background: black; color: white; overflow-x: hidden; }
|
body { margin: 24px; background: black; color: white; overflow: hidden; text-align: -webkit-center; min-height: 100%; max-height: 100%; }
|
||||||
img { object-fit: contain; }
|
::-webkit-scrollbar { height: 8px; border: 0; border-radius: 0; }
|
||||||
.face { width: 128px; height: 128px; }
|
::-webkit-scrollbar-thumb { background: grey }
|
||||||
|
::-webkit-scrollbar-track { margin: 3px; }
|
||||||
|
.orig { width: 200px; height: 200px; padding-bottom: 20px; filter: blur(16px); transition : all 0.5s ease; }
|
||||||
|
.text { margin: 24px; }
|
||||||
|
.face { width: 128px; height: 128px; margin: 2px; padding: 2px; cursor: grab; transform: scale(1.00); transition : all 0.3s ease; }
|
||||||
|
.face:hover { filter: grayscale(1); transform: scale(1.08); transition : all 0.3s ease; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="display: block">
|
<div style="display: block">
|
||||||
<div style="display: flex">
|
<div style="display: flex">
|
||||||
<div>
|
<div style="min-width: 400px">
|
||||||
Selected Face<br>
|
<div class="text" id="title"></div>
|
||||||
<canvas id="orig" style="width: 200px; height: 200px; padding: 20px"></canvas>
|
<canvas id="orig" class="orig"></canvas>
|
||||||
<div id="desc" style="font-size: 0.8rem;"></div>
|
<div id="desc" style="font-size: 0.8rem; text-align: left;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 20px"></div>
|
<div style="width: 20px"></div>
|
||||||
<div>
|
<div>
|
||||||
Sample Images<br>
|
<div class="text">Input Images</div>
|
||||||
<div id="images" style="display: flex; flex-wrap: wrap; width: 85vw"></div>
|
<div id="images" style="display: flex; width: 60vw; overflow-x: auto; overflow-y: hidden; scroll-behavior: smooth"></div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div class="text">Select person to sort by similarity and get a known face match</div>
|
||||||
<div id="faces"></div>
|
<div id="faces" style="height: 50vh; overflow-y: auto"></div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
2706
test/build.log
2706
test/build.log
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue