human/src/human.js

259 lines
8.6 KiB
JavaScript
Raw Normal View History

2020-10-12 16:08:00 +02:00
const tf = require('@tensorflow/tfjs');
2020-10-14 17:43:33 +02:00
const facemesh = require('./facemesh/facemesh.js');
const ssrnet = require('./ssrnet/ssrnet.js');
2020-10-15 00:22:38 +02:00
const emotion = require('./emotion/emotion.js');
2020-10-14 17:43:33 +02:00
const posenet = require('./posenet/posenet.js');
const handpose = require('./handpose/handpose.js');
2020-10-17 12:30:00 +02:00
const defaults = require('../config.js').default;
2020-10-15 21:25:58 +02:00
const app = require('../package.json');
let config;
2020-10-17 13:15:23 +02:00
let state = 'idle';
2020-10-12 01:22:43 +02:00
2020-10-15 15:43:16 +02:00
// object that contains all initialized models
2020-10-12 01:22:43 +02:00
const models = {
facemesh: null,
2020-10-15 15:43:16 +02:00
posenet: null,
handpose: null,
2020-10-12 01:22:43 +02:00
iris: null,
2020-10-15 15:43:16 +02:00
age: null,
gender: null,
emotion: null,
2020-10-12 01:22:43 +02:00
};
2020-10-18 14:07:45 +02:00
const override = {
face: { detector: { skipFrames: 0 }, age: { skipFrames: 0 }, emotion: { skipFrames: 0 } },
hand: { skipFrames: 0 },
};
// helper function: gets elapsed time on both browser and nodejs
2020-10-16 16:12:12 +02:00
const now = () => {
if (typeof performance !== 'undefined') return performance.now();
return parseInt(Number(process.hrtime.bigint()) / 1000 / 1000);
};
2020-10-12 01:22:43 +02:00
// helper function: wrapper around console output
2020-10-15 21:25:58 +02:00
const log = (...msg) => {
// eslint-disable-next-line no-console
if (msg && config.console) console.log(...msg);
};
// helper function: measure tensor leak
let numTensors = 0;
const analyzeMemoryLeaks = false;
const analyze = (...msg) => {
if (!analyzeMemoryLeaks) return;
const current = tf.engine().state.numTensors;
const previous = numTensors;
numTensors = current;
const leaked = current - previous;
if (leaked !== 0) log(...msg, leaked);
2020-10-15 21:25:58 +02:00
};
// helper function: perform deep merge of multiple objects so it allows full inheriance with overrides
2020-10-12 01:22:43 +02:00
function mergeDeep(...objects) {
const isObject = (obj) => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
2020-10-12 03:21:41 +02:00
Object.keys(obj || {}).forEach((key) => {
2020-10-12 01:22:43 +02:00
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat(...oVal);
} else if (isObject(pVal) && isObject(oVal)) {
prev[key] = mergeDeep(pVal, oVal);
} else {
prev[key] = oVal;
}
});
return prev;
}, {});
}
2020-10-16 16:12:12 +02:00
function sanity(input) {
if (!input) return 'input is not defined';
if (!(input instanceof tf.Tensor)
|| (tf.ENV.flags.IS_BROWSER
&& (input instanceof ImageData || input instanceof HTMLImageElement || input instanceof HTMLCanvasElement || input instanceof HTMLVideoElement || input instanceof HTMLMediaElement))) {
2020-10-18 14:07:45 +02:00
const width = input.naturalWidth || input.videoWidth || input.width || (input.shape && (input.shape[1] > 0));
if (!width || (width === 0)) return 'input is empty';
}
if (tf.ENV.flags.IS_BROWSER && (input instanceof HTMLVideoElement || input instanceof HTMLMediaElement)) {
if (input.readyState && (input.readyState <= 2)) return 'input is not ready';
}
if (tf.ENV.flags.IS_NODE && !(input instanceof tf.Tensor)) {
return 'input must be a tensor';
}
2020-10-16 16:12:12 +02:00
try {
tf.getBackend();
} catch {
return 'backend not loaded';
}
return null;
}
2020-10-17 13:15:23 +02:00
async function load(userConfig) {
if (userConfig) config = mergeDeep(defaults, userConfig);
if (config.face.enabled && !models.facemesh) models.facemesh = await facemesh.load(config.face);
if (config.body.enabled && !models.posenet) models.posenet = await posenet.load(config.body);
if (config.hand.enabled && !models.handpose) models.handpose = await handpose.load(config.hand);
if (config.face.enabled && config.face.age.enabled && !models.age) models.age = await ssrnet.loadAge(config);
if (config.face.enabled && config.face.gender.enabled && !models.gender) models.gender = await ssrnet.loadGender(config);
if (config.face.enabled && config.face.emotion.enabled && !models.emotion) models.emotion = await emotion.load(config);
}
function tfImage(input) {
let image;
if (input instanceof tf.Tensor) {
image = tf.clone(input);
} else {
const pixels = tf.browser.fromPixels(input);
const casted = pixels.toFloat();
image = casted.expandDims(0);
pixels.dispose();
casted.dispose();
}
return image;
}
2020-10-17 13:15:23 +02:00
async function detect(input, userConfig = {}) {
state = 'config';
2020-10-17 17:38:24 +02:00
const perf = {};
let timeStamp;
timeStamp = now();
2020-10-18 14:07:45 +02:00
const shouldOverride = tf.ENV.flags.IS_NODE || (tf.ENV.flags.IS_BROWSER && !((input instanceof HTMLVideoElement) || (input instanceof HTMLMediaElement)));
config = mergeDeep(defaults, userConfig, shouldOverride ? override : {});
2020-10-17 17:38:24 +02:00
perf.config = Math.trunc(now() - timeStamp);
2020-10-16 16:12:12 +02:00
// sanity checks
2020-10-17 17:38:24 +02:00
timeStamp = now();
2020-10-17 13:15:23 +02:00
state = 'check';
2020-10-16 16:12:12 +02:00
const error = sanity(input);
if (error) {
log(error, input);
return { error };
}
2020-10-17 17:38:24 +02:00
perf.sanity = Math.trunc(now() - timeStamp);
2020-10-16 16:12:12 +02:00
2020-10-14 02:52:30 +02:00
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
2020-10-17 17:38:24 +02:00
const timeStart = now();
2020-10-15 21:25:58 +02:00
// configure backend
2020-10-17 17:38:24 +02:00
timeStamp = now();
2020-10-15 21:25:58 +02:00
if (tf.getBackend() !== config.backend) {
2020-10-17 13:15:23 +02:00
state = 'backend';
2020-10-15 21:25:58 +02:00
log('Human library setting backend:', config.backend);
await tf.setBackend(config.backend);
await tf.ready();
}
2020-10-17 17:43:04 +02:00
perf.backend = Math.trunc(now() - timeStamp);
// check number of loaded models
const loadedModels = Object.values(models).filter((a) => a).length;
if (loadedModels === 0) {
log('Human library starting');
log('Configuration:', config);
log('Flags:', tf.ENV.flags);
}
2020-10-14 17:43:33 +02:00
2020-10-15 21:25:58 +02:00
// load models if enabled
2020-10-17 17:38:24 +02:00
timeStamp = now();
2020-10-17 13:15:23 +02:00
state = 'load';
await load();
2020-10-17 17:38:24 +02:00
perf.load = Math.trunc(now() - timeStamp);
if (config.scoped) tf.engine().startScope();
analyze('Start Detect:');
2020-10-17 13:15:23 +02:00
const imageTensor = tfImage(input);
2020-10-14 02:52:30 +02:00
// run posenet
2020-10-17 13:15:23 +02:00
state = 'run:body';
2020-10-16 16:12:12 +02:00
timeStamp = now();
analyze('Start PoseNet');
const poseRes = config.body.enabled ? await models.posenet.estimatePoses(imageTensor, config.body) : [];
analyze('End PoseNet:');
2020-10-16 16:12:12 +02:00
perf.body = Math.trunc(now() - timeStamp);
2020-10-12 01:22:43 +02:00
2020-10-14 02:52:30 +02:00
// run handpose
2020-10-17 13:15:23 +02:00
state = 'run:hand';
2020-10-16 16:12:12 +02:00
timeStamp = now();
analyze('Start HandPose:');
const handRes = config.hand.enabled ? await models.handpose.estimateHands(imageTensor, config.hand) : [];
analyze('End HandPose:');
2020-10-16 16:12:12 +02:00
perf.hand = Math.trunc(now() - timeStamp);
2020-10-12 01:22:43 +02:00
2020-10-14 02:52:30 +02:00
// run facemesh, includes blazeface and iris
const faceRes = [];
if (config.face.enabled) {
2020-10-17 13:15:23 +02:00
state = 'run:face';
2020-10-16 16:12:12 +02:00
timeStamp = now();
analyze('Start FaceMesh:');
const faces = await models.facemesh.estimateFaces(imageTensor, config.face);
2020-10-16 16:12:12 +02:00
perf.face = Math.trunc(now() - timeStamp);
2020-10-14 02:52:30 +02:00
for (const face of faces) {
2020-10-16 16:12:12 +02:00
// is something went wrong, skip the face
if (!face.image || face.image.isDisposedInternal) {
log('face object is disposed:', face.image);
continue;
}
2020-10-14 02:52:30 +02:00
// run ssr-net age & gender, inherits face from blazeface
2020-10-17 13:15:23 +02:00
state = 'run:agegender';
2020-10-16 16:12:12 +02:00
timeStamp = now();
2020-10-15 15:43:16 +02:00
const ssrData = (config.face.age.enabled || config.face.gender.enabled) ? await ssrnet.predict(face.image, config) : {};
2020-10-16 16:12:12 +02:00
perf.agegender = Math.trunc(now() - timeStamp);
2020-10-15 00:22:38 +02:00
// run emotion, inherits face from blazeface
2020-10-17 13:15:23 +02:00
state = 'run:emotion';
2020-10-16 16:12:12 +02:00
timeStamp = now();
2020-10-15 15:43:16 +02:00
const emotionData = config.face.emotion.enabled ? await emotion.predict(face.image, config) : {};
2020-10-16 16:12:12 +02:00
perf.emotion = Math.trunc(now() - timeStamp);
2020-10-17 17:38:24 +02:00
// dont need face anymore
2020-10-14 02:52:30 +02:00
face.image.dispose();
2020-10-15 00:22:38 +02:00
// calculate iris distance
2020-10-14 02:52:30 +02:00
// iris: array[ bottom, left, top, right, center ]
const iris = (face.annotations.leftEyeIris && face.annotations.rightEyeIris)
? Math.max(face.annotations.leftEyeIris[3][0] - face.annotations.leftEyeIris[1][0], face.annotations.rightEyeIris[3][0] - face.annotations.rightEyeIris[1][0])
: 0;
faceRes.push({
confidence: face.confidence,
box: face.box,
mesh: face.mesh,
annotations: face.annotations,
2020-10-15 15:43:16 +02:00
age: ssrData.age,
gender: ssrData.gender,
2020-10-16 21:04:51 +02:00
agConfidence: ssrData.confidence,
2020-10-15 15:43:16 +02:00
emotion: emotionData,
2020-10-15 00:22:38 +02:00
iris: (iris !== 0) ? Math.trunc(100 * 11.7 /* human iris size in mm */ / iris) / 100 : 0,
2020-10-14 02:52:30 +02:00
});
analyze('End FaceMesh:');
2020-10-14 02:52:30 +02:00
}
2020-10-12 01:22:43 +02:00
}
2020-10-13 04:01:35 +02:00
imageTensor.dispose();
state = 'idle';
2020-10-14 17:43:33 +02:00
if (config.scoped) tf.engine().endScope();
analyze('End Scope:');
2020-10-17 13:15:23 +02:00
2020-10-17 17:38:24 +02:00
perf.total = Math.trunc(now() - timeStart);
resolve({ face: faceRes, body: poseRes, hand: handRes, performance: perf });
2020-10-14 02:52:30 +02:00
});
2020-10-12 01:22:43 +02:00
}
exports.detect = detect;
exports.defaults = defaults;
2020-10-15 21:25:58 +02:00
exports.config = config;
2020-10-12 01:22:43 +02:00
exports.models = models;
2020-10-12 03:21:41 +02:00
exports.facemesh = facemesh;
exports.ssrnet = ssrnet;
exports.posenet = posenet;
exports.handpose = handpose;
2020-10-12 16:08:00 +02:00
exports.tf = tf;
2020-10-15 21:25:58 +02:00
exports.version = app.version;
2020-10-17 13:15:23 +02:00
exports.state = state;
2020-10-18 14:07:45 +02:00
// Error: Failed to compile fragment shader