enhanced age, gender, emotion detection

pull/91/head
Vladimir Mandic 2021-03-10 09:44:45 -05:00
parent b25e7fd459
commit 26f8f76a7b
24 changed files with 443529 additions and 6798 deletions

View File

@ -1,7 +1,6 @@
# To-Do list for Human library # To-Do list for Human library
- Fix BlazeFace NodeJS missing ops - Strong typing
- Prune pre-packaged models
- Build Face embedding database - Build Face embedding database
- Dynamic sample processing - Dynamic sample processing
- Optimize for v1 release - Explore EfficientPose: <https://github.com/daniegr/EfficientPose> <https://github.com/PINTO0309/PINTO_model_zoo/tree/main/084_EfficientPose>

View File

@ -80,10 +80,10 @@ export default {
// in short time (10 * 1/25 = 0.25 sec) // in short time (10 * 1/25 = 0.25 sec)
skipInitial: false, // if previous detection resulted in no faces detected, skipInitial: false, // if previous detection resulted in no faces detected,
// should skipFrames be reset immediately // should skipFrames be reset immediately
minConfidence: 0.1, // threshold for discarding a prediction minConfidence: 0.2, // threshold for discarding a prediction
iouThreshold: 0.1, // threshold for deciding whether boxes overlap too much in iouThreshold: 0.1, // threshold for deciding whether boxes overlap too much in
// non-maximum suppression (0.1 means drop if overlap 10%) // non-maximum suppression (0.1 means drop if overlap 10%)
scoreThreshold: 0.1, // threshold for deciding when to remove boxes based on score scoreThreshold: 0.2, // threshold for deciding when to remove boxes based on score
// in non-maximum suppression, // in non-maximum suppression,
// this is applied on detection objects only and before minConfidence // this is applied on detection objects only and before minConfidence
}, },

View File

