increase face similarity match resolution

pull/356/head
Vladimir Mandic 2021-10-25 09:44:13 -04:00
parent 12ef0a846b
commit 2bd59f1276
6 changed files with 93 additions and 19 deletions

View File

@ -9,8 +9,10 @@
## Changelog
### **HEAD -> main** 2021/10/22 mandic00@live.com
### **HEAD -> main** 2021/10/23 mandic00@live.com
- time based caching
- turn on minification
- initial work on skiptime
- added generic types
- enhanced typing exports

View File

@ -0,0 +1,68 @@
/**
* Human Person Similarity test for NodeJS
*/
const log = require('@vladmandic/pilogger');
const fs = require('fs');
const process = require('process');
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const tf = require('@tensorflow/tfjs-node');
const Human = require('../../dist/human.node.js').default;
let human = null;
const myConfig = {
backend: 'tensorflow',
modelBasePath: 'file://models/',
debug: true,
face: { emotion: { enabled: false } },
body: { enabled: false },
hand: { enabled: false },
gesture: { enabled: false },
};
async function init() {
human = new Human(myConfig);
await human.tf.ready();
log.info('Human:', human.version);
await human.load();
const loaded = Object.keys(human.models).filter((a) => human.models[a]);
log.info('Loaded:', loaded);
log.info('Memory state:', human.tf.engine().memory());
}
async function detect(input) {
if (!fs.existsSync(input)) {
log.error('Cannot load image:', input);
process.exit(1);
}
const buffer = fs.readFileSync(input);
const decode = human.tf.node.decodeImage(buffer, 3);
const expand = human.tf.expandDims(decode, 0);
const tensor = human.tf.cast(expand, 'float32');
log.state('Loaded image:', input, tensor['shape']);
const result = await human.detect(tensor, myConfig);
human.tf.dispose([tensor, decode, expand]);
log.state('Detected faces:', result.face.length);
return result;
}
async function main() {
log.configure({ inspect: { breakLength: 265 } });
log.header();
if (process.argv.length !== 4) {
log.error('Parameters: <first image> <second image> missing');
process.exit(1);
}
await init();
const res1 = await detect(process.argv[2]);
const res2 = await detect(process.argv[3]);
if (!res1 || !res1.face || res1.face.length === 0 || !res2 || !res2.face || res2.face.length === 0) {
log.error('Could not detect face descriptors');
process.exit(1);
}
const similarity = human.similarity(res1.face[0].embedding, res2.face[0].embedding, { order: 2 });
log.data('Similarity: ', similarity);
}
main();

View File

