add full nodejs test coverage

pull/280/head
Vladimir Mandic 2021-04-15 09:43:55 -04:00
parent f625e1e7ef
commit 82730dda1d
6 changed files with 117 additions and 44 deletions

View File

@ -1,6 +1,6 @@
# @vladmandic/human # @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** 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>** Author: **Vladimir Mandic <mandic00@live.com>**
@ -9,8 +9,9 @@ Repository: **<git+https://github.com/vladmandic/human.git>**
## Changelog ## 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 ### **1.5.1** 2021/04/13 mandic00@live.com

View File

@ -2,7 +2,7 @@
## Big Ticket Items ## Big Ticket Items
- Improve automated testing framework - N/A
## Explore Models ## Explore Models

View File

@ -21,11 +21,11 @@
"url": "git+https://github.com/vladmandic/human.git" "url": "git+https://github.com/vladmandic/human.git"
}, },
"scripts": { "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", "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", "build": "rimraf dist/* typedoc/* types/* && node --trace-warnings --unhandled-rejections=strict --trace-uncaught server/build.js",
"lint": "eslint src server demo test", "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": [ "keywords": [
"human", "human",
@ -61,14 +61,14 @@
"@tensorflow/tfjs-layers": "^3.4.0", "@tensorflow/tfjs-layers": "^3.4.0",
"@tensorflow/tfjs-node": "^3.4.0", "@tensorflow/tfjs-node": "^3.4.0",
"@tensorflow/tfjs-node-gpu": "^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/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.22.0",
"@vladmandic/pilogger": "^0.2.16", "@vladmandic/pilogger": "^0.2.17",
"canvas": "^2.7.0", "canvas": "^2.7.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"esbuild": "^0.11.10", "esbuild": "^0.11.11",
"eslint": "^7.24.0", "eslint": "^7.24.0",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",

View File

@ -4,22 +4,23 @@
### NodeJS using TensorFlow library ### NodeJS using TensorFlow library
- Image filters are disabled due to lack of Canvas and WeBGL access
- Face rotation is disabled for `NodeJS` platform: - Face rotation is disabled for `NodeJS` platform:
`Kernel 'RotateWithOffset' not registered for backend 'tensorflow'` `Kernel 'RotateWithOffset' not registered for backend 'tensorflow'`
<https://github.com/tensorflow/tfjs/issues/4606> <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 ### 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: - Face rotation is disabled for `NodeJS` platform:
`Kernel 'RotateWithOffset' not registered for backend 'tensorflow'` `Kernel 'RotateWithOffset' not registered for backend 'tensorflow'`
<https://github.com/tensorflow/tfjs/issues/4606> <https://github.com/tensorflow/tfjs/issues/4606>
- Image filters are disabled due to lack of Canvas and WeBGL access
### NodeJS using WASM ### NodeJS using WASM
- Requires dev http server - Requires dev http server
See <https://github.com/tensorflow/tfjs/issues/4927> 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 - Only supported input is Tensor due to missing image decoders
- Warmup returns null and is marked as failed - Warmup returns null and is marked as failed
Missing native Image implementation in NodeJS Missing native Image implementation in NodeJS

View File

@ -1,23 +1,25 @@
const process = require('process'); const process = require('process');
const path = require('path');
const log = require('@vladmandic/pilogger');
const canvasJS = require('canvas'); const canvasJS = require('canvas');
const fetch = require('node-fetch').default; const fetch = require('node-fetch').default;
let config; 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() { async function testHTTP() {
if (config.modelBasePath.startsWith('file:')) return true; if (config.modelBasePath.startsWith('file:')) return true;
return new Promise((resolve) => { return new Promise((resolve) => {
fetch(config.modelBasePath) fetch(config.modelBasePath)
.then((res) => { .then((res) => {
if (res && res.ok) log.state('passed: model server:', config.modelBasePath); if (res && res.ok) log('state', 'passed: model server:', config.modelBasePath);
else log.error('failed: model server:', config.modelBasePath); else log('error', 'failed: model server:', config.modelBasePath);
resolve(res && res.ok); resolve(res && res.ok);
}) })
.catch((err) => { .catch((err) => {
log.error('failed: model server:', err.message); log('error', 'failed: model server:', err.message);
resolve(false); resolve(false);
}); });
}); });
@ -28,7 +30,7 @@ async function getImage(human, input) {
try { try {
img = await canvasJS.loadImage(input); img = await canvasJS.loadImage(input);
} catch (err) { } catch (err) {
log.error('failed: load image', input, err.message); log('error', 'failed: load image', input, err.message);
return img; return img;
} }
const canvas = canvasJS.createCanvas(img.width, img.height); 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 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; return reshape;
}); });
if (res && res.shape[0] === 1 && res.shape[3] === 3) log.state('passed: load image:', input, res.shape); 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); else log('error', 'failed: load image:', input, res);
return 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 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 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 } : {}; 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.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.performance) log('data', ' result: performance:', 'load:', detect?.performance.load, 'total:', detect.performance?.total);
} }
async function testInstance(human) { async function testInstance(human) {
if (human) log.state('passed: create human'); if (human) log('state', 'passed: create human');
else log.error('failed: create human'); else log('error', 'failed: create human');
// if (!human.tf) human.tf = tf; // if (!human.tf) human.tf = tf;
log.info('human version:', human.version); log('info', 'human version:', human.version);
log.info('platform:', human.sysinfo.platform, 'agent:', human.sysinfo.agent); log('info', 'platform:', human.sysinfo.platform, 'agent:', human.sysinfo.agent);
log.info('tfjs version:', human.tf.version.tfjs); log('info', 'tfjs version:', human.tf.version.tfjs);
await human.load(); await human.load();
if (config.backend === human.tf.getBackend()) log.state('passed: 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); else log('error', 'failed: set backend:', config.backend);
if (human.models) { if (human.models) {
log.state('passed: load models'); log('state', 'passed: load models');
const keys = Object.keys(human.models); const keys = Object.keys(human.models);
const loaded = keys.filter((model) => human.models[model]); 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; return true;
} }
log.error('failed: load models'); log('error', 'failed: load models');
return false; return false;
} }
@ -84,36 +86,36 @@ async function testWarmup(human, title) {
try { try {
warmup = await human.warmup(config); warmup = await human.warmup(config);
} catch (err) { } catch (err) {
log.error('error warmup'); log('error', 'error warmup');
} }
if (warmup) { if (warmup) {
log.state('passed: warmup:', config.warmup, title); log('state', 'passed: warmup:', config.warmup, title);
printResults(warmup); printResults(warmup);
return true; return true;
} }
log.error('failed: warmup:', config.warmup, title); log('error', 'failed: warmup:', config.warmup, title);
return false; return false;
} }
async function testDetect(human, input, title) { async function testDetect(human, input, title) {
const image = input ? await getImage(human, input) : human.tf.randomNormal([1, 1024, 1024, 3]); const image = input ? await getImage(human, input) : human.tf.randomNormal([1, 1024, 1024, 3]);
if (!image) { if (!image) {
log.error('failed: detect: input is null'); log('error', 'failed: detect: input is null');
return false; return false;
} }
let detect; let detect;
try { try {
detect = await human.detect(image, config); detect = await human.detect(image, config);
} catch (err) { } catch (err) {
log.error('error: detect', err); log('error', 'error: detect', err);
} }
if (image instanceof human.tf.Tensor) human.tf.dispose(image); if (image instanceof human.tf.Tensor) human.tf.dispose(image);
if (detect) { if (detect) {
log.state('passed: detect:', input || 'random', title); log('state', 'passed: detect:', input || 'random', title);
printResults(detect); printResults(detect);
return true; return true;
} }
log.error('failed: detect', input || 'random', title); log('error', 'failed: detect', input || 'random', title);
return false; return false;
} }
@ -121,7 +123,7 @@ async function test(Human, inputConfig) {
config = inputConfig; config = inputConfig;
const ok = await testHTTP(); const ok = await testHTTP();
if (!ok) { if (!ok) {
log.warn('aborting test'); log('error', 'aborting test');
return; return;
} }
const t0 = process.hrtime.bigint(); const t0 = process.hrtime.bigint();
@ -134,7 +136,7 @@ async function test(Human, inputConfig) {
config.warmup = 'body'; config.warmup = 'body';
await testWarmup(human, 'default'); await testWarmup(human, 'default');
log.info('test body variants'); log('info', 'test body variants');
config.body = { modelPath: 'posenet.json', enabled: true }; config.body = { modelPath: 'posenet.json', enabled: true };
await testDetect(human, 'assets/human-sample-body.jpg', 'posenet'); await testDetect(human, 'assets/human-sample-body.jpg', 'posenet');
config.body = { modelPath: 'efficientpose.json', enabled: true }; 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, 'assets/human-sample-body.jpg', 'blazepose');
await testDetect(human, null, 'default'); await testDetect(human, null, 'default');
log.info('test: first instance'); log('info', 'test: first instance');
await testDetect(human, 'assets/sample-me.jpg', 'default'); await testDetect(human, 'assets/sample-me.jpg', 'default');
log.info('test: second instance'); log('info', 'test: second instance');
const second = new Human(config); const second = new Human(config);
await testDetect(second, 'assets/sample-me.jpg', 'default'); await testDetect(second, 'assets/sample-me.jpg', 'default');
log.info('test: concurrent'); log('info', 'test: concurrent');
await Promise.all([ await Promise.all([
testDetect(human, 'assets/human-sample-face.jpg', 'default'), testDetect(human, 'assets/human-sample-face.jpg', 'default'),
testDetect(second, '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'), testDetect(second, 'assets/human-sample-body.jpg', 'default'),
]); ]);
const t1 = process.hrtime.bigint(); 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; exports.test = test;

69
test/test.js Normal file
View File

@ -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();