mirror of https://github.com/vladmandic/human
distance based on minkowski space and limited euclidean space
parent
ee9d0d50b2
commit
ba1ba91b07
|
@ -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
24
TODO.md
|
@ -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*
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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
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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 }));
|
||||||
|
|
11
src/human.ts
11
src/human.ts
|
@ -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
2
wiki
|
@ -1 +1 @@
|
||||||
Subproject commit cef8c9cc9f09f8709cd4bd1daff817a59df4b4d1
|
Subproject commit ac5d01255f2f02de6b308b82976c5866c458f149
|
Loading…
Reference in New Issue