distance based on minkowski space and limited euclidean space

pull/91/head
Vladimir Mandic 2021-03-12 18:24:34 -05:00
parent ee9d0d50b2
commit ba1ba91b07
22 changed files with 6388 additions and 443430 deletions

View File

@ -9,6 +9,9 @@ Repository: **<git+https://github.com/vladmandic/human.git>**
## Changelog ## Changelog
### **HEAD -> main** 2021/03/12 mandic00@live.com
### **1.1.1** 2021/03/12 mandic00@live.com ### **1.1.1** 2021/03/12 mandic00@live.com
- switched face embedding to mobileface - switched face embedding to mobileface

24
TODO.md
View File

@ -1,7 +1,25 @@
# To-Do list for Human library # To-Do list for Human library
- Strong typing ## Big Ticket Items
- Automated testing
- Explore EfficientPose - Strong(er) typing
- Automated testing framework
## Explore Models
- EfficientPose
<https://github.com/daniegr/EfficientPose> <https://github.com/daniegr/EfficientPose>
<https://github.com/PINTO0309/PINTO_model_zoo/tree/main/084_EfficientPose> <https://github.com/PINTO0309/PINTO_model_zoo/tree/main/084_EfficientPose>
- ArcFace
- RetinaFace
- CenterFace
## WiP Items
- Embedding:
- Try average of flipped image
- Try with variable aspect ratio
## Issues
*N/A*

View File

@ -24,7 +24,7 @@ const userConfig = {
const human = new Human(userConfig); // new instance of human const human = new Human(userConfig); // new instance of human
const samples = ['../assets/sample-me.jpg', '../assets/sample6.jpg', '../assets/sample1.jpg', '../assets/sample4.jpg', '../assets/sample5.jpg', '../assets/sample3.jpg', '../assets/sample2.jpg']; // const samples = ['../assets/sample-me.jpg', '../assets/sample6.jpg', '../assets/sample1.jpg', '../assets/sample4.jpg', '../assets/sample5.jpg', '../assets/sample3.jpg', '../assets/sample2.jpg'];
// const samples = ['../assets/sample-me.jpg', '../assets/sample6.jpg', '../assets/sample1.jpg', '../assets/sample4.jpg', '../assets/sample5.jpg', '../assets/sample3.jpg', '../assets/sample2.jpg', // const samples = ['../assets/sample-me.jpg', '../assets/sample6.jpg', '../assets/sample1.jpg', '../assets/sample4.jpg', '../assets/sample5.jpg', '../assets/sample3.jpg', '../assets/sample2.jpg',
// '../private/me (1).jpg', '../private/me (2).jpg', '../private/me (3).jpg', '../private/me (4).jpg', '../private/me (5).jpg', '../private/me (6).jpg', '../private/me (7).jpg', '../private/me (8).jpg', // '../private/me (1).jpg', '../private/me (2).jpg', '../private/me (3).jpg', '../private/me (4).jpg', '../private/me (5).jpg', '../private/me (6).jpg', '../private/me (7).jpg', '../private/me (8).jpg',
// '../private/me (9).jpg', '../private/me (10).jpg', '../private/me (11).jpg', '../private/me (12).jpg', '../private/me (13).jpg']; // '../private/me (9).jpg', '../private/me (10).jpg', '../private/me (11).jpg', '../private/me (12).jpg', '../private/me (13).jpg'];
@ -57,7 +57,7 @@ async function analyze(face) {
const canvases = document.getElementsByClassName('face'); const canvases = document.getElementsByClassName('face');
for (const canvas of canvases) { for (const canvas of canvases) {
// calculate simmilarity from selected face to current one in the loop // calculate simmilarity from selected face to current one in the loop
const res = human.simmilarity(face.embedding, all[canvas.tag.sample][canvas.tag.face].embedding); const res = human.simmilarity(face.embedding, all[canvas.tag.sample][canvas.tag.face].embedding, 3);
// draw the canvas and simmilarity score // draw the canvas and simmilarity score
canvas.title = res; canvas.title = res;
await human.tf.browser.toPixels(all[canvas.tag.sample][canvas.tag.face].tensor, canvas); await human.tf.browser.toPixels(all[canvas.tag.sample][canvas.tag.face].tensor, canvas);
@ -98,8 +98,8 @@ async function faces(index, res) {
} }
} }
async function add(index) { async function add(index, image) {
log('Add image:', samples[index]); log('Add image:', index + 1, image);
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image(100, 100); const img = new Image(100, 100);
img.onload = () => { // must wait until image is loaded img.onload = () => { // must wait until image is loaded
@ -107,14 +107,27 @@ async function add(index) {
document.getElementById('images').appendChild(img); // and finally we can add it document.getElementById('images').appendChild(img); // and finally we can add it
resolve(true); resolve(true);
}; };
img.title = samples[index]; img.title = image;
img.src = samples[index]; img.src = encodeURI(image);
}); });
} }
async function main() { async function main() {
await human.load(); await human.load();
for (const i in samples) await add(i); // download and analyze all images // enumerate all sample images in /assets
let res = await fetch('/assets');
let dir = (res && res.ok) ? await res.json() : [];
let images = dir.filter((img) => (img.endsWith('.jpg') && img.includes('sample')));
// enumerate additional private test images in /private, not includded in git repository
res = await fetch('/private');
dir = (res && res.ok) ? await res.json() : [];
images = images.concat(dir.filter((img) => (img.endsWith('.jpg'))));
// download and analyze all images
log('Enumerated:', images.length, 'images');
for (const i in images) await add(i, images[i]);
log('Ready'); log('Ready');
} }

