mirror of https://github.com/vladmandic/human
add full nodejs test coverage
parent
f625e1e7ef
commit
82730dda1d
|
@ -1,6 +1,6 @@
|
|||
# @vladmandic/human
|
||||
|
||||
Version: **1.5.1**
|
||||
Version: **1.5.2**
|
||||
Description: **Human: AI-powered 3D Face Detection & Rotation Tracking, Face Description & Recognition, Body Pose Tracking, 3D Hand & Finger Tracking, Iris Analysis, Age & Gender & Emotion Prediction, Gesture Recognition**
|
||||
|
||||
Author: **Vladimir Mandic <mandic00@live.com>**
|
||||
|
@ -9,8 +9,9 @@ Repository: **<git+https://github.com/vladmandic/human.git>**
|
|||
|
||||
## Changelog
|
||||
|
||||
### **HEAD -> main** 2021/04/13 mandic00@live.com
|
||||
### **1.5.2** 2021/04/14 mandic00@live.com
|
||||
|
||||
- experimental node-wasm support
|
||||
|
||||
### **1.5.1** 2021/04/13 mandic00@live.com
|
||||
|
||||
|
|
2
TODO.md
2
TODO.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Big Ticket Items
|
||||
|
||||
- Improve automated testing framework
|
||||
- N/A
|
||||
|
||||
## Explore Models
|
||||
|
||||
|
|
10
package.json
10
package.json
|
@ -21,11 +21,11 @@
|
|||
"url": "git+https://github.com/vladmandic/human.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught --no-deprecation demo/node.js",
|
||||
"start": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught demo/node.js",
|
||||
"dev": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught server/serve.js",
|
||||
"build": "rimraf dist/* typedoc/* types/* && node --trace-warnings --unhandled-rejections=strict --trace-uncaught server/build.js",
|
||||
"lint": "eslint src server demo test",
|
||||
"test": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught --no-deprecation test/test-node.js"
|
||||
"test": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught test/test.js"
|
||||
},
|
||||
"keywords": [
|
||||
"human",
|
||||
|
@ -61,14 +61,14 @@
|
|||
"@tensorflow/tfjs-layers": "^3.4.0",
|
||||
"@tensorflow/tfjs-node": "^3.4.0",
|
||||
"@tensorflow/tfjs-node-gpu": "^3.4.0",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/node": "^14.14.39",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"@vladmandic/pilogger": "^0.2.16",
|
||||
"@vladmandic/pilogger": "^0.2.17",
|
||||
"canvas": "^2.7.0",
|
||||
"chokidar": "^3.5.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"esbuild": "^0.11.10",
|
||||
"esbuild": "^0.11.11",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
|
|
|
@ -4,22 +4,23 @@
|
|||
|
||||
### NodeJS using TensorFlow library
|
||||
|
||||
- Image filters are disabled due to lack of Canvas and WeBGL access
|
||||
- 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
|
||||
|
||||
- Image filters are disabled due to lack of Canvas and WeBGL access
|
||||
- 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>
|
||||
- Image filters are disabled due to lack of Canvas and WeBGL access
|
||||
- Only supported input is Tensor due to missing image decoders
|
||||
- Warmup returns null and is marked as failed
|
||||
Missing native Image implementation in NodeJS
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
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]));
|
||||
|
||||
const log = (status, ...data) => {
|
||||
if (typeof process.send !== 'undefined') process.send([status, data]);
|
||||
else process.stdout.write(JSON.stringify(data));
|
||||
};
|
||||
|
||||
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);
|
||||
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);
|
||||
log('error', 'failed: model server:', err.message);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
@ -28,7 +30,7 @@ async function getImage(human, input) {
|
|||
try {
|
||||
img = await canvasJS.loadImage(input);
|
||||
} catch (err) {
|
||||
log.error('failed: load image', input, err.message);
|
||||
log('error', 'failed: load image', input, err.message);
|
||||
return img;
|
||||
}
|
||||
const canvas = canvasJS.createCanvas(img.width, img.height);
|
||||
|
@ -42,8 +44,8 @@ async function getImage(human, input) {
|
|||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -51,31 +53,31 @@ 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);
|
||||
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) 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);
|
||||
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 (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');
|
||||
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);
|
||||
log('state', ' result: defined models:', keys.length, 'loaded models:', loaded.length);
|
||||
return true;
|
||||
}
|
||||
log.error('failed: load models');
|
||||
log('error', 'failed: load models');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -84,36 +86,36 @@ async function testWarmup(human, title) {
|
|||
try {
|
||||
warmup = await human.warmup(config);
|
||||
} catch (err) {
|
||||
log.error('error warmup');
|
||||
log('error', 'error warmup');
|
||||
}
|
||||
if (warmup) {
|
||||
log.state('passed: warmup:', config.warmup, title);
|
||||
log('state', 'passed: warmup:', config.warmup, title);
|
||||
printResults(warmup);
|
||||
return true;
|
||||
}
|
||||
log.error('failed: warmup:', config.warmup, title);
|
||||
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');
|
||||
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);
|
||||
log('error', 'error: detect', err);
|
||||
}
|
||||
if (image instanceof human.tf.Tensor) human.tf.dispose(image);
|
||||
if (detect) {
|
||||
log.state('passed: detect:', input || 'random', title);
|
||||
log('state', 'passed: detect:', input || 'random', title);
|
||||
printResults(detect);
|
||||
return true;
|
||||
}
|
||||
log.error('failed: detect', input || 'random', title);
|
||||
log('error', 'failed: detect', input || 'random', title);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -121,7 +123,7 @@ async function test(Human, inputConfig) {
|
|||
config = inputConfig;
|
||||
const ok = await testHTTP();
|
||||
if (!ok) {
|
||||
log.warn('aborting test');
|
||||
log('error', 'aborting test');
|
||||
return;
|
||||
}
|
||||
const t0 = process.hrtime.bigint();
|
||||
|
@ -134,7 +136,7 @@ async function test(Human, inputConfig) {
|
|||
config.warmup = 'body';
|
||||
await testWarmup(human, 'default');
|
||||
|
||||
log.info('test body variants');
|
||||
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 };
|
||||
|
@ -143,12 +145,12 @@ async function test(Human, inputConfig) {
|
|||
await testDetect(human, 'assets/human-sample-body.jpg', 'blazepose');
|
||||
|
||||
await testDetect(human, null, 'default');
|
||||
log.info('test: first instance');
|
||||
log('info', 'test: first instance');
|
||||
await testDetect(human, 'assets/sample-me.jpg', 'default');
|
||||
log.info('test: second instance');
|
||||
log('info', 'test: second instance');
|
||||
const second = new Human(config);
|
||||
await testDetect(second, 'assets/sample-me.jpg', 'default');
|
||||
log.info('test: concurrent');
|
||||
log('info', 'test: concurrent');
|
||||
await Promise.all([
|
||||
testDetect(human, 'assets/human-sample-face.jpg', 'default'),
|
||||
testDetect(second, 'assets/human-sample-face.jpg', 'default'),
|
||||
|
@ -156,7 +158,7 @@ async function test(Human, inputConfig) {
|
|||
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');
|
||||
log('info', 'test complete:', Math.trunc(parseInt((t1 - t0).toString()) / 1000 / 1000), 'ms');
|
||||
}
|
||||
|
||||
exports.test = test;
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
const path = require('path');
|
||||
const process = require('process');
|
||||
const { fork } = require('child_process');
|
||||
const log = require('@vladmandic/pilogger');
|
||||
|
||||
const tests = [
|
||||
'test-node.js',
|
||||
'test-node-gpu.js',
|
||||
'test-node-wasm.js',
|
||||
];
|
||||
|
||||
const ignore = [
|
||||
'cpu_feature_guard.cc',
|
||||
'rebuild TensorFlow',
|
||||
'xla_gpu_device.cc',
|
||||
'cudart_stub.cc',
|
||||
'cuda_driver.cc:326',
|
||||
'cpu_allocator_impl.cc',
|
||||
];
|
||||
|
||||
const status = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
};
|
||||
|
||||
function logMessage(test, data) {
|
||||
log[data[0]](test, ...data[1]);
|
||||
if (data[1][0].startsWith('passed')) status.passed++;
|
||||
if (data[1][0].startsWith('failed')) status.failed++;
|
||||
}
|
||||
|
||||
function logStdIO(ok, test, buffer) {
|
||||
const lines = buffer.toString().split(/\r\n|\n\r|\n|\r/);
|
||||
const filtered = lines.filter((line) => {
|
||||
for (const ignoreString of ignore) {
|
||||
if (line.includes(ignoreString)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
for (const line of filtered) {
|
||||
if (line.length < 2) continue;
|
||||
if (ok) log.data(test, 'stdout:', line);
|
||||
else log.warn(test, 'stderr:', line);
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest(test) {
|
||||
return new Promise((resolve) => {
|
||||
log.info(test, 'start');
|
||||
const child = fork(path.join(__dirname, test), [], { silent: true });
|
||||
child.on('message', (data) => logMessage(test, data));
|
||||
child.on('error', (data) => log.error(test, ':', data.message || data));
|
||||
child.on('close', (code) => resolve(code));
|
||||
child.stdout?.on('data', (data) => logStdIO(true, test, data));
|
||||
child.stderr?.on('data', (data) => logStdIO(false, test, data));
|
||||
});
|
||||
}
|
||||
|
||||
async function testAll() {
|
||||
log.logFile(path.join(__dirname, 'test.log'));
|
||||
log.header();
|
||||
process.on('unhandledRejection', (data) => log.error('nodejs unhandled rejection', data));
|
||||
process.on('uncaughtException', (data) => log.error('nodejs unhandled exception', data));
|
||||
log.info('tests:', tests);
|
||||
for (const test of tests) await runTest(test);
|
||||
log.info('status:', status);
|
||||
}
|
||||
|
||||
testAll();
|
Loading…
Reference in New Issue