@ -66,15 +66,15 @@
"@tensorflow/tfjs-layers": "^3.10.0",
"@tensorflow/tfjs-node": "^3.10.0",
"@tensorflow/tfjs-node-gpu": "^3.10.0",
"@types/node": "^16.11.3",
"@types/node": "^16.11.4",
"@typescript-eslint/eslint-plugin": "^5.1.0",
"@typescript-eslint/parser": "^5.1.0",
"@vladmandic/build": "^0.6.2",
"@vladmandic/build": "^0.6.3",
"@vladmandic/pilogger": "^0.3.3",
"canvas": "^2.8.0",
"dayjs": "^1.10.7",
"esbuild": "^0.13.8",
"eslint": "8.0.1",
"esbuild": "^0.13.9",
"eslint": "8.1.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-json": "^3.1.0",
@ -84,7 +84,7 @@
"rimraf": "^3.0.2",
"seedrandom": "^3.0.5",
"tslib": "^2.3.1",
"typedoc": "0.22.6",
"typedoc": "0.22.7",
"typescript": "4.4.4"
},
"dependencies": {

View File

@ -7,25 +7,27 @@ export type Descriptor = Array<number>
*
* Options:
* - `order`
* - `multiplier` factor by how much to enhance difference analysis in range of 1..100
*
* Note: No checks are performed for performance reasons so make sure to pass valid number arrays of equal length
*/
export function distance(descriptor1: Descriptor, descriptor2: Descriptor, options = { order: 2 }) {
export function distance(descriptor1: Descriptor, descriptor2: Descriptor, options = { order: 2, multiplier: 20 }) {
// general minkowski distance, euclidean distance is limited case where order is 2
let sum = 0;
for (let i = 0; i < descriptor1.length; i++) {
const diff = (options.order === 2) ? (descriptor1[i] - descriptor2[i]) : (Math.abs(descriptor1[i] - descriptor2[i]));
sum += (options.order === 2) ? (diff * diff) : (diff ** options.order);
}
return sum;
return (options.multiplier || 20) * sum;
}
/** Calculates normalized similarity between two descriptors based on their `distance`
*/
export function similarity(descriptor1: Descriptor, descriptor2: Descriptor, options = { order: 2 }) {
export function similarity(descriptor1: Descriptor, descriptor2: Descriptor, options = { order: 2, multiplier: 20 }) {
const dist = distance(descriptor1, descriptor2, options);
const invert = (options.order === 2) ? Math.sqrt(dist) : dist ** (1 / options.order);
return Math.max(0, 100 - invert) / 100.0;
const root = (options.order === 2) ? Math.sqrt(dist) : dist ** (1 / options.order);
const invert = Math.max(0, 100 - root) / 100.0;
return invert;
}
/** Matches given descriptor to a closest entry in array of descriptors
@ -33,22 +35,23 @@ export function similarity(descriptor1: Descriptor, descriptor2: Descriptor, opt
* @param descriptors array of face descriptors to commpare given descriptor to
*
* Options:
* - `order` see {@link distance} method
* - `threshold` match will return result first result for which {@link distance} is below `threshold` even if there may be better results
* - `order` see {@link distance} method
* - `multiplier` see {@link distance} method
*
* @returns object with index, distance and similarity
* - `index` index array index where best match was found or -1 if no matches
* - {@link distance} calculated `distance` of given descriptor to the best match
* - {@link similarity} calculated normalized `similarity` of given descriptor to the best match
*/
export function match(descriptor: Descriptor, descriptors: Array<Descriptor>, options = { order: 2, threshold: 0 }) {
export function match(descriptor: Descriptor, descriptors: Array<Descriptor>, options = { order: 2, threshold: 0, multiplier: 20 }) {
if (!Array.isArray(descriptor) || !Array.isArray(descriptors) || descriptor.length < 64 || descriptors.length === 0 || descriptor.length !== descriptors[0].length) { // validate input
return { index: -1, distance: Number.POSITIVE_INFINITY, similarity: 0 };
}
let best = Number.MAX_SAFE_INTEGER;
let index = -1;
for (let i = 0; i < descriptors.length; i++) {
const res = distance(descriptor, descriptors[i], { order: options.order });
const res = distance(descriptor, descriptors[i], options);
if (res < best) {
best = res;
index = i;

View File

@ -22,16 +22,16 @@ let fx: fxImage.GLImageFilter | null; // instance of imagefx
export function canvas(width, height): AnyCanvas {
let c;
if (env.browser) {
if (env.offscreen) {
if (env.browser) { // browser defines canvas object
if (env.worker) { // if runing in web worker use OffscreenCanvas
c = new OffscreenCanvas(width, height);
} else {
} else { // otherwise use DOM canvas
if (typeof document === 'undefined') throw new Error('attempted to run in web worker but offscreenCanvas is not supported');
c = document.createElement('canvas');
c.width = width;
c.height = height;
}
} else {
} else { // if not running in browser, there is no "default" canvas object, so we need monkey patch or fail
// @ts-ignore // env.canvas is an external monkey-patch
if (typeof env.Canvas !== 'undefined') c = new env.Canvas(width, height);
else if (typeof globalThis.Canvas !== 'undefined') c = new globalThis.Canvas(width, height);
@ -40,6 +40,7 @@ export function canvas(width, height): AnyCanvas {
return c;
}
// helper function to copy canvas from input to output
export function copy(input: AnyCanvas, output?: AnyCanvas) {
const outputCanvas = output || canvas(input.width, input.height);
const ctx = outputCanvas.getContext('2d') as CanvasRenderingContext2D;

2
wiki

@ -1 +1 @@
Subproject commit 20ac74ed7ddf5fa0d51bd6eeb59156344d58a67b
Subproject commit 82ade650a7cd593e29a98a8b8a1cba893e14c2f0