diff --git a/CHANGELOG.md b/CHANGELOG.md index cb26bfc9..adcfa6de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/demo/nodejs/node-similarity.js b/demo/nodejs/node-similarity.js new file mode 100644 index 00000000..2f262833 --- /dev/null +++ b/demo/nodejs/node-similarity.js @@ -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: 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(); diff --git a/package.json b/package.json index c60b3189..8340086e 100644 --- a/package.json +++ b/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": { diff --git a/src/face/match.ts b/src/face/match.ts index d0ee82f3..ee9e7631 100644 --- a/src/face/match.ts +++ b/src/face/match.ts @@ -7,25 +7,27 @@ export type Descriptor = Array * * 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, options = { order: 2, threshold: 0 }) { +export function match(descriptor: Descriptor, descriptors: Array, 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; diff --git a/src/image/image.ts b/src/image/image.ts index c753e8a7..0ffe272a 100644 --- a/src/image/image.ts +++ b/src/image/image.ts @@ -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; diff --git a/wiki b/wiki index 20ac74ed..82ade650 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 20ac74ed7ddf5fa0d51bd6eeb59156344d58a67b +Subproject commit 82ade650a7cd593e29a98a8b8a1cba893e14c2f0