update tfjs 3.4.0

pull/280/head
Vladimir Mandic 2021-04-14 12:53:00 -04:00
parent e231e68d2d
commit e42465d004
12 changed files with 310 additions and 113 deletions

View File

@ -61,6 +61,7 @@
"node/no-unpublished-import": "off",
"node/no-unpublished-require": "off",
"node/no-unsupported-features/es-syntax": "off",
"no-lonely-if": "off",
"node/shebang": "off",
"object-curly-newline": "off",
"prefer-destructuring": "off",

View File

@ -9,6 +9,9 @@ Repository: **<git+https://github.com/vladmandic/human.git>**
## Changelog
### **HEAD -> main** 2021/04/13 mandic00@live.com
### **1.5.1** 2021/04/13 mandic00@live.com
- fix for safari imagebitmap

View File

@ -41,7 +41,6 @@ async function webRTC(server, streamName, elementName) {
connection.ontrack = (event) => {
stream.addTrack(event.track);
const video = (typeof elementName === 'string') ? document.getElementById(elementName) : elementName;
// @ts-ignore
if (video instanceof HTMLVideoElement) video.srcObject = stream;
else log('element is not a video element:', elementName);
// video.onloadeddata = async () => log('resolution:', video.videoWidth, video.videoHeight);

View File

@ -198,7 +198,7 @@ async function setupCamera() {
ui.busy = true;
const viewportScale = Math.min(1, Math.round(100 * window.outerWidth / 700) / 100);
// log('demo viewport scale:', viewportScale);
document.querySelector('meta[name=viewport]').setAttribute('content', `width=device-width, shrink-to-fit=no; initial-scale=${viewportScale}`);
document.querySelector('meta[name=viewport]').setAttribute('content', `width=device-width, shrink-to-fit=no, initial-scale=${viewportScale}`);
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const output = document.getElementById('log');
@ -587,7 +587,7 @@ async function main() {
log('overriding worker:', ui.useWorker);
}
if (params.has('backend')) {
userConfig.backend = JSON.parse(params.get('backend'));
userConfig.backend = params.get('backend');
log('overriding backend:', userConfig.backend);
}
if (params.has('preload')) {

View File

@ -51,20 +51,21 @@
"tensorflow"
],
"devDependencies": {
"@tensorflow/tfjs": "^3.3.0",
"@tensorflow/tfjs-backend-cpu": "^3.3.0",
"@tensorflow/tfjs-backend-wasm": "^3.3.0",
"@tensorflow/tfjs-backend-webgl": "^3.3.0",
"@tensorflow/tfjs-converter": "^3.3.0",
"@tensorflow/tfjs-core": "^3.3.0",
"@tensorflow/tfjs-data": "^3.3.0",
"@tensorflow/tfjs-layers": "^3.3.0",
"@tensorflow/tfjs-node": "^3.3.0",
"@tensorflow/tfjs-node-gpu": "^3.3.0",
"@tensorflow/tfjs": "^3.4.0",
"@tensorflow/tfjs-backend-cpu": "^3.4.0",
"@tensorflow/tfjs-backend-wasm": "^3.4.0",
"@tensorflow/tfjs-backend-webgl": "^3.4.0",
"@tensorflow/tfjs-converter": "^3.4.0",
"@tensorflow/tfjs-core": "^3.4.0",
"@tensorflow/tfjs-data": "^3.4.0",
"@tensorflow/tfjs-layers": "^3.4.0",
"@tensorflow/tfjs-node": "^3.4.0",
"@tensorflow/tfjs-node-gpu": "^3.4.0",
"@types/node": "^14.14.37",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"@vladmandic/pilogger": "^0.2.16",
"canvas": "^2.7.0",
"chokidar": "^3.5.1",
"dayjs": "^1.10.4",
"esbuild": "^0.11.10",
@ -75,9 +76,10 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"hint": "^6.1.3",
"node-fetch": "^2.6.1",
"rimraf": "^3.0.2",
"seedrandom": "^3.0.5",
"simple-git": "^2.37.0",
"simple-git": "^2.38.0",
"tslib": "^2.2.0",
"typedoc": "^0.20.35",
"typescript": "^4.2.4"

View File

@ -157,7 +157,7 @@ export class Human {
faceres: null,
};
// export access to image processing
// @ts-ignore // typescript cannot infer type
// @ts-ignore eslint-typescript cannot correctly infer type in anonymous function
this.image = (input: Input) => image.process(input, this.config);
// export raw access to underlying models
this.classes = {
@ -266,9 +266,7 @@ export class Human {
this.models.gender,
this.models.emotion,
this.models.embedding,
// @ts-ignore // typescript cannot infer type
this.models.handpose,
// @ts-ignore // typescript cannot infer type
this.models.posenet,
this.models.blazepose,
this.models.efficientpose,
@ -280,8 +278,8 @@ export class Human {
this.models.gender || ((this.config.face.enabled && this.config.face.gender.enabled) ? gender.load(this.config) : null),
this.models.emotion || ((this.config.face.enabled && this.config.face.emotion.enabled) ? emotion.load(this.config) : null),
this.models.embedding || ((this.config.face.enabled && this.config.face.embedding.enabled) ? embedding.load(this.config) : null),
this.models.handpose || (this.config.hand.enabled ? <Promise<handpose.HandPose>>handpose.load(this.config) : null),
this.models.posenet || (this.config.body.enabled && this.config.body.modelPath.includes('posenet') ? posenet.load(this.config) : null),
this.models.handpose || (this.config.hand.enabled ? handpose.load(this.config) : null),
this.models.posenet || (this.config.body.enabled && this.config.body.modelPath.includes('posenet') ? <any>posenet.load(this.config) : null),
this.models.blazepose || (this.config.body.enabled && this.config.body.modelPath.includes('blazepose') ? blazepose.load(this.config) : null),
this.models.efficientpose || (this.config.body.enabled && this.config.body.modelPath.includes('efficientpose') ? efficientpose.load(this.config) : null),
this.models.nanodet || (this.config.object.enabled ? nanodet.load(this.config) : null),
@ -340,7 +338,7 @@ export class Human {
const simd = await this.tf.env().getAsync('WASM_HAS_SIMD_SUPPORT');
const mt = await this.tf.env().getAsync('WASM_HAS_MULTITHREAD_SUPPORT');
if (this.config.debug) log(`wasm execution: ${simd ? 'SIMD' : 'no SIMD'} ${mt ? 'multithreaded' : 'singlethreaded'}`);
if (!simd) log('warning: wasm simd support is not enabled');
if (this.config.debug && !simd) log('warning: wasm simd support is not enabled');
}
if (this.config.backend === 'humangl') backend.register();
@ -574,23 +572,29 @@ export class Human {
/** @hidden */
#warmupNode = async () => {
// @ts-ignore
if (typeof tf.node === 'undefined') {
if (this.config.debug) log('Warmup tfjs-node not loaded');
return null;
}
const atob = (str) => Buffer.from(str, 'base64');
let img;
if (this.config.warmup === 'face') img = atob(sample.face);
if (this.config.warmup === 'body' || this.config.warmup === 'full') img = atob(sample.body);
if (!img) return null;
// @ts-ignore // tf.node is only defined when compiling for nodejs
const data = tf.node?.decodeJpeg(img);
let res;
if (typeof tf['node'] !== 'undefined') {
const data = tf['node'].decodeJpeg(img);
const expanded = data.expandDims(0);
this.tf.dispose(data);
// log('Input:', expanded);
const res = await this.detect(expanded, this.config);
res = await this.detect(expanded, this.config);
this.tf.dispose(expanded);
} else {
if (this.config.debug) log('Warmup tfjs-node not loaded');
/*
const input = await canvasJS.loadImage(img);
const canvas = canvasJS.createCanvas(input.width, input.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, input.width, input.height);
res = await this.detect(input, this.config);
*/
}
return res;
}

47
test/README.md Normal file
View File

@ -0,0 +1,47 @@
# Test Results
## Automatic Tests
### NodeJS using TensorFlow library
- Face rotation is disabled for `NodeJS` platform:
`Kernel 'RotateWithOffset' not registered for backend 'tensorflow'`
<https://github.com/tensorflow/tfjs/issues/4606>
- Image filters are disabled due to lack of Canvas and WeBGL access
### NodeJS with GPU acceleation using CUDA
- Face rotation is disabled for `NodeJS` platform:
`Kernel 'RotateWithOffset' not registered for backend 'tensorflow'`
<https://github.com/tensorflow/tfjs/issues/4606>
- Image filters are disabled due to lack of Canvas and WeBGL access
### NodeJS using WASM
- Requires dev http server
See <https://github.com/tensorflow/tfjs/issues/4927>
- Only supported input is Tensor due to missing image decoders
- Warmup returns null and is marked as failed
Missing native Image implementation in NodeJS
- Fails on object detection:
`Kernel 'SparseToDense' not registered for backend 'wasm'`
<https://github.com/tensorflow/tfjs/issues/4824>
<br>
## Manual Tests
### Browser using WebGL backend
- Chrome/Edge: All Passing
- Firefox: WebWorkers not supported due to missing support for OffscreenCanvas
- Safari: Limited Testing
### Browser using WASM backend
- Chrome/Edge: All Passing
- Firefox: WebWorkers not supported due to missing support for OffscreenCanvas
- Safari: Limited Testing
- Fails on object detection:
`Kernel 'SparseToDense' not registered for backend 'wasm'`
<https://github.com/tensorflow/tfjs/issues/4824>

162
test/test-main.js Normal file
View File

@ -0,0 +1,162 @@
const process = require('process');
const path = require('path');
const log = require('@vladmandic/pilogger');
const canvasJS = require('canvas');
const fetch = require('node-fetch').default;
let config;
log.info('test:', path.basename(process.argv[1]));
async function testHTTP() {
if (config.modelBasePath.startsWith('file:')) return true;
return new Promise((resolve) => {
fetch(config.modelBasePath)
.then((res) => {
if (res && res.ok) log.state('passed: model server:', config.modelBasePath);
else log.error('failed: model server:', config.modelBasePath);
resolve(res && res.ok);
})
.catch((err) => {
log.error('failed: model server:', err.message);
resolve(false);
});
});
}
async function getImage(human, input) {
let img;
try {
img = await canvasJS.loadImage(input);
} catch (err) {
log.error('failed: load image', input, err.message);
return img;
}
const canvas = canvasJS.createCanvas(img.width, img.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const res = human.tf.tidy(() => {
const tensor = human.tf.tensor(Array.from(imageData.data), [canvas.height, canvas.width, 4], 'int32'); // create rgba image tensor from flat array
const channels = human.tf.split(tensor, 4, 2); // split rgba to channels
const rgb = human.tf.stack([channels[0], channels[1], channels[2]], 2); // stack channels back to rgb
const reshape = human.tf.reshape(rgb, [1, canvas.width, canvas.height, 3]); // move extra dim from the end of tensor and use it as batch number instead
return reshape;
});
if (res && res.shape[0] === 1 && res.shape[3] === 3) log.state('passed: load image:', input, res.shape);
else log.error('failed: load image:', input, res);
return res;
}
function printResults(detect) {
const person = (detect.face && detect.face[0]) ? { confidence: detect.face[0].confidence, age: detect.face[0].age, gender: detect.face[0].gender } : {};
const object = (detect.object && detect.object[0]) ? { score: detect.object[0].score, class: detect.object[0].label } : {};
const body = (detect.body && detect.body[0]) ? { score: detect.body[0].score, keypoints: detect.body[0].keypoints.length } : {};
if (detect.face) log.data(' result: face:', detect.face?.length, 'body:', detect.body?.length, 'hand:', detect.hand?.length, 'gesture:', detect.gesture?.length, 'object:', detect.object?.length, person, object, body);
if (detect.performance) log.data(' result: performance:', 'load:', detect?.performance.load, 'total:', detect.performance?.total);
}
async function testInstance(human) {
if (human) log.state('passed: create human');
else log.error('failed: create human');
// if (!human.tf) human.tf = tf;
log.info('human version:', human.version);
log.info('platform:', human.sysinfo.platform, 'agent:', human.sysinfo.agent);
log.info('tfjs version:', human.tf.version.tfjs);
await human.load();
if (config.backend === human.tf.getBackend()) log.state('passed: set backend:', config.backend);
else log.error('failed: set backend:', config.backend);
if (human.models) {
log.state('passed: load models');
const keys = Object.keys(human.models);
const loaded = keys.filter((model) => human.models[model]);
log.data(' result: defined models:', keys.length, 'loaded models:', loaded.length);
return true;
}
log.error('failed: load models');
return false;
}
async function testWarmup(human, title) {
let warmup;
try {
warmup = await human.warmup(config);
} catch (err) {
log.error('error warmup');
}
if (warmup) {
log.state('passed: warmup:', config.warmup, title);
printResults(warmup);
return true;
}
log.error('failed: warmup:', config.warmup, title);
return false;
}
async function testDetect(human, input, title) {
const image = input ? await getImage(human, input) : human.tf.randomNormal([1, 1024, 1024, 3]);
if (!image) {
log.error('failed: detect: input is null');
return false;
}
let detect;
try {
detect = await human.detect(image, config);
} catch (err) {
log.error('error: detect', err);
}
if (image instanceof human.tf.Tensor) human.tf.dispose(image);
if (detect) {
log.state('passed: detect:', input || 'random', title);
printResults(detect);
return true;
}
log.error('failed: detect', input || 'random', title);
return false;
}
async function test(Human, inputConfig) {
config = inputConfig;
const ok = await testHTTP();
if (!ok) {
log.warn('aborting test');
return;
}
const t0 = process.hrtime.bigint();
const human = new Human(config);
await testInstance(human);
config.warmup = 'none';
await testWarmup(human, 'default');
config.warmup = 'face';
await testWarmup(human, 'default');
config.warmup = 'body';
await testWarmup(human, 'default');
log.info('test body variants');
config.body = { modelPath: 'posenet.json', enabled: true };
await testDetect(human, 'assets/human-sample-body.jpg', 'posenet');
config.body = { modelPath: 'efficientpose.json', enabled: true };
await testDetect(human, 'assets/human-sample-body.jpg', 'efficientpose');
config.body = { modelPath: 'blazepose.json', enabled: true };
await testDetect(human, 'assets/human-sample-body.jpg', 'blazepose');
await testDetect(human, null, 'default');
log.info('test: first instance');
await testDetect(human, 'assets/sample-me.jpg', 'default');
log.info('test: second instance');
const second = new Human(config);
await testDetect(second, 'assets/sample-me.jpg', 'default');
log.info('test: concurrent');
await Promise.all([
testDetect(human, 'assets/human-sample-face.jpg', 'default'),
testDetect(second, 'assets/human-sample-face.jpg', 'default'),
testDetect(human, 'assets/human-sample-body.jpg', 'default'),
testDetect(second, 'assets/human-sample-body.jpg', 'default'),
]);
const t1 = process.hrtime.bigint();
log.info('test complete:', Math.trunc(parseInt((t1 - t0).toString()) / 1000 / 1000), 'ms');
}
exports.test = test;

26
test/test-node-gpu.js Normal file
View File

@ -0,0 +1,26 @@
const Human = require('../dist/human.node-gpu.js').default;
const test = require('./test-main.js').test;
const config = {
modelBasePath: 'file://models/',
backend: 'tensorflow',
debug: false,
videoOptimized: false,
async: false,
filter: {
enabled: true,
},
face: {
enabled: true,
detector: { enabled: true, rotation: true },
mesh: { enabled: true },
iris: { enabled: true },
description: { enabled: true },
emotion: { enabled: true },
},
hand: { enabled: true },
body: { enabled: true },
object: { enabled: true },
};
test(Human, config);

27
test/test-node-wasm.js Normal file
View File

@ -0,0 +1,27 @@
const Human = require('../dist/human.node-wasm.js').default;
const test = require('./test-main.js').test;
const config = {
modelBasePath: 'http://localhost:10030/models/',
backend: 'wasm',
wasmPath: 'assets/',
debug: false,
videoOptimized: false,
async: false,
filter: {
enabled: true,
},
face: {
enabled: true,
detector: { enabled: true, rotation: true },
mesh: { enabled: true },
iris: { enabled: true },
description: { enabled: true },
emotion: { enabled: true },
},
hand: { enabled: true },
body: { enabled: true },
object: { enabled: false },
};
test(Human, config);

View File

@ -1,100 +1,26 @@
const log = require('@vladmandic/pilogger');
// 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;
const test = require('./test-main.js').test;
const config = {
modelBasePath: 'file://models/',
backend: 'tensorflow',
debug: false,
videoOptimized: false,
async: false,
warmup: 'full',
modelBasePath: 'file://models/',
filter: {
enabled: true,
},
face: {
enabled: true,
detector: { enabled: true, rotation: false },
detector: { enabled: true, rotation: true },
mesh: { enabled: true },
iris: { enabled: true },
description: { enabled: true },
emotion: { enabled: true },
},
hand: {
enabled: true,
},
// body: { modelPath: 'efficientpose.json', enabled: true },
// body: { modelPath: 'blazepose.json', enabled: true },
body: { modelPath: 'posenet.json', enabled: true },
hand: { enabled: true },
body: { enabled: true },
object: { enabled: true },
};
async function testInstance(human) {
if (human) log.state('passed: create human');
else log.error('failed: create human');
// if (!human.tf) human.tf = tf;
log.info('human version:', human.version);
log.info('tfjs version:', human.tf.version_core);
log.info('platform:', human.sysinfo.platform);
log.info('agent:', human.sysinfo.agent);
await human.load();
if (human.models) {
log.state('passed: load models');
const keys = Object.keys(human.models);
const loaded = keys.filter((model) => human.models[model]);
log.data(' result: defined models:', keys.length, 'loaded models:', loaded.length);
} else {
log.error('failed: load models');
}
let warmup;
try {
warmup = await human.warmup();
} catch (err) {
log.error('error warmup');
}
if (warmup) {
log.state('passed: warmup:', config.warmup);
log.data(' result: face:', warmup.face?.length, 'body:', warmup.body?.length, 'hand:', warmup.hand?.length, 'gesture:', warmup.gesture?.length, 'object:', warmup.object?.length);
log.data(' result: performance:', 'load:', warmup.performance?.load, 'total:', warmup.performance?.total);
} else {
log.error('failed: warmup');
}
const random = tf.randomNormal([1, 1024, 1024, 3]);
let detect;
try {
detect = await human.detect(random);
} catch (err) {
log.error('error: detect', err);
}
tf.dispose(random);
if (detect) {
log.state('passed: detect:', 'random');
log.data(' result: face:', detect.face?.length, 'body:', detect.body?.length, 'hand:', detect.hand?.length, 'gesture:', detect.gesture?.length, 'object:', detect.object?.length);
log.data(' result: performance:', 'load:', detect?.performance.load, 'total:', detect.performance?.total);
} else {
log.error('failed: detect');
}
}
async function test() {
log.info('testing instance#1 - none');
config.warmup = 'none';
const human1 = new Human(config);
await testInstance(human1);
log.info('testing instance#2 - face');
config.warmup = 'face';
const human2 = new Human(config);
await testInstance(human2);
log.info('testing instance#3 - body');
config.warmup = 'body';
const human3 = new Human(config);
await testInstance(human3);
}
test();
test(Human, config);

2
wiki

@ -1 +1 @@
Subproject commit 77b1cd6cfd86abe0b21aae23e2be2beff84b68ff
Subproject commit 00a239fa51eda5b366f0e1d05d66fccf4edb0ce1