enhanced age, gender, emotion detection

pull/280/head
Vladimir Mandic 2021-03-10 09:44:45 -05:00
parent 02696a65b3
commit 74ad5a2837
7 changed files with 167 additions and 154 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');

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,41 +543,39 @@ 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) { case 'face':
case 'face': size = 256;
size = 256; src = 'data:image/jpeg;base64,' + sample.face;
src = 'data:image/jpeg;base64,' + sample.face; break;
break; case 'full':
case 'full': case 'body':
case 'body': size = 1200;
size = 1200; src = 'data:image/jpeg;base64,' + sample.body;
src = 'data:image/jpeg;base64,' + sample.body; break;
break; default:
default: src = null;
src = null; }
} // src = encodeURI('../assets/human-sample-upper.jpg');
// src = encodeURI('../assets/human-sample-upper.jpg'); const img = new Image();
const img = new Image(); img.onload = async () => {
img.onload = async () => { const canvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(size, size) : document.createElement('canvas');
const canvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(size, size) : document.createElement('canvas'); canvas.width = img.naturalWidth;
canvas.width = img.naturalWidth; canvas.height = img.naturalHeight;
canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d');
const ctx = canvas.getContext('2d'); ctx?.drawImage(img, 0, 0);
ctx?.drawImage(img, 0, 0); // const data = ctx?.getImageData(0, 0, canvas.height, canvas.width);
// const data = ctx?.getImageData(0, 0, canvas.height, canvas.width); const res = await this.detect(canvas, this.config);
const res = await this.detect(canvas, this.config); resolve(res);
resolve(res); };
}; 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 };
} }