100014
dist/demo-browser-index.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

98996
dist/human.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

99002
dist/human.js vendored

File diff suppressed because one or more lines are too long

4
dist/human.js.map vendored

File diff suppressed because one or more lines are too long

25088
dist/human.node-gpu.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

25088
dist/human.node.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

76304
dist/tfjs.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -85,18 +85,22 @@ async function watch() {
// get file content for a valid url request // get file content for a valid url request
function handle(url) { function handle(url) {
return new Promise((resolve) => { return new Promise(async (resolve) => {
let obj = { ok: false }; let obj = { ok: false, file: decodeURI(url) };
obj.file = decodeURI(url); if (!fs.existsSync(obj.file)) {
if (!fs.existsSync(obj.file)) resolve(null); resolve(obj);
} else {
obj.stat = fs.statSync(obj.file); obj.stat = fs.statSync(obj.file);
if (obj.stat.isFile()) obj.ok = true; if (obj.stat.isFile()) obj.ok = true;
if (!obj.ok && obj.stat.isDirectory()) { if (!obj.ok && obj.stat.isDirectory()) {
obj.file = path.join(obj.file, options.default); if (fs.existsSync(path.join(obj.file, options.default))) {
// @ts-ignore obj = await handle(path.join(obj.file, options.default));
obj = handle(obj.file); } else {
obj.ok = obj.stat.isDirectory();
}
} }
resolve(obj); resolve(obj);
}
}); });
} }
@ -106,11 +110,12 @@ async function httpRequest(req, res) {
// get original ip of requestor, regardless if it's behind proxy or not // get original ip of requestor, regardless if it's behind proxy or not
const forwarded = (req.headers['forwarded'] || '').match(/for="\[(.*)\]:/); const forwarded = (req.headers['forwarded'] || '').match(/for="\[(.*)\]:/);
const ip = (Array.isArray(forwarded) ? forwarded[1] : null) || req.headers['x-forwarded-for'] || req.ip || req.socket.remoteAddress; const ip = (Array.isArray(forwarded) ? forwarded[1] : null) || req.headers['x-forwarded-for'] || req.ip || req.socket.remoteAddress;
if (!result || !result.ok) { if (!result || !result.ok || !result.stat) {
res.writeHead(404, { 'Content-Type': 'text/html' }); res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('Error 404: Not Found\n', 'utf-8'); res.end('Error 404: Not Found\n', 'utf-8');
log.warn(`${req.method}/${req.httpVersion}`, res.statusCode, req.url, ip); log.warn(`${req.method}/${req.httpVersion}`, res.statusCode, req.url, ip);
} else { } else {
if (result?.stat?.isFile()) {
const ext = String(path.extname(result.file)).toLowerCase(); const ext = String(path.extname(result.file)).toLowerCase();
const contentType = mime[ext] || 'application/octet-stream'; const contentType = mime[ext] || 'application/octet-stream';
const accept = req.headers['accept-encoding'] ? req.headers['accept-encoding'].includes('br') : false; // does target accept brotli compressed data const accept = req.headers['accept-encoding'] ? req.headers['accept-encoding'].includes('br') : false; // does target accept brotli compressed data
@ -134,6 +139,14 @@ async function httpRequest(req, res) {
// res.write(data); // res.write(data);
log.data(`${req.method}/${req.httpVersion}`, res.statusCode, contentType, result.stat.size, req.url, ip); log.data(`${req.method}/${req.httpVersion}`, res.statusCode, contentType, result.stat.size, req.url, ip);
} }
if (result?.stat?.isDirectory()) {
res.writeHead(200, { 'Content-Language': 'en', 'Content-Type': 'application/json; charset=utf-8', 'Last-Modified': result.stat.mtime, 'Cache-Control': 'no-cache', 'X-Content-Type-Options': 'nosniff' });
let dir = fs.readdirSync(result.file);
dir = dir.map((f) => '/' + path.join(path.basename(result.file), f));
res.end(JSON.stringify(dir), 'utf-8');
log.data(`${req.method}/${req.httpVersion}`, res.statusCode, 'directory/json', result.stat.size, req.url, ip);
}
}
}); });
} }