@ -1,4 +1,5 @@
import Human from '../dist/human.esm.js'; // equivalent of @vladmandic/human // import Human from '../dist/human.esm.js'; // equivalent of @vladmandic/human
import Human from '../src/human';
import Menu from './menu.js'; import Menu from './menu.js';
import GLBench from './gl-bench.js'; import GLBench from './gl-bench.js';
@ -8,7 +9,7 @@ const userConfig = { backend: 'webgl' }; // add any user configuration overrides
const userConfig = { const userConfig = {
backend: 'wasm', backend: 'wasm',
async: false, async: false,
warmup: 'face', warmup: 'none',
videoOptimized: false, videoOptimized: false,
face: { enabled: true, mesh: { enabled: false }, iris: { enabled: false }, age: { enabled: false }, gender: { enabled: false }, emotion: { enabled: false }, embedding: { enabled: false } }, face: { enabled: true, mesh: { enabled: false }, iris: { enabled: false }, age: { enabled: false }, gender: { enabled: false }, emotion: { enabled: false }, embedding: { enabled: false } },
hand: { enabled: false }, hand: { enabled: false },
@ -360,7 +361,6 @@ async function processImage(input) {
// just initialize everything and call main function // just initialize everything and call main function
async function detectVideo() { async function detectVideo() {
userConfig.videoOptimized = true;
document.getElementById('samples-container').style.display = 'none'; document.getElementById('samples-container').style.display = 'none';
document.getElementById('canvas').style.display = 'block'; document.getElementById('canvas').style.display = 'block';
const video = document.getElementById('video'); const video = document.getElementById('video');
@ -389,8 +389,8 @@ async function detectVideo() {
// just initialize everything and call main function // just initialize everything and call main function
async function detectSampleImages() { async function detectSampleImages() {
userConfig.videoOptimized = false; // force disable video optimizations
document.getElementById('play').style.display = 'none'; document.getElementById('play').style.display = 'none';
userConfig.videoOptimized = false;
document.getElementById('canvas').style.display = 'none'; document.getElementById('canvas').style.display = 'none';
document.getElementById('samples-container').style.display = 'block'; document.getElementById('samples-container').style.display = 'block';
log('Running detection of sample images'); log('Running detection of sample images');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

98981
dist/human.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

98993
dist/human.js vendored

File diff suppressed because one or more lines are too long

4
dist/human.js.map vendored

File diff suppressed because one or more lines are too long

25029
dist/human.node-gpu.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

25029
dist/human.node.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

76712
dist/tfjs.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -75,26 +75,26 @@ export class BlazeFaceModel {
async getBoundingBoxes(inputImage) { async getBoundingBoxes(inputImage) {
// sanity check on input // sanity check on input
if ((!inputImage) || (inputImage.isDisposedInternal) || (inputImage.shape.length !== 4) || (inputImage.shape[1] < 1) || (inputImage.shape[2] < 1)) return null; if ((!inputImage) || (inputImage.isDisposedInternal) || (inputImage.shape.length !== 4) || (inputImage.shape[1] < 1) || (inputImage.shape[2] < 1)) return null;
const [detectedOutputs, boxes, scores] = tf.tidy(() => { const [batch, boxes, scores] = tf.tidy(() => {
const resizedImage = inputImage.resizeBilinear([this.width, this.height]); const resizedImage = inputImage.resizeBilinear([this.width, this.height]);
// const normalizedImage = tf.mul(tf.sub(resizedImage.div(255), 0.5), 2); // const normalizedImage = tf.mul(tf.sub(resizedImage.div(255), 0.5), 2);
const normalizedImage = resizedImage.div(127.5).sub(0.5); const normalizedImage = resizedImage.div(127.5).sub(0.5);
const batchedPrediction = this.blazeFaceModel.predict(normalizedImage); const batchedPrediction = this.blazeFaceModel.predict(normalizedImage);
let prediction; let batchOut;
// are we using tfhub or pinto converted model? // are we using tfhub or pinto converted model?
if (Array.isArray(batchedPrediction)) { if (Array.isArray(batchedPrediction)) {
const sorted = batchedPrediction.sort((a, b) => a.size - b.size); const sorted = batchedPrediction.sort((a, b) => a.size - b.size);
const concat384 = tf.concat([sorted[0], sorted[2]], 2); // dim: 384, 1 + 16 const concat384 = tf.concat([sorted[0], sorted[2]], 2); // dim: 384, 1 + 16
const concat512 = tf.concat([sorted[1], sorted[3]], 2); // dim: 512, 1 + 16 const concat512 = tf.concat([sorted[1], sorted[3]], 2); // dim: 512, 1 + 16
const concat = tf.concat([concat512, concat384], 1); const concat = tf.concat([concat512, concat384], 1);
prediction = concat.squeeze(0); batchOut = concat.squeeze(0);
} else { } else {
prediction = batchedPrediction.squeeze(); // when using tfhub model batchOut = batchedPrediction.squeeze(); // when using tfhub model
} }
const decodedBounds = decodeBounds(prediction, this.anchors, this.inputSize); const boxesOut = decodeBounds(batchOut, this.anchors, this.inputSize);
const logits = tf.slice(prediction, [0, 0], [-1, 1]); const logits = tf.slice(batchOut, [0, 0], [-1, 1]);
const scoresOut = tf.sigmoid(logits).squeeze(); const scoresOut = tf.sigmoid(logits).squeeze();
return [prediction, decodedBounds, scoresOut]; return [batchOut, boxesOut, scoresOut];
}); });
const boxIndicesTensor = await tf.image.nonMaxSuppressionAsync(boxes, scores, this.config.face.detector.maxFaces, this.config.face.detector.iouThreshold, this.config.face.detector.scoreThreshold); const boxIndicesTensor = await tf.image.nonMaxSuppressionAsync(boxes, scores, this.config.face.detector.maxFaces, this.config.face.detector.iouThreshold, this.config.face.detector.scoreThreshold);
const boxIndices = boxIndicesTensor.arraySync(); const boxIndices = boxIndicesTensor.arraySync();
@ -114,14 +114,13 @@ export class BlazeFaceModel {
if (confidence > this.config.face.detector.minConfidence) { if (confidence > this.config.face.detector.minConfidence) {
const box = createBox(boundingBoxes[i]); const box = createBox(boundingBoxes[i]);
const anchor = this.anchorsData[boxIndex]; const anchor = this.anchorsData[boxIndex];
const landmarks = tf.tidy(() => tf.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]).squeeze().reshape([NUM_LANDMARKS, -1])); const landmarks = tf.tidy(() => tf.slice(batch, [boxIndex, NUM_LANDMARKS - 1], [1, -1]).squeeze().reshape([NUM_LANDMARKS, -1]));
annotatedBoxes.push({ box, landmarks, anchor, confidence }); annotatedBoxes.push({ box, landmarks, anchor, confidence });
} }
} }
detectedOutputs.dispose(); batch.dispose();
boxes.dispose(); boxes.dispose();
scores.dispose(); scores.dispose();
detectedOutputs.dispose();
return { return {
boxes: annotatedBoxes, boxes: annotatedBoxes,
scaleFactor: [inputImage.shape[2] / this.width, inputImage.shape[1] / this.height], scaleFactor: [inputImage.shape[2] / this.width, inputImage.shape[1] / this.height],

View File

@ -9,7 +9,6 @@ let skipped = Number.MAX_SAFE_INTEGER;
// tuning values // tuning values
const rgb = [0.2989, 0.5870, 0.1140]; // factors for red/green/blue colors when converting to grayscale const rgb = [0.2989, 0.5870, 0.1140]; // factors for red/green/blue colors when converting to grayscale
const scale = 1; // score multiplication factor
export async function load(config) { export async function load(config) {
if (!model) { if (!model) {
@ -58,7 +57,7 @@ export async function predict(image, config) {
if (config.face.emotion.enabled) { if (config.face.emotion.enabled) {
let data; let data;
if (!config.profile) { if (!config.profile) {
const emotionT = await model.predict(normalize); const emotionT = await model.predict(normalize); // result is already in range 0..1, no need for additional activation
data = emotionT.dataSync(); data = emotionT.dataSync();
tf.dispose(emotionT); tf.dispose(emotionT);
} else { } else {
@ -68,7 +67,7 @@ export async function predict(image, config) {
profile.run('emotion', profileData); profile.run('emotion', profileData);
} }
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
if (scale * data[i] > config.face.emotion.minConfidence) obj.push({ score: Math.min(0.99, Math.trunc(100 * scale * data[i]) / 100), emotion: annotations[i] }); if (data[i] > config.face.emotion.minConfidence) obj.push({ score: Math.min(0.99, Math.trunc(100 * data[i]) / 100), emotion: annotations[i] });
} }
obj.sort((a, b) => b.score - a.score); obj.sort((a, b) => b.score - a.score);
} }

View File

@ -37,10 +37,11 @@ export async function predict(image, config) {
const greenNorm = tf.mul(green, rgb[1]); const greenNorm = tf.mul(green, rgb[1]);
const blueNorm = tf.mul(blue, rgb[2]); const blueNorm = tf.mul(blue, rgb[2]);
const grayscale = tf.addN([redNorm, greenNorm, blueNorm]); const grayscale = tf.addN([redNorm, greenNorm, blueNorm]);
return grayscale.sub(0.5).mul(2); const normalize = grayscale.sub(0.5).mul(2); // range grayscale:-1..1
return normalize;
}); });
} else { } else {
enhance = tf.mul(resize, [255.0]); enhance = tf.mul(resize, [255.0]); // range RGB:0..255
} }
tf.dispose(resize); tf.dispose(resize);
@ -61,10 +62,9 @@ export async function predict(image, config) {
const data = genderT.dataSync(); const data = genderT.dataSync();
if (alternative) { if (alternative) {
// returns two values 0..1, bigger one is prediction // returns two values 0..1, bigger one is prediction
const confidence = Math.trunc(100 * Math.abs(data[0] - data[1])) / 100; if (data[0] > config.face.gender.minConfidence || data[1] > config.face.gender.minConfidence) {
if (confidence > config.face.gender.minConfidence) {
obj.gender = data[0] > data[1] ? 'female' : 'male'; obj.gender = data[0] > data[1] ? 'female' : 'male';
obj.confidence = confidence; obj.confidence = data[0] > data[1] ? (Math.trunc(100 * data[0]) / 100) : (Math.trunc(100 * data[1]) / 100);
} }
} else { } else {
// returns one value 0..1, .5 is prediction threshold // returns one value 0..1, .5 is prediction threshold

View File

@ -40,45 +40,57 @@ function mergeDeep(...objects) {
} }
class Human { class Human {
tf: any;
draw: any;
package: any;
version: string; version: string;
config: any; config: typeof config.default;
fx: any;
state: string; state: string;
numTensors: number; image: { tensor, canvas };
analyzeMemoryLeaks: boolean; // classes
checkSanity: boolean; tf: typeof tf;
firstRun: boolean; draw: typeof draw;
perf: any;
image: any;
models: any;
// models // models
facemesh: any; models: {
age: any; face,
gender: any; posenet,
emotion: any; blazepose,
body: any; handpose,
hand: any; iris,
sysinfo: any; age,
gender,
emotion,
embedding,
};
classes: {
facemesh: typeof facemesh;
age: typeof age;
gender: typeof gender;
emotion: typeof emotion;
body: typeof posenet | typeof blazepose;
hand: typeof handpose;
};
sysinfo: { platform, agent };
#package: any;
#perf: any;
#numTensors: number;
#analyzeMemoryLeaks: boolean;
#checkSanity: boolean;
#firstRun: boolean;
// definition end
constructor(userConfig = {}) { constructor(userConfig = {}) {
this.tf = tf; this.tf = tf;
this.draw = draw; this.draw = draw;
this.package = app; this.#package = app;
this.version = app.version; this.version = app.version;
this.config = mergeDeep(config.default, userConfig); this.config = mergeDeep(config.default, userConfig);
this.fx = null;
this.state = 'idle'; this.state = 'idle';
this.numTensors = 0; this.#numTensors = 0;
this.analyzeMemoryLeaks = false; this.#analyzeMemoryLeaks = false;
this.checkSanity = false; this.#checkSanity = false;
this.firstRun = true; this.#firstRun = true;
this.perf = {}; this.#perf = {};
// object that contains all initialized models // object that contains all initialized models
this.models = { this.models = {
facemesh: null, face: null,
posenet: null, posenet: null,
blazepose: null, blazepose: null,
handpose: null, handpose: null,
@ -86,38 +98,42 @@ class Human {
age: null, age: null,
gender: null, gender: null,
emotion: null, emotion: null,
embedding: null,
}; };
// export access to image processing // export access to image processing
this.image = (input) => image.process(input, this.config); // @ts-ignore
this.image = (input: any) => image.process(input, this.config);
// export raw access to underlying models // export raw access to underlying models
this.facemesh = facemesh; this.classes = {
this.age = age; facemesh,
this.gender = gender; age,
this.emotion = emotion; gender,
this.body = this.config.body.modelType.startsWith('posenet') ? posenet : blazepose; emotion,
this.hand = handpose; body: this.config.body.modelType.startsWith('posenet') ? posenet : blazepose,
hand: handpose,
};
// include platform info // include platform info
this.sysinfo = sysinfo.info(); this.sysinfo = sysinfo.info();
} }
profile() { profileData(): { newBytes, newTensors, peakBytes, numKernelOps, timeKernelOps, slowestKernelOps, largestKernelOps } | {} {
if (this.config.profile) return profile.data; if (this.config.profile) return profile.data;
return {}; return {};
} }
// helper function: measure tensor leak // helper function: measure tensor leak
analyze(...msg) { #analyze = (...msg) => {
if (!this.analyzeMemoryLeaks) return; if (!this.#analyzeMemoryLeaks) return;
const current = this.tf.engine().state.numTensors; const current = this.tf.engine().state.numTensors;
const previous = this.numTensors; const previous = this.#numTensors;
this.numTensors = current; this.#numTensors = current;
const leaked = current - previous; const leaked = current - previous;
if (leaked !== 0) log(...msg, leaked); if (leaked !== 0) log(...msg, leaked);
} }
// quick sanity check on inputs // quick sanity check on inputs
sanity(input) { #sanity = (input) => {
if (!this.checkSanity) return null; if (!this.#checkSanity) return null;
if (!input) return 'input is not defined'; if (!input) return 'input is not defined';
if (this.tf.ENV.flags.IS_NODE && !(input instanceof this.tf.Tensor)) { if (this.tf.ENV.flags.IS_NODE && !(input instanceof this.tf.Tensor)) {
return 'input must be a tensor'; return 'input must be a tensor';
@ -130,7 +146,7 @@ class Human {
return null; return null;
} }
simmilarity(embedding1, embedding2) { simmilarity(embedding1, embedding2): number {
if (this.config.face.embedding.enabled) return embedding.simmilarity(embedding1, embedding2); if (this.config.face.embedding.enabled) return embedding.simmilarity(embedding1, embedding2);
return 0; return 0;
} }
@ -141,13 +157,13 @@ class Human {
const timeStamp = now(); const timeStamp = now();
if (userConfig) this.config = mergeDeep(this.config, userConfig); if (userConfig) this.config = mergeDeep(this.config, userConfig);
if (this.firstRun) { if (this.#firstRun) {
if (this.config.debug) log(`version: ${this.version}`); if (this.config.debug) log(`version: ${this.version}`);
if (this.config.debug) log(`tfjs version: ${this.tf.version_core}`); if (this.config.debug) log(`tfjs version: ${this.tf.version_core}`);
if (this.config.debug) log('platform:', this.sysinfo.platform); if (this.config.debug) log('platform:', this.sysinfo.platform);
if (this.config.debug) log('agent:', this.sysinfo.agent); if (this.config.debug) log('agent:', this.sysinfo.agent);
await this.checkBackend(true); await this.#checkBackend(true);
if (this.tf.ENV.flags.IS_BROWSER) { if (this.tf.ENV.flags.IS_BROWSER) {
if (this.config.debug) log('configuration:', this.config); if (this.config.debug) log('configuration:', this.config);
if (this.config.debug) log('tf flags:', this.tf.ENV.flags); if (this.config.debug) log('tf flags:', this.tf.ENV.flags);
@ -184,17 +200,17 @@ class Human {
if (this.config.body.enabled && !this.models.blazepose && this.config.body.modelType.startsWith('blazepose')) this.models.blazepose = await blazepose.load(this.config); if (this.config.body.enabled && !this.models.blazepose && this.config.body.modelType.startsWith('blazepose')) this.models.blazepose = await blazepose.load(this.config);
} }
if (this.firstRun) { if (this.#firstRun) {
if (this.config.debug) log('tf engine state:', this.tf.engine().state.numBytes, 'bytes', this.tf.engine().state.numTensors, 'tensors'); if (this.config.debug) log('tf engine state:', this.tf.engine().state.numBytes, 'bytes', this.tf.engine().state.numTensors, 'tensors');
this.firstRun = false; this.#firstRun = false;
} }
const current = Math.trunc(now() - timeStamp); const current = Math.trunc(now() - timeStamp);
if (current > (this.perf.load || 0)) this.perf.load = current; if (current > (this.#perf.load || 0)) this.#perf.load = current;
} }
// check if backend needs initialization if it changed // check if backend needs initialization if it changed
async checkBackend(force = false) { #checkBackend = async (force = false) => {
if (this.config.backend && (this.config.backend !== '') && force || (this.tf.getBackend() !== this.config.backend)) { if (this.config.backend && (this.config.backend !== '') && force || (this.tf.getBackend() !== this.config.backend)) {
const timeStamp = now(); const timeStamp = now();
this.state = 'backend'; this.state = 'backend';
@ -242,11 +258,11 @@ class Human {
if (this.config.debug) log(`gl version:${gl.getParameter(gl.VERSION)} renderer:${gl.getParameter(gl.RENDERER)}`); if (this.config.debug) log(`gl version:${gl.getParameter(gl.VERSION)} renderer:${gl.getParameter(gl.RENDERER)}`);
} }
await this.tf.ready(); await this.tf.ready();
this.perf.backend = Math.trunc(now() - timeStamp); this.#perf.backend = Math.trunc(now() - timeStamp);
} }
} }
calculateFaceAngle = (mesh) => { #calculateFaceAngle = (mesh) => {
if (!mesh || mesh.length < 300) return {}; if (!mesh || mesh.length < 300) return {};
const radians = (a1, a2, b1, b2) => Math.atan2(b2 - a2, b1 - a1); const radians = (a1, a2, b1, b2) => Math.atan2(b2 - a2, b1 - a1);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
@ -264,7 +280,7 @@ class Human {
return angle; return angle;
} }
async detectFace(input) { #detectFace = async (input) => {
// run facemesh, includes blazeface and iris // run facemesh, includes blazeface and iris
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
let timeStamp; let timeStamp;
@ -293,9 +309,9 @@ class Human {
this.state = 'run:face'; this.state = 'run:face';
timeStamp = now(); timeStamp = now();
const faces = await this.models.face?.estimateFaces(input, this.config); const faces = await this.models.face?.estimateFaces(input, this.config);
this.perf.face = Math.trunc(now() - timeStamp); this.#perf.face = Math.trunc(now() - timeStamp);
for (const face of faces) { for (const face of faces) {
this.analyze('Get Face'); this.#analyze('Get Face');
// is something went wrong, skip the face // is something went wrong, skip the face
if (!face.image || face.image.isDisposedInternal) { if (!face.image || face.image.isDisposedInternal) {
@ -303,60 +319,60 @@ class Human {
continue; continue;
} }
const angle = this.calculateFaceAngle(face.mesh); const angle = this.#calculateFaceAngle(face.mesh);
// run age, inherits face from blazeface // run age, inherits face from blazeface
this.analyze('Start Age:'); this.#analyze('Start Age:');
if (this.config.async) { if (this.config.async) {
ageRes = this.config.face.age.enabled ? age.predict(face.image, this.config) : {}; ageRes = this.config.face.age.enabled ? age.predict(face.image, this.config) : {};
} else { } else {
this.state = 'run:age'; this.state = 'run:age';
timeStamp = now(); timeStamp = now();
ageRes = this.config.face.age.enabled ? await age.predict(face.image, this.config) : {}; ageRes = this.config.face.age.enabled ? await age.predict(face.image, this.config) : {};
this.perf.age = Math.trunc(now() - timeStamp); this.#perf.age = Math.trunc(now() - timeStamp);
} }
// run gender, inherits face from blazeface // run gender, inherits face from blazeface
this.analyze('Start Gender:'); this.#analyze('Start Gender:');
if (this.config.async) { if (this.config.async) {
genderRes = this.config.face.gender.enabled ? gender.predict(face.image, this.config) : {}; genderRes = this.config.face.gender.enabled ? gender.predict(face.image, this.config) : {};
} else { } else {
this.state = 'run:gender'; this.state = 'run:gender';
timeStamp = now(); timeStamp = now();
genderRes = this.config.face.gender.enabled ? await gender.predict(face.image, this.config) : {}; genderRes = this.config.face.gender.enabled ? await gender.predict(face.image, this.config) : {};
this.perf.gender = Math.trunc(now() - timeStamp); this.#perf.gender = Math.trunc(now() - timeStamp);
} }
// run emotion, inherits face from blazeface // run emotion, inherits face from blazeface
this.analyze('Start Emotion:'); this.#analyze('Start Emotion:');
if (this.config.async) { if (this.config.async) {
emotionRes = this.config.face.emotion.enabled ? emotion.predict(face.image, this.config) : {}; emotionRes = this.config.face.emotion.enabled ? emotion.predict(face.image, this.config) : {};
} else { } else {
this.state = 'run:emotion'; this.state = 'run:emotion';
timeStamp = now(); timeStamp = now();
emotionRes = this.config.face.emotion.enabled ? await emotion.predict(face.image, this.config) : {}; emotionRes = this.config.face.emotion.enabled ? await emotion.predict(face.image, this.config) : {};
this.perf.emotion = Math.trunc(now() - timeStamp); this.#perf.emotion = Math.trunc(now() - timeStamp);
} }
this.analyze('End Emotion:'); this.#analyze('End Emotion:');
// run emotion, inherits face from blazeface // run emotion, inherits face from blazeface
this.analyze('Start Embedding:'); this.#analyze('Start Embedding:');
if (this.config.async) { if (this.config.async) {
embeddingRes = this.config.face.embedding.enabled ? embedding.predict(face.image, this.config) : []; embeddingRes = this.config.face.embedding.enabled ? embedding.predict(face.image, this.config) : [];
} else { } else {
this.state = 'run:embedding'; this.state = 'run:embedding';
timeStamp = now(); timeStamp = now();
embeddingRes = this.config.face.embedding.enabled ? await embedding.predict(face.image, this.config) : []; embeddingRes = this.config.face.embedding.enabled ? await embedding.predict(face.image, this.config) : [];
this.perf.embedding = Math.trunc(now() - timeStamp); this.#perf.embedding = Math.trunc(now() - timeStamp);
} }
this.analyze('End Emotion:'); this.#analyze('End Emotion:');
// if async wait for results // if async wait for results
if (this.config.async) { if (this.config.async) {
[ageRes, genderRes, emotionRes, embeddingRes] = await Promise.all([ageRes, genderRes, emotionRes, embeddingRes]); [ageRes, genderRes, emotionRes, embeddingRes] = await Promise.all([ageRes, genderRes, emotionRes, embeddingRes]);
} }
this.analyze('Finish Face:'); this.#analyze('Finish Face:');
// calculate iris distance // calculate iris distance
// iris: array[ center, left, top, right, bottom] // iris: array[ center, left, top, right, bottom]
@ -391,20 +407,20 @@ class Human {
// dont need face anymore // dont need face anymore
face.image?.dispose(); face.image?.dispose();
this.analyze('End Face'); this.#analyze('End Face');
} }
this.analyze('End FaceMesh:'); this.#analyze('End FaceMesh:');
if (this.config.async) { if (this.config.async) {
if (this.perf.face) delete this.perf.face; if (this.#perf.face) delete this.#perf.face;
if (this.perf.age) delete this.perf.age; if (this.#perf.age) delete this.#perf.age;
if (this.perf.gender) delete this.perf.gender; if (this.#perf.gender) delete this.#perf.gender;
if (this.perf.emotion) delete this.perf.emotion; if (this.#perf.emotion) delete this.#perf.emotion;
} }
return faceRes; return faceRes;
} }
// main detect function // main detect function
async detect(input, userConfig = {}) { async detect(input, userConfig = {}): Promise<{ face, body, hand, gesture, performance, canvas } | { error }> {
// detection happens inside a promise // detection happens inside a promise
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
this.state = 'config'; this.state = 'config';
@ -415,7 +431,7 @@ class Human {
// sanity checks // sanity checks
this.state = 'check'; this.state = 'check';
const error = this.sanity(input); const error = this.#sanity(input);
if (error) { if (error) {
log(error, input); log(error, input);
resolve({ error }); resolve({ error });
@ -424,13 +440,13 @@ class Human {
const timeStart = now(); const timeStart = now();
// configure backend // configure backend
await this.checkBackend(); await this.#checkBackend();
// load models if enabled // load models if enabled
await this.load(); await this.load();
if (this.config.scoped) this.tf.engine().startScope(); if (this.config.scoped) this.tf.engine().startScope();
this.analyze('Start Scope:'); this.#analyze('Start Scope:');
timeStamp = now(); timeStamp = now();
const process = image.process(input, this.config); const process = image.process(input, this.config);
@ -439,8 +455,8 @@ class Human {
resolve({ error: 'could not convert input to tensor' }); resolve({ error: 'could not convert input to tensor' });
return; return;
} }
this.perf.image = Math.trunc(now() - timeStamp); this.#perf.image = Math.trunc(now() - timeStamp);
this.analyze('Get Image:'); this.#analyze('Get Image:');
// prepare where to store model results // prepare where to store model results
let bodyRes; let bodyRes;
@ -449,42 +465,42 @@ class Human {
// run face detection followed by all models that rely on face bounding box: face mesh, age, gender, emotion // run face detection followed by all models that rely on face bounding box: face mesh, age, gender, emotion
if (this.config.async) { if (this.config.async) {
faceRes = this.config.face.enabled ? this.detectFace(process.tensor) : []; faceRes = this.config.face.enabled ? this.#detectFace(process.tensor) : [];
if (this.perf.face) delete this.perf.face; if (this.#perf.face) delete this.#perf.face;
} else { } else {
this.state = 'run:face'; this.state = 'run:face';
timeStamp = now(); timeStamp = now();
faceRes = this.config.face.enabled ? await this.detectFace(process.tensor) : []; faceRes = this.config.face.enabled ? await this.#detectFace(process.tensor) : [];
this.perf.face = Math.trunc(now() - timeStamp); this.#perf.face = Math.trunc(now() - timeStamp);
} }
// run body: can be posenet or blazepose // run body: can be posenet or blazepose
this.analyze('Start Body:'); this.#analyze('Start Body:');
if (this.config.async) { if (this.config.async) {
if (this.config.body.modelType.startsWith('posenet')) bodyRes = this.config.body.enabled ? this.models.posenet?.estimatePoses(process.tensor, this.config) : []; if (this.config.body.modelType.startsWith('posenet')) bodyRes = this.config.body.enabled ? this.models.posenet?.estimatePoses(process.tensor, this.config) : [];
else bodyRes = this.config.body.enabled ? blazepose.predict(process.tensor, this.config) : []; else bodyRes = this.config.body.enabled ? blazepose.predict(process.tensor, this.config) : [];
if (this.perf.body) delete this.perf.body; if (this.#perf.body) delete this.#perf.body;
} else { } else {
this.state = 'run:body'; this.state = 'run:body';
timeStamp = now(); timeStamp = now();
if (this.config.body.modelType.startsWith('posenet')) bodyRes = this.config.body.enabled ? await this.models.posenet?.estimatePoses(process.tensor, this.config) : []; if (this.config.body.modelType.startsWith('posenet')) bodyRes = this.config.body.enabled ? await this.models.posenet?.estimatePoses(process.tensor, this.config) : [];
else bodyRes = this.config.body.enabled ? await blazepose.predict(process.tensor, this.config) : []; else bodyRes = this.config.body.enabled ? await blazepose.predict(process.tensor, this.config) : [];
this.perf.body = Math.trunc(now() - timeStamp); this.#perf.body = Math.trunc(now() - timeStamp);
} }
this.analyze('End Body:'); this.#analyze('End Body:');
// run handpose // run handpose
this.analyze('Start Hand:'); this.#analyze('Start Hand:');
if (this.config.async) { if (this.config.async) {
handRes = this.config.hand.enabled ? this.models.handpose?.estimateHands(process.tensor, this.config) : []; handRes = this.config.hand.enabled ? this.models.handpose?.estimateHands(process.tensor, this.config) : [];
if (this.perf.hand) delete this.perf.hand; if (this.#perf.hand) delete this.#perf.hand;
} else { } else {
this.state = 'run:hand'; this.state = 'run:hand';
timeStamp = now(); timeStamp = now();
handRes = this.config.hand.enabled ? await this.models.handpose?.estimateHands(process.tensor, this.config) : []; handRes = this.config.hand.enabled ? await this.models.handpose?.estimateHands(process.tensor, this.config) : [];
this.perf.hand = Math.trunc(now() - timeStamp); this.#perf.hand = Math.trunc(now() - timeStamp);
} }
this.analyze('End Hand:'); this.#analyze('End Hand:');
// if async wait for results // if async wait for results
if (this.config.async) { if (this.config.async) {
@ -493,24 +509,24 @@ class Human {
process.tensor.dispose(); process.tensor.dispose();
if (this.config.scoped) this.tf.engine().endScope(); if (this.config.scoped) this.tf.engine().endScope();
this.analyze('End Scope:'); this.#analyze('End Scope:');
let gestureRes = []; let gestureRes = [];
if (this.config.gesture.enabled) { if (this.config.gesture.enabled) {
timeStamp = now(); timeStamp = now();
// @ts-ignore // @ts-ignore
gestureRes = [...gesture.face(faceRes), ...gesture.body(bodyRes), ...gesture.hand(handRes), ...gesture.iris(faceRes)]; gestureRes = [...gesture.face(faceRes), ...gesture.body(bodyRes), ...gesture.hand(handRes), ...gesture.iris(faceRes)];
if (!this.config.async) this.perf.gesture = Math.trunc(now() - timeStamp); if (!this.config.async) this.#perf.gesture = Math.trunc(now() - timeStamp);
else if (this.perf.gesture) delete this.perf.gesture; else if (this.#perf.gesture) delete this.#perf.gesture;
} }
this.perf.total = Math.trunc(now() - timeStart); this.#perf.total = Math.trunc(now() - timeStart);
this.state = 'idle'; this.state = 'idle';
resolve({ face: faceRes, body: bodyRes, hand: handRes, gesture: gestureRes, performance: this.perf, canvas: process.canvas }); resolve({ face: faceRes, body: bodyRes, hand: handRes, gesture: gestureRes, performance: this.#perf, canvas: process.canvas });
}); });
} }
async warmupBitmap() { #warmupBitmap = async () => {
const b64toBlob = (base64, type = 'application/octet-stream') => fetch(`data:${type};base64,${base64}`).then((res) => res.blob()); const b64toBlob = (base64, type = 'application/octet-stream') => fetch(`data:${type};base64,${base64}`).then((res) => res.blob());
let blob; let blob;
let res; let res;
@ -527,8 +543,7 @@ class Human {
return res; return res;
} }
async warmupCanvas() { #warmupCanvas = async () => new Promise((resolve) => {
return new Promise((resolve) => {
let src; let src;
let size = 0; let size = 0;
switch (this.config.warmup) { switch (this.config.warmup) {
@ -559,9 +574,8 @@ class Human {
if (src) img.src = src; if (src) img.src = src;
else resolve(null); else resolve(null);
}); });
}
async warmupNode() { #warmupNode = async () => {
const atob = (str) => Buffer.from(str, 'base64'); const atob = (str) => Buffer.from(str, 'base64');
const img = this.config.warmup === 'face' ? atob(sample.face) : atob(sample.body); const img = this.config.warmup === 'face' ? atob(sample.face) : atob(sample.body);
// @ts-ignore // @ts-ignore
@ -574,15 +588,15 @@ class Human {
return res; return res;
} }
async warmup(userConfig) { async warmup(userConfig): Promise<{ face, body, hand, gesture, performance, canvas } | { error }> {
const t0 = now(); const t0 = now();
if (userConfig) this.config = mergeDeep(this.config, userConfig); if (userConfig) this.config = mergeDeep(this.config, userConfig);
const video = this.config.videoOptimized; const video = this.config.videoOptimized;
this.config.videoOptimized = false; this.config.videoOptimized = false;
let res; let res;
if (typeof createImageBitmap === 'function') res = await this.warmupBitmap(); if (typeof createImageBitmap === 'function') res = await this.#warmupBitmap();
else if (typeof Image !== 'undefined') res = await this.warmupCanvas(); else if (typeof Image !== 'undefined') res = await this.#warmupCanvas();
else res = await this.warmupNode(); else res = await this.#warmupNode();
this.config.videoOptimized = video; this.config.videoOptimized = video;
const t1 = now(); const t1 = now();
if (this.config.debug) log('Warmup', this.config.warmup, Math.round(t1 - t0), 'ms', res); if (this.config.debug) log('Warmup', this.config.warmup, Math.round(t1 - t0), 'ms', res);

View File

@ -13,7 +13,7 @@ let fx = null;
// process input image and return tensor // process input image and return tensor
// input can be tensor, imagedata, htmlimageelement, htmlvideoelement // input can be tensor, imagedata, htmlimageelement, htmlvideoelement
// input is resized and run through imagefx filter // input is resized and run through imagefx filter
export function process(input, config) { export function process(input, config): { tensor, canvas } {
let tensor; let tensor;
if (input instanceof tf.Tensor) { if (input instanceof tf.Tensor) {
tensor = tf.clone(input); tensor = tf.clone(input);
@ -28,7 +28,7 @@ export function process(input, config) {
else if (config.filter.width > 0) targetHeight = originalHeight * (config.filter.width / originalWidth); else if (config.filter.width > 0) targetHeight = originalHeight * (config.filter.width / originalWidth);
if (!targetWidth || !targetHeight) { if (!targetWidth || !targetHeight) {
log('Human: invalid input', input); log('Human: invalid input', input);
return null; return { tensor: null, canvas: null };
} }
if (!inCanvas || (inCanvas.width !== targetWidth) || (inCanvas.height !== targetHeight)) { if (!inCanvas || (inCanvas.width !== targetWidth) || (inCanvas.height !== targetHeight)) {
inCanvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(targetWidth, targetHeight) : document.createElement('canvas'); inCanvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(targetWidth, targetHeight) : document.createElement('canvas');
@ -46,7 +46,7 @@ export function process(input, config) {
// log('created FX filter'); // log('created FX filter');
fx = tf.ENV.flags.IS_BROWSER ? new fxImage.GLImageFilter({ canvas: outCanvas }) : null; // && (typeof document !== 'undefined') fx = tf.ENV.flags.IS_BROWSER ? new fxImage.GLImageFilter({ canvas: outCanvas }) : null; // && (typeof document !== 'undefined')
} }
if (!fx) return inCanvas; if (!fx) return { tensor: null, canvas: inCanvas };
fx.reset(); fx.reset();
fx.addFilter('brightness', config.filter.brightness); // must have at least one filter enabled fx.addFilter('brightness', config.filter.brightness); // must have at least one filter enabled
if (config.filter.contrast !== 0) fx.addFilter('contrast', config.filter.contrast); if (config.filter.contrast !== 0) fx.addFilter('contrast', config.filter.contrast);
@ -110,5 +110,6 @@ export function process(input, config) {
pixels.dispose(); pixels.dispose();
casted.dispose(); casted.dispose();
} }
return { tensor, canvas: config.filter.return ? outCanvas : null }; const canvas = config.filter.return ? outCanvas : null;
return { tensor, canvas };
} }

110
types/human.d.ts vendored
View File

@ -1,54 +1,78 @@
import * as tf from '../dist/tfjs.esm.js';
import * as facemesh from './blazeface/facemesh';
import * as age from './age/age';
import * as gender from './gender/gender';
import * as emotion from './emotion/emotion';
import * as posenet from './posenet/posenet';
import * as handpose from './handpose/handpose';
import * as blazepose from './blazepose/blazepose';
import * as config from '../config';
import * as draw from './draw';
declare class Human { declare class Human {
tf: any; #private;
draw: any;
package: any;
version: string; version: string;
config: any; config: typeof config.default;
fx: any;
state: string; state: string;
numTensors: number; image: {
analyzeMemoryLeaks: boolean; tensor: any;
checkSanity: boolean; canvas: any;
firstRun: boolean; };
perf: any; tf: typeof tf;
image: any; draw: typeof draw;
models: any; models: {
facemesh: any; face: any;
posenet: any;
blazepose: any;
handpose: any;
iris: any;
age: any; age: any;
gender: any; gender: any;
emotion: any; emotion: any;
body: any; embedding: any;
hand: any; };
sysinfo: any; classes: {
facemesh: typeof facemesh;
age: typeof age;
gender: typeof gender;
emotion: typeof emotion;
body: typeof posenet | typeof blazepose;
hand: typeof handpose;
};
sysinfo: {
platform: any;
agent: any;
};
constructor(userConfig?: {}); constructor(userConfig?: {});
profile(): {}; profileData(): {
analyze(...msg: any[]): void; newBytes: any;
sanity(input: any): "input is not defined" | "input must be a tensor" | "backend not loaded" | null; newTensors: any;
peakBytes: any;
numKernelOps: any;
timeKernelOps: any;
slowestKernelOps: any;
largestKernelOps: any;
} | {};
simmilarity(embedding1: any, embedding2: any): number; simmilarity(embedding1: any, embedding2: any): number;
load(userConfig?: null): Promise<void>; load(userConfig?: null): Promise<void>;
checkBackend(force?: boolean): Promise<void>; detect(input: any, userConfig?: {}): Promise<{
calculateFaceAngle: (mesh: any) => {}; face: any;
detectFace(input: any): Promise<{ body: any;
confidence: number; hand: any;
boxConfidence: number; gesture: any;
faceConfidence: number; performance: any;
box: any; canvas: any;
mesh: any; } | {
meshRaw: any; error: any;
boxRaw: any; }>;
annotations: any; warmup(userConfig: any): Promise<{
age: number; face: any;
gender: string; body: any;
genderConfidence: number; hand: any;
emotion: string; gesture: any;
embedding: any; performance: any;
iris: number; canvas: any;
angle: any; } | {
}[]>; error: any;
detect(input: any, userConfig?: {}): Promise<unknown>; }>;
warmupBitmap(): Promise<any>;
warmupCanvas(): Promise<unknown>;
warmupNode(): Promise<unknown>;
warmup(userConfig: any): Promise<any>;
} }
export { Human as default }; export { Human as default };

4
types/image.d.ts vendored
View File

@ -1,4 +1,4 @@
export declare function process(input: any, config: any): { export declare function process(input: any, config: any): {
tensor: any; tensor: any;
canvas: null; canvas: any;
} | null; };