mirror of https://github.com/vladmandic/human
increase face similarity match resolution
parent
90ce714446
commit
1f03270e76
|
@ -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
|
||||
|
|
|
@ -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();
|
10
package.json
10
package.json
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
2
wiki
|
@ -1 +1 @@
|
|||
Subproject commit 20ac74ed7ddf5fa0d51bd6eeb59156344d58a67b
|
||||
Subproject commit 82ade650a7cd593e29a98a8b8a1cba893e14c2f0
|
Loading…
Reference in New Issue