View File

@ -32,26 +32,15 @@ export class MediaPipeFaceMesh {
const box = prediction.box ? [ const box = prediction.box ? [
Math.max(0, prediction.box.startPoint[0]), Math.max(0, prediction.box.startPoint[0]),
Math.max(0, prediction.box.startPoint[1]), Math.max(0, prediction.box.startPoint[1]),
Math.min(input.shape[2], prediction.box.endPoint[0]) - prediction.box.startPoint[0], Math.min(input.shape[1], prediction.box.endPoint[0]) - prediction.box.startPoint[0],
Math.min(input.shape[1], prediction.box.endPoint[1]) - prediction.box.startPoint[1], Math.min(input.shape[2], prediction.box.endPoint[1]) - prediction.box.startPoint[1],
] : 0; ] : 0;
const boxRaw = prediction.box ? [ const boxRaw = prediction.box ? [
Math.max(0, prediction.box.startPoint[0] / input.shape[2]), Math.max(0, prediction.box.startPoint[0] / input.shape[2]),
Math.max(0, prediction.box.startPoint[1] / input.shape[1]), Math.max(0, prediction.box.startPoint[1] / input.shape[1]),
Math.min(input.shape[2], (prediction.box.endPoint[0]) - prediction.box.startPoint[0]) / input.shape[2], Math.min(input.shape[1], (prediction.box.endPoint[0]) - prediction.box.startPoint[0]) / input.shape[2],
Math.min(input.shape[1], (prediction.box.endPoint[1]) - prediction.box.startPoint[1]) / input.shape[1], Math.min(input.shape[2], (prediction.box.endPoint[1]) - prediction.box.startPoint[1]) / input.shape[1],
] : []; ] : [];
let offsetRaw = <any>[];
if (meshRaw.length > 0 && boxRaw.length > 0) {
const dimX = meshRaw.map((pt) => pt[0]);
const dimY = meshRaw.map((pt) => pt[1]);
offsetRaw = [
Math.max(0, 0 + Math.min(...dimY) - boxRaw[0]), // distance of detected face border to box top edge
Math.max(0, 0 + Math.min(...dimX) - boxRaw[1]), // distance of detected face border to box left edge
Math.min(1, 1 - Math.max(...dimY) + boxRaw[2]), // distance of detected face border to box bottom edge
Math.min(1, 1 - Math.max(...dimX) + boxRaw[3]), // distance of detected face border to box right edge
];
}
results.push({ results.push({
confidence: prediction.faceConfidence || prediction.boxConfidence || 0, confidence: prediction.faceConfidence || prediction.boxConfidence || 0,
boxConfidence: prediction.boxConfidence, boxConfidence: prediction.boxConfidence,
@ -60,7 +49,6 @@ export class MediaPipeFaceMesh {
mesh, mesh,
boxRaw, boxRaw,
meshRaw, meshRaw,
offsetRaw,
annotations, annotations,
image: prediction.image ? tf.clone(prediction.image) : null, image: prediction.image ? tf.clone(prediction.image) : null,
}); });

View File

@ -16,13 +16,12 @@ export function simmilarity(embedding1, embedding2, order = 2) {
if (!embedding1 || !embedding2) return 0; if (!embedding1 || !embedding2) return 0;
if (embedding1?.length === 0 || embedding2?.length === 0) return 0; if (embedding1?.length === 0 || embedding2?.length === 0) return 0;
if (embedding1?.length !== embedding2?.length) return 0; if (embedding1?.length !== embedding2?.length) return 0;
// general minkowski distance // general minkowski distance, euclidean distance is limited case where order is 2
// euclidean distance is limited case where order is 2
const distance = embedding1 const distance = embedding1
.map((val, i) => (Math.abs(embedding1[i] - embedding2[i]) ** order)) // distance squared .map((val, i) => (Math.abs(embedding1[i] - embedding2[i]) ** order)) // distance squared
.reduce((sum, now) => (sum + now), 0) // sum all distances .reduce((sum, now) => (sum + now), 0) // sum all distances
** (1 / order); // get root of ** (1 / order); // get root of
const res = Math.max(Math.trunc(1000 * (1 - (1 * distance))) / 1000, 0); const res = Math.max(Math.trunc(1000 * (1 - distance)) / 1000, 0);
return res; return res;
} }
@ -33,14 +32,10 @@ export function enhance(input) {
// const data = tf.image.resizeBilinear(input, [model.inputs[0].shape[2], model.inputs[0].shape[1]], false); // just resize to fit the embedding model // const data = tf.image.resizeBilinear(input, [model.inputs[0].shape[2], model.inputs[0].shape[1]], false); // just resize to fit the embedding model
// do a tight crop of image and resize it to fit the model // do a tight crop of image and resize it to fit the model
// maybe offsets are already prepared by face model, if not use empirical values const box = [[0.05, 0.15, 0.85, 0.85]]; // empyrical values for top, left, bottom, right
const box = input.offsetRaw
? [input.offsetRaw] // crop based on face mesh borders
: [[0.05, 0.15, 0.85, 0.85]]; // fixed crop for top, left, bottom, right
console.log('BOX', box[0]);
const tensor = input.image || input.tensor; const tensor = input.image || input.tensor;
const crop = tensor.shape.length === 3 const crop = (tensor.shape.length === 3)
? tf.image.cropAndResize(tensor.expandDims(0), box, [0], [model.inputs[0].shape[2], model.inputs[0].shape[1]]) // add batch if missing ? tf.image.cropAndResize(tensor.expandDims(0), box, [0], [model.inputs[0].shape[2], model.inputs[0].shape[1]]) // add batch dimension if missing
: tf.image.cropAndResize(tensor, box, [0], [model.inputs[0].shape[2], model.inputs[0].shape[1]]); : tf.image.cropAndResize(tensor, box, [0], [model.inputs[0].shape[2], model.inputs[0].shape[1]]);
// convert to black&white to avoid colorization impact // convert to black&white to avoid colorization impact
@ -77,9 +72,9 @@ export async function predict(input, config) {
const scale = res.div(l2); const scale = res.div(l2);
return scale; return scale;
}); });
tf.dispose(scaled);
*/ */
data = res.dataSync(); data = res.dataSync();
// tf.dispose(scaled);
tf.dispose(res); tf.dispose(res);
} else { } else {
const profileData = await tf.profile(() => model.predict({ img_inputs: image })); const profileData = await tf.profile(() => model.predict({ img_inputs: image }));

View File

@ -394,17 +394,6 @@ class Human {
// combine results // combine results
faceRes.push({ faceRes.push({
...face, ...face,
/*
confidence: face.confidence,
faceConfidence: face.faceConfidence,
boxConfidence: face.boxConfidence,
box: face.box,
mesh: face.mesh,
boxRaw: face.boxRaw,
meshRaw: face.meshRaw,
offsetRaw: face.offsetRaw,
annotations: face.annotations,
*/
age: ageRes.age, age: ageRes.age,
gender: genderRes.gender, gender: genderRes.gender,
genderConfidence: genderRes.confidence, genderConfidence: genderRes.confidence,

2
wiki

@ -1 +1 @@
Subproject commit cef8c9cc9f09f8709cd4bd1daff817a59df4b4d1 Subproject commit ac5d01255f2f02de6b308b82976c5866c458f149