mirror of https://github.com/vladmandic/human
switch to async data reads
parent
f73520bbd5
commit
334bb7061f
|
@ -1,6 +1,6 @@
|
||||||
# @vladmandic/human
|
# @vladmandic/human
|
||||||
|
|
||||||
Version: **2.1.2**
|
Version: **2.1.3**
|
||||||
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,7 +9,10 @@ Repository: **<git+https://github.com/vladmandic/human.git>**
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### **HEAD -> main** 2021/08/09 mandic00@live.com
|
### **2.1.3** 2021/08/12 mandic00@live.com
|
||||||
|
|
||||||
|
|
||||||
|
### **origin/main** 2021/08/11 mandic00@live.com
|
||||||
|
|
||||||
- minor update
|
- minor update
|
||||||
- replace movenet with lightning-v4
|
- replace movenet with lightning-v4
|
||||||
|
|
41
TODO.md
41
TODO.md
|
@ -21,31 +21,50 @@ WebGL shader optimizations for faster load and initial detection
|
||||||
- Optical Flow: <https://docs.opencv.org/3.3.1/db/d7f/tutorial_js_lucas_kanade.html>
|
- Optical Flow: <https://docs.opencv.org/3.3.1/db/d7f/tutorial_js_lucas_kanade.html>
|
||||||
- TFLite Models: <https://js.tensorflow.org/api_tflite/0.0.1-alpha.4/>
|
- TFLite Models: <https://js.tensorflow.org/api_tflite/0.0.1-alpha.4/>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
### Object Detection
|
<br>
|
||||||
|
|
||||||
Object detection using CenterNet or NanoDet models is not working when using WASM backend due to missing kernel ops in TFJS
|
|
||||||
*Target: `Human` v2.2 with `TFJS` v3.9*
|
|
||||||
|
|
||||||
- CenterNet with WASM: <https://github.com/tensorflow/tfjs/issues/5110>
|
|
||||||
- NanoDet with WASM: <https://github.com/tensorflow/tfjs/issues/4824>
|
|
||||||
|
|
||||||
### Face Detection
|
### Face Detection
|
||||||
|
|
||||||
Enhanced rotation correction for face detection is not working in NodeJS due to missing kernel op in TFJS
|
Enhanced rotation correction for face detection is not working in NodeJS due to missing kernel op in TFJS
|
||||||
Feature is automatically disabled in NodeJS without user impact
|
Feature is automatically disabled in NodeJS without user impact
|
||||||
*Target: `Human` v2.2 with `TFJS` v3.9*
|
|
||||||
|
|
||||||
- BlazeFace rotation correction in NodeJS: <https://github.com/tensorflow/tfjs/issues/4066>
|
- Backend NodeJS missing kernel op `FlipLeftRight`
|
||||||
|
<https://github.com/tensorflow/tfjs/issues/4066>
|
||||||
|
*Target: `Human` v2.2 with `TFJS` v3.9*
|
||||||
|
- Backend NodeJS missing kernel op `RotateWithOffset`
|
||||||
|
<https://github.com/tensorflow/tfjs/issues/5473>
|
||||||
|
*Target: N/A*
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
### Hand Detection
|
### Hand Detection
|
||||||
|
|
||||||
Enhanced rotation correction for hand detection is not working in NodeJS due to missing kernel op in TFJS
|
Enhanced rotation correction for hand detection is not working in NodeJS due to missing kernel op in TFJS
|
||||||
Feature is automatically disabled in NodeJS without user impact
|
Feature is automatically disabled in NodeJS without user impact
|
||||||
*Target: `Human` v2.2 with `TFJS` v3.9*
|
|
||||||
|
|
||||||
- HandPose rotation correction in NodeJS: <https://github.com/tensorflow/tfjs/issues/4066>
|
- Backend NodeJS missing kernel op `FlipLeftRight`
|
||||||
|
<https://github.com/tensorflow/tfjs/issues/4066>
|
||||||
|
*Target: `Human` v2.2 with `TFJS` v3.9*
|
||||||
|
- Backend NodeJS missing kernel op `RotateWithOffset`
|
||||||
|
<https://github.com/tensorflow/tfjs/issues/5473>
|
||||||
|
*Target: N/A*
|
||||||
|
|
||||||
Hand detection using WASM backend has reduced precision due to math rounding errors in backend
|
Hand detection using WASM backend has reduced precision due to math rounding errors in backend
|
||||||
*Target: N/A*
|
*Target: N/A*
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Object Detection
|
||||||
|
|
||||||
|
Object detection using CenterNet or NanoDet models is not working when using WASM backend due to missing kernel ops in TFJS
|
||||||
|
|
||||||
|
- Backend WASM missing kernel op `Mod`
|
||||||
|
<https://github.com/tensorflow/tfjs/issues/5110>
|
||||||
|
*Target: `Human` v2.2 with `TFJS` v3.9*
|
||||||
|
- Backend WASM missing kernel op `SparseToDense`
|
||||||
|
<https://github.com/tensorflow/tfjs/issues/4824>
|
||||||
|
*Target: `Human` v2.2 with `TFJS` v3.9*
|
||||||
|
|
|
@ -121,7 +121,7 @@ async function detect(input) {
|
||||||
if (result && result.hand && result.hand.length > 0) {
|
if (result && result.hand && result.hand.length > 0) {
|
||||||
for (let i = 0; i < result.hand.length; i++) {
|
for (let i = 0; i < result.hand.length; i++) {
|
||||||
const hand = result.hand[i];
|
const hand = result.hand[i];
|
||||||
log.data(` Hand: #${i} score:${hand.score}`);
|
log.data(` Hand: #${i} score:${hand.score} keypoints:${hand.keypoints?.length}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.data(' Hand: N/A');
|
log.data(' Hand: N/A');
|
||||||
|
|
|
@ -45,7 +45,7 @@ export async function predict(image: Tensor, config: Config | any) {
|
||||||
tf.dispose(enhance);
|
tf.dispose(enhance);
|
||||||
|
|
||||||
if (ageT) {
|
if (ageT) {
|
||||||
const data = ageT.dataSync();
|
const data = await ageT.data();
|
||||||
obj.age = Math.trunc(10 * data[0]) / 10;
|
obj.age = Math.trunc(10 * data[0]) / 10;
|
||||||
}
|
}
|
||||||
tf.dispose(ageT);
|
tf.dispose(ageT);
|
||||||
|
|
|
@ -39,12 +39,12 @@ function max2d(inputs, minScore) {
|
||||||
// combine all data
|
// combine all data
|
||||||
const reshaped = tf.reshape(inputs, [height * width]);
|
const reshaped = tf.reshape(inputs, [height * width]);
|
||||||
// get highest score
|
// get highest score
|
||||||
const newScore = tf.max(reshaped, 0).dataSync()[0];
|
const newScore = tf.max(reshaped, 0).dataSync()[0]; // inside tf.tidy
|
||||||
if (newScore > minScore) {
|
if (newScore > minScore) {
|
||||||
// skip coordinate calculation is score is too low
|
// skip coordinate calculation is score is too low
|
||||||
const coords = tf.argMax(reshaped, 0);
|
const coords = tf.argMax(reshaped, 0);
|
||||||
const x = mod(coords, width).dataSync()[0];
|
const x = mod(coords, width).dataSync()[0]; // inside tf.tidy
|
||||||
const y = tf.div(coords, tf.scalar(width, 'int32')).dataSync()[0];
|
const y = tf.div(coords, tf.scalar(width, 'int32')).dataSync()[0]; // inside tf.tidy
|
||||||
return [x, y, newScore];
|
return [x, y, newScore];
|
||||||
}
|
}
|
||||||
return [0, 0, newScore];
|
return [0, 0, newScore];
|
||||||
|
|
|
@ -53,7 +53,7 @@ export async function predict(image: Tensor, config: Config, idx, count) {
|
||||||
const obj: Array<{ score: number, emotion: string }> = [];
|
const obj: Array<{ score: number, emotion: string }> = [];
|
||||||
if (config.face.emotion.enabled) {
|
if (config.face.emotion.enabled) {
|
||||||
const emotionT = await model.predict(normalize); // result is already in range 0..1, no need for additional activation
|
const emotionT = await model.predict(normalize); // result is already in range 0..1, no need for additional activation
|
||||||
const data = emotionT.dataSync();
|
const data = await emotionT.data();
|
||||||
tf.dispose(emotionT);
|
tf.dispose(emotionT);
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
if (data[i] > config.face.emotion.minConfidence) obj.push({ score: Math.min(0.99, Math.trunc(100 * 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] });
|
||||||
|
|
|
@ -134,21 +134,21 @@ export async function predict(image: Tensor, config: Config, idx, count) {
|
||||||
|
|
||||||
if (resT) {
|
if (resT) {
|
||||||
tf.tidy(() => {
|
tf.tidy(() => {
|
||||||
const gender = resT.find((t) => t.shape[1] === 1).dataSync();
|
const gender = resT.find((t) => t.shape[1] === 1).dataSync(); // inside tf.tidy
|
||||||
const confidence = Math.trunc(200 * Math.abs((gender[0] - 0.5))) / 100;
|
const confidence = Math.trunc(200 * Math.abs((gender[0] - 0.5))) / 100;
|
||||||
if (confidence > config.face.description.minConfidence) {
|
if (confidence > config.face.description.minConfidence) {
|
||||||
obj.gender = gender[0] <= 0.5 ? 'female' : 'male';
|
obj.gender = gender[0] <= 0.5 ? 'female' : 'male';
|
||||||
obj.genderScore = Math.min(0.99, confidence);
|
obj.genderScore = Math.min(0.99, confidence);
|
||||||
}
|
}
|
||||||
const age = tf.argMax(resT.find((t) => t.shape[1] === 100), 1).dataSync()[0];
|
const age = tf.argMax(resT.find((t) => t.shape[1] === 100), 1).dataSync()[0]; // inside tf.tidy
|
||||||
const all = resT.find((t) => t.shape[1] === 100).dataSync();
|
const all = resT.find((t) => t.shape[1] === 100).dataSync(); // inside tf.tidy
|
||||||
obj.age = Math.round(all[age - 1] > all[age + 1] ? 10 * age - 100 * all[age - 1] : 10 * age + 100 * all[age + 1]) / 10;
|
obj.age = Math.round(all[age - 1] > all[age + 1] ? 10 * age - 100 * all[age - 1] : 10 * age + 100 * all[age + 1]) / 10;
|
||||||
|
|
||||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||||
// const reshape = desc.reshape([128, 8]); // reshape large 1024-element descriptor to 128 x 8
|
// const reshape = desc.reshape([128, 8]); // reshape large 1024-element descriptor to 128 x 8
|
||||||
// const reduce = reshape.logSumExp(1); // reduce 2nd dimension by calculating logSumExp on it which leaves us with 128-element descriptor
|
// const reduce = reshape.logSumExp(1); // reduce 2nd dimension by calculating logSumExp on it which leaves us with 128-element descriptor
|
||||||
|
|
||||||
obj.descriptor = [...desc.dataSync()];
|
obj.descriptor = [...desc.dataSync()]; // inside tf.tidy
|
||||||
});
|
});
|
||||||
resT.forEach((t) => tf.dispose(t));
|
resT.forEach((t) => tf.dispose(t));
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ export async function predict(image: Tensor, config: Config | any) {
|
||||||
|
|
||||||
if (genderT) {
|
if (genderT) {
|
||||||
if (!Array.isArray(genderT)) {
|
if (!Array.isArray(genderT)) {
|
||||||
const data = genderT.dataSync();
|
const data = await genderT.data();
|
||||||
if (alternative) {
|
if (alternative) {
|
||||||
// returns two values 0..1, bigger one is prediction
|
// returns two values 0..1, bigger one is prediction
|
||||||
if (data[0] > config.face.gender.minConfidence || data[1] > config.face.gender.minConfidence) {
|
if (data[0] > config.face.gender.minConfidence || data[1] > config.face.gender.minConfidence) {
|
||||||
|
@ -80,7 +80,7 @@ export async function predict(image: Tensor, config: Config | any) {
|
||||||
}
|
}
|
||||||
tf.dispose(genderT);
|
tf.dispose(genderT);
|
||||||
} else {
|
} else {
|
||||||
const gender = genderT[0].dataSync();
|
const gender = await genderT[0].data();
|
||||||
const confidence = Math.trunc(200 * Math.abs((gender[0] - 0.5))) / 100;
|
const confidence = Math.trunc(200 * Math.abs((gender[0] - 0.5))) / 100;
|
||||||
if (confidence > config.face.gender.minConfidence) {
|
if (confidence > config.face.gender.minConfidence) {
|
||||||
obj.gender = gender[0] <= 0.5 ? 'female' : 'male';
|
obj.gender = gender[0] <= 0.5 ? 'female' : 'male';
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class HandDetector {
|
||||||
const predictions = tf.squeeze(batched);
|
const predictions = tf.squeeze(batched);
|
||||||
tf.dispose(batched);
|
tf.dispose(batched);
|
||||||
const scoresT = tf.tidy(() => tf.squeeze(tf.sigmoid(tf.slice(predictions, [0, 0], [-1, 1]))));
|
const scoresT = tf.tidy(() => tf.squeeze(tf.sigmoid(tf.slice(predictions, [0, 0], [-1, 1]))));
|
||||||
const scores = scoresT.dataSync();
|
const scores = await scoresT.data();
|
||||||
const rawBoxes = tf.slice(predictions, [0, 1], [-1, 4]);
|
const rawBoxes = tf.slice(predictions, [0, 1], [-1, 4]);
|
||||||
const boxes = this.normalizeBoxes(rawBoxes);
|
const boxes = this.normalizeBoxes(rawBoxes);
|
||||||
tf.dispose(rawBoxes);
|
tf.dispose(rawBoxes);
|
||||||
|
@ -78,7 +78,7 @@ export class HandDetector {
|
||||||
const hands: Array<{ startPoint: number[]; endPoint: number[]; palmLandmarks: number[]; confidence: number }> = [];
|
const hands: Array<{ startPoint: number[]; endPoint: number[]; palmLandmarks: number[]; confidence: number }> = [];
|
||||||
if (!predictions || predictions.length === 0) return hands;
|
if (!predictions || predictions.length === 0) return hands;
|
||||||
for (const prediction of predictions) {
|
for (const prediction of predictions) {
|
||||||
const boxes = prediction.box.dataSync();
|
const boxes = await prediction.box.data();
|
||||||
const startPoint = boxes.slice(0, 2);
|
const startPoint = boxes.slice(0, 2);
|
||||||
const endPoint = boxes.slice(2, 4);
|
const endPoint = boxes.slice(2, 4);
|
||||||
const palmLandmarks = await prediction.palmLandmarks.array();
|
const palmLandmarks = await prediction.palmLandmarks.array();
|
||||||
|
|
|
@ -367,7 +367,7 @@ export class Human {
|
||||||
sumT.dispose();
|
sumT.dispose();
|
||||||
*/
|
*/
|
||||||
// use js loop sum, faster than uploading tensor to gpu calculating and downloading back
|
// use js loop sum, faster than uploading tensor to gpu calculating and downloading back
|
||||||
const reducedData = reduced.dataSync(); // raw image rgb array
|
const reducedData = await reduced.data(); // raw image rgb array
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i = 0; i < reducedData.length / 3; i++) sum += reducedData[3 * i + 2]; // look only at green value of each pixel
|
for (let i = 0; i < reducedData.length / 3; i++) sum += reducedData[3 * i + 2]; // look only at green value of each pixel
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ async function process(res: Tensor, inputSize, outputShape, config: Config) {
|
||||||
tf.dispose(boxesT);
|
tf.dispose(boxesT);
|
||||||
tf.dispose(scoresT);
|
tf.dispose(scoresT);
|
||||||
tf.dispose(classesT);
|
tf.dispose(classesT);
|
||||||
const nms = nmsT.dataSync();
|
const nms = await nmsT.data();
|
||||||
tf.dispose(nmsT);
|
tf.dispose(nmsT);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const id of nms) {
|
for (const id of nms) {
|
||||||
|
|
|
@ -90,7 +90,7 @@ async function process(res, inputSize, outputShape, config) {
|
||||||
let nmsIdx: Array<number> = [];
|
let nmsIdx: Array<number> = [];
|
||||||
if (nmsBoxes && nmsBoxes.length > 0) {
|
if (nmsBoxes && nmsBoxes.length > 0) {
|
||||||
const nms = await tf.image.nonMaxSuppressionAsync(nmsBoxes, nmsScores, config.object.maxDetected, config.object.iouThreshold, config.object.minConfidence);
|
const nms = await tf.image.nonMaxSuppressionAsync(nmsBoxes, nmsScores, config.object.maxDetected, config.object.iouThreshold, config.object.minConfidence);
|
||||||
nmsIdx = nms.dataSync();
|
nmsIdx = await nms.data();
|
||||||
tf.dispose(nms);
|
tf.dispose(nms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue