add additional hand gestures

pull/280/head
Vladimir Mandic 2021-11-08 07:36:26 -05:00
parent 585ca1d1b8
commit e87bd17ff4
10 changed files with 103 additions and 102 deletions

View File

@ -126,7 +126,7 @@
"format": "iife", "format": "iife",
"input": "src/human.ts", "input": "src/human.ts",
"output": "dist/human.js", "output": "dist/human.js",
"minify": false, "minify": true,
"globalName": "Human", "globalName": "Human",
"external": ["fs", "os", "buffer", "util"] "external": ["fs", "os", "buffer", "util"]
}, },

View File

@ -9,8 +9,10 @@
## Changelog ## Changelog
### **HEAD -> main** 2021/11/07 mandic00@live.com ### **2.5.1** 2021/11/08 mandic00@live.com
- new human.compare api
- added links to release notes
### **origin/main** 2021/11/06 mandic00@live.com ### **origin/main** 2021/11/06 mandic00@live.com

31
TODO.md
View File

@ -38,34 +38,3 @@ MoveNet MultiPose model does not work with WASM backend due to missing F32 broad
<https://github.com/tensorflow/tfjs/issues/5516> <https://github.com/tensorflow/tfjs/issues/5516>
<br><hr><br> <br><hr><br>
### Pending release
New:
- New frame change detection algorithm used for [cache determination](https://vladmandic.github.io/human/typedoc/interfaces/Config.html#cacheSensitivity)
based on temporal input difference
- New built-in Tensorflow profiling [human.profile](https://vladmandic.github.io/human/typedoc/classes/Human.html#profile)
- New optional input histogram equalization [config.filter.equalization](https://vladmandic.github.io/human/)
auto-level input for optimal brightness/contrast
- New event-baseed interface [human.events](https://vladmandic.github.io/human/typedoc/classes/Human.html#events)
- New configuration validation [human.validate](https://vladmandic.github.io/human/typedoc/classes/Human.html#validate)
- New input compare function [human.compare](https://vladmandic.github.io/human/typedoc/classes/Human.html#compare)
this function is internally used by `human` to determine frame changes and cache validation
- New [custom built TFJS](https://github.com/vladmandic/tfjs) for bundled version
result is a pure module with reduced bundle size and include built-in support for all backends
note: **nobundle** and **node** versions link to standard `@tensorflow` packages
Changed:
- [Default configuration values](https://github.com/vladmandic/human/blob/main/src/config.ts#L262) have been tuned for precision and performance
- Supports all built-in modules on all backends
via custom implementation of missing kernel ops
- Performance and precision improvements
- **face**, **hand**
- **gestures** modules
- **face matching**
- Fix **ReactJS** compatibility
- Better precision using **WASM**
Previous issues due to math low-precision in WASM implementation
- Full **TS** type definitions for all modules and imports
- Focus on simplified demo
<https://vladmandic.github.io/human/demo/typescript/>

View File

@ -68,12 +68,7 @@ export async function predict(input: Tensor, config: Config): Promise<FaceResult
annotations: {}, annotations: {},
}; };
if (config.face.detector?.rotation && config.face.mesh?.enabled && env.kernels.includes('rotatewithoffset')) { [angle, rotationMatrix, face.tensor] = util.correctFaceRotation(false && config.face.detector?.rotation, box, input, inputSize); // optional rotate based on detector data // disabled
[angle, rotationMatrix, face.tensor] = util.correctFaceRotation(box, input, inputSize);
} else {
rotationMatrix = util.fixedRotationMatrix;
face.tensor = util.cutBoxFromImageAndResize(box, input, config.face.mesh?.enabled ? [inputSize, inputSize] : [blazeface.size(), blazeface.size()]);
}
if (config?.filter?.equalization) { if (config?.filter?.equalization) {
const equilized = await histogramEqualization(face.tensor as Tensor); const equilized = await histogramEqualization(face.tensor as Tensor);
tf.dispose(face.tensor); tf.dispose(face.tensor);
@ -112,14 +107,8 @@ export async function predict(input: Tensor, config: Config): Promise<FaceResult
face.boxRaw = util.getRawBox(box, input); face.boxRaw = util.getRawBox(box, input);
face.score = face.faceScore; face.score = face.faceScore;
newCache.push(box); newCache.push(box);
// other modules prefer different crop for a face so we dispose it and do it again
/*
tf.dispose(face.tensor); tf.dispose(face.tensor);
face.tensor = config.face.detector?.rotation && config.face.mesh?.enabled && env.kernels.includes('rotatewithoffset') [angle, rotationMatrix, face.tensor] = util.correctFaceRotation(config.face.detector?.rotation, box, input, inputSize); // optional rotate once more based on mesh data
? face.tensor = util.correctFaceRotation(util.enlargeBox(box, Math.sqrt(enlargeFact)), input, inputSize)[2]
: face.tensor = util.cutBoxFromImageAndResize(util.enlargeBox(box, Math.sqrt(enlargeFact)), input, [inputSize, inputSize]);
*/
} }
} }
faces.push(face); faces.push(face);

View File

@ -6,6 +6,7 @@
import * as tf from '../../dist/tfjs.esm.js'; import * as tf from '../../dist/tfjs.esm.js';
import * as coords from './facemeshcoords'; import * as coords from './facemeshcoords';
import type { Box, Point } from '../result'; import type { Box, Point } from '../result';
import { env } from '../util/env';
export const createBox = (startEndTensor) => ({ startPoint: tf.slice(startEndTensor, [0, 0], [-1, 2]), endPoint: tf.slice(startEndTensor, [0, 2], [-1, 2]) }); export const createBox = (startEndTensor) => ({ startPoint: tf.slice(startEndTensor, [0, 0], [-1, 2]), endPoint: tf.slice(startEndTensor, [0, 2], [-1, 2]) });
@ -155,21 +156,28 @@ export function transformRawCoords(coordsRaw, box, angle, rotationMatrix, inputS
])); ]));
} }
export function correctFaceRotation(box, input, inputSize) { export function correctFaceRotation(rotate, box, input, inputSize) {
const symmetryLine = (box.landmarks.length >= coords.meshLandmarks.count) ? coords.meshLandmarks.symmetryLine : coords.blazeFaceLandmarks.symmetryLine; const symmetryLine = (box.landmarks.length >= coords.meshLandmarks.count)
const angle: number = computeRotation(box.landmarks[symmetryLine[0]], box.landmarks[symmetryLine[1]]); ? coords.meshLandmarks.symmetryLine
const largeAngle = angle && (angle !== 0) && (Math.abs(angle) > 0.2); : coords.blazeFaceLandmarks.symmetryLine;
let rotationMatrix; let angle = 0; // default
let face; let rotationMatrix = fixedRotationMatrix; // default
if (largeAngle) { let face; // default
const faceCenter: Point = getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint });
const faceCenterNormalized: Point = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]]; if (rotate && env.kernels.includes('rotatewithoffset')) {
const rotated = tf.image.rotateWithOffset(input, angle, 0, faceCenterNormalized); // rotateWithOffset is not defined for tfjs-node angle = computeRotation(box.landmarks[symmetryLine[0]], box.landmarks[symmetryLine[1]]);
rotationMatrix = buildRotationMatrix(-angle, faceCenter); const largeAngle = angle && (angle !== 0) && (Math.abs(angle) > 0.2);
face = cutBoxFromImageAndResize(box, rotated, [inputSize, inputSize]); if (largeAngle) {
tf.dispose(rotated); const center: Point = getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint });
const centerRaw: Point = [center[0] / input.shape[2], center[1] / input.shape[1]];
const rotated = tf.image.rotateWithOffset(input, angle, 0, centerRaw); // rotateWithOffset is not defined for tfjs-node
rotationMatrix = buildRotationMatrix(-angle, center);
face = cutBoxFromImageAndResize(box, rotated, [inputSize, inputSize]);
tf.dispose(rotated);
} else {
face = cutBoxFromImageAndResize(box, input, [inputSize, inputSize]);
}
} else { } else {
rotationMatrix = fixedRotationMatrix;
face = cutBoxFromImageAndResize(box, input, [inputSize, inputSize]); face = cutBoxFromImageAndResize(box, input, [inputSize, inputSize]);
} }
return [angle, rotationMatrix, face]; return [angle, rotationMatrix, face];

View File

@ -66,17 +66,17 @@ export class FingerGesture {
this.weightsRelative = [1.0, 1.0, 1.0, 1.0, 1.0]; this.weightsRelative = [1.0, 1.0, 1.0, 1.0, 1.0];
} }
addCurl(finger, curl, confidence) { curl(finger, curl, confidence) {
if (typeof this.curls[finger] === 'undefined') this.curls[finger] = []; if (typeof this.curls[finger] === 'undefined') this.curls[finger] = [];
this.curls[finger].push([curl, confidence]); this.curls[finger].push([curl, confidence]);
} }
addDirection(finger, position, confidence) { direction(finger, position, confidence) {
if (!this.directions[finger]) this.directions[finger] = []; if (!this.directions[finger]) this.directions[finger] = [];
this.directions[finger].push([position, confidence]); this.directions[finger].push([position, confidence]);
} }
setWeight(finger, weight) { weight(finger, weight) {
this.weights[finger] = weight; this.weights[finger] = weight;
// recalculate relative weights // recalculate relative weights
const total = this.weights.reduce((a, b) => a + b, 0); const total = this.weights.reduce((a, b) => a + b, 0);

View File

@ -5,39 +5,74 @@
import { Finger, FingerCurl, FingerDirection, FingerGesture } from './fingerdef'; import { Finger, FingerCurl, FingerDirection, FingerGesture } from './fingerdef';
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const { thumb, index, middle, ring, pinky } = Finger;
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const { none, half, full } = FingerCurl;
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const { verticalUp, verticalDown, horizontalLeft, horizontalRight, diagonalUpRight, diagonalUpLeft, diagonalDownRight, diagonalDownLeft } = FingerDirection;
// describe thumbs up gesture 👍 // describe thumbs up gesture 👍
const ThumbsUp = new FingerGesture('thumbs up'); const ThumbsUp = new FingerGesture('thumbs up');
ThumbsUp.addCurl(Finger.thumb, FingerCurl.none, 1.0); ThumbsUp.curl(thumb, none, 1.0);
ThumbsUp.addDirection(Finger.thumb, FingerDirection.verticalUp, 1.0); ThumbsUp.direction(thumb, verticalUp, 1.0);
ThumbsUp.addDirection(Finger.thumb, FingerDirection.diagonalUpLeft, 0.25); ThumbsUp.direction(thumb, diagonalUpLeft, 0.25);
ThumbsUp.addDirection(Finger.thumb, FingerDirection.diagonalUpRight, 0.25); ThumbsUp.direction(thumb, diagonalUpRight, 0.25);
for (const finger of [Finger.index, Finger.middle, Finger.ring, Finger.pinky]) { for (const finger of [Finger.index, Finger.middle, Finger.ring, Finger.pinky]) {
ThumbsUp.addCurl(finger, FingerCurl.full, 1.0); ThumbsUp.curl(finger, full, 1.0);
ThumbsUp.addDirection(finger, FingerDirection.horizontalLeft, 1.0); ThumbsUp.direction(finger, horizontalLeft, 1.0);
ThumbsUp.addDirection(finger, FingerDirection.horizontalRight, 1.0); ThumbsUp.direction(finger, horizontalRight, 1.0);
} }
// describe Victory gesture ✌️ // describe Victory gesture ✌️
const Victory = new FingerGesture('victory'); const Victory = new FingerGesture('victory');
Victory.addCurl(Finger.thumb, FingerCurl.half, 0.5); Victory.curl(thumb, half, 0.5);
Victory.addCurl(Finger.thumb, FingerCurl.none, 0.5); Victory.curl(thumb, none, 0.5);
Victory.addDirection(Finger.thumb, FingerDirection.verticalUp, 1.0); Victory.direction(thumb, verticalUp, 1.0);
Victory.addDirection(Finger.thumb, FingerDirection.diagonalUpLeft, 1.0); Victory.direction(thumb, diagonalUpLeft, 1.0);
Victory.addCurl(Finger.index, FingerCurl.none, 1.0); Victory.curl(index, none, 1.0);
Victory.addDirection(Finger.index, FingerDirection.verticalUp, 0.75); Victory.direction(index, verticalUp, 0.75);
Victory.addDirection(Finger.index, FingerDirection.diagonalUpLeft, 1.0); Victory.direction(index, diagonalUpLeft, 1.0);
Victory.addCurl(Finger.middle, FingerCurl.none, 1.0); Victory.curl(middle, none, 1.0);
Victory.addDirection(Finger.middle, FingerDirection.verticalUp, 1.0); Victory.direction(middle, verticalUp, 1.0);
Victory.addDirection(Finger.middle, FingerDirection.diagonalUpLeft, 0.75); Victory.direction(middle, diagonalUpLeft, 0.75);
Victory.addCurl(Finger.ring, FingerCurl.full, 1.0); Victory.curl(ring, full, 1.0);
Victory.addDirection(Finger.ring, FingerDirection.verticalUp, 0.2); Victory.direction(ring, verticalUp, 0.2);
Victory.addDirection(Finger.ring, FingerDirection.diagonalUpLeft, 1.0); Victory.direction(ring, diagonalUpLeft, 1.0);
Victory.addDirection(Finger.ring, FingerDirection.horizontalLeft, 0.2); Victory.direction(ring, horizontalLeft, 0.2);
Victory.addCurl(Finger.pinky, FingerCurl.full, 1.0); Victory.curl(pinky, full, 1.0);
Victory.addDirection(Finger.pinky, FingerDirection.verticalUp, 0.2); Victory.direction(pinky, verticalUp, 0.2);
Victory.addDirection(Finger.pinky, FingerDirection.diagonalUpLeft, 1.0); Victory.direction(pinky, diagonalUpLeft, 1.0);
Victory.addDirection(Finger.pinky, FingerDirection.horizontalLeft, 0.2); Victory.direction(pinky, horizontalLeft, 0.2);
Victory.setWeight(Finger.index, 2); Victory.weight(index, 2);
Victory.setWeight(Finger.middle, 2); Victory.weight(middle, 2);
export default [ThumbsUp, Victory]; // describe Point gesture ✌️
const Point = new FingerGesture('point');
Point.curl(thumb, full, 1.0);
Point.curl(index, none, 0.5);
Point.curl(middle, full, 0.5);
Point.curl(ring, full, 0.5);
Point.curl(pinky, full, 0.5);
Point.weight(index, 2);
Point.weight(middle, 2);
// describe Point gesture ✌️
const MiddleFinger = new FingerGesture('middle finger');
MiddleFinger.curl(thumb, none, 1.0);
MiddleFinger.curl(index, full, 0.5);
MiddleFinger.curl(middle, full, 0.5);
MiddleFinger.curl(ring, full, 0.5);
MiddleFinger.curl(pinky, full, 0.5);
MiddleFinger.weight(index, 2);
MiddleFinger.weight(middle, 2);
// describe Open Palm gesture ✌️
const OpenPalm = new FingerGesture('open palm');
OpenPalm.curl(thumb, none, 0.75);
OpenPalm.curl(index, none, 0.75);
OpenPalm.curl(middle, none, 0.75);
OpenPalm.curl(ring, none, 0.75);
OpenPalm.curl(pinky, none, 0.75);
export default [ThumbsUp, Victory, Point, MiddleFinger, OpenPalm];

View File

@ -24,7 +24,7 @@ const inputSize = [[0, 0], [0, 0]];
const classes = ['hand', 'fist', 'pinch', 'point', 'face', 'tip', 'pinchtip']; const classes = ['hand', 'fist', 'pinch', 'point', 'face', 'tip', 'pinchtip'];
const faceIndex = 4; const faceIndex = 4;
const boxExpandFact = 1.7; const boxExpandFact = 1.6;
const maxDetectorResolution = 512; const maxDetectorResolution = 512;
const detectorExpandFact = 1.4; const detectorExpandFact = 1.4;
@ -170,9 +170,7 @@ async function detectFingers(input: Tensor, h: HandDetectResult, config: Config)
outputSize[1] * (kpt[1] + h.boxRaw[1]), outputSize[1] * (kpt[1] + h.boxRaw[1]),
(kpt[2] || 0), (kpt[2] || 0),
]); ]);
// hand.box = box.scale(h.box, 1 / detectorExpandFact); // scale box down for visual appeal hand.landmarks = fingerPose.analyze(hand.keypoints) as HandResult['landmarks']; // calculate finger gestures
// hand.boxRaw = box.scale(h.boxRaw, 1 / detectorExpandFact); // scale box down for visual appeal
hand.landmarks = fingerPose.analyze(hand.keypoints) as HandResult['landmarks']; // calculate finger landmarks
for (const key of Object.keys(fingerMap)) { // map keypoints to per-finger annotations for (const key of Object.keys(fingerMap)) { // map keypoints to per-finger annotations
hand.annotations[key] = fingerMap[key].map((index) => (hand.landmarks && hand.keypoints[index] ? hand.keypoints[index] : null)); hand.annotations[key] = fingerMap[key].map((index) => (hand.landmarks && hand.keypoints[index] ? hand.keypoints[index] : null));
} }

View File

@ -153,9 +153,9 @@ async function verifyDetails(human) {
verify(res.face.length === 1, 'details face length', res.face.length); verify(res.face.length === 1, 'details face length', res.face.length);
for (const face of res.face) { for (const face of res.face) {
verify(face.score > 0.9 && face.boxScore > 0.9 && face.faceScore > 0.9, 'details face score', face.score, face.boxScore, face.faceScore); verify(face.score > 0.9 && face.boxScore > 0.9 && face.faceScore > 0.9, 'details face score', face.score, face.boxScore, face.faceScore);
verify(face.age > 29 && face.age < 30 && face.gender === 'female' && face.genderScore > 0.9 && face.iris > 70 && face.iris < 80, 'details face age/gender', face.age, face.gender, face.genderScore, face.iris); verify(face.age > 23 && face.age < 24 && face.gender === 'female' && face.genderScore > 0.9 && face.iris > 70 && face.iris < 80, 'details face age/gender', face.age, face.gender, face.genderScore, face.iris);
verify(face.box.length === 4 && face.boxRaw.length === 4 && face.mesh.length === 478 && face.meshRaw.length === 478 && face.embedding.length === 1024, 'details face arrays', face.box.length, face.mesh.length, face.embedding.length); verify(face.box.length === 4 && face.boxRaw.length === 4 && face.mesh.length === 478 && face.meshRaw.length === 478 && face.embedding.length === 1024, 'details face arrays', face.box.length, face.mesh.length, face.embedding.length);
verify(face.emotion.length === 3 && face.emotion[0].score > 0.5 && face.emotion[0].emotion === 'angry', 'details face emotion', face.emotion.length, face.emotion[0]); verify(face.emotion.length === 3 && face.emotion[0].score > 0.45 && face.emotion[0].emotion === 'neutral', 'details face emotion', face.emotion.length, face.emotion[0]);
} }
verify(res.body.length === 1, 'details body length', res.body.length); verify(res.body.length === 1, 'details body length', res.body.length);
for (const body of res.body) { for (const body of res.body) {
@ -166,7 +166,7 @@ async function verifyDetails(human) {
verify(hand.score > 0.5 && hand.boxScore > 0.5 && hand.fingerScore > 0.5 && hand.box.length === 4 && hand.boxRaw.length === 4 && hand.label === 'point', 'details hand', hand.boxScore, hand.fingerScore, hand.label); verify(hand.score > 0.5 && hand.boxScore > 0.5 && hand.fingerScore > 0.5 && hand.box.length === 4 && hand.boxRaw.length === 4 && hand.label === 'point', 'details hand', hand.boxScore, hand.fingerScore, hand.label);
verify(hand.keypoints.length === 21 && Object.keys(hand.landmarks).length === 5 && Object.keys(hand.annotations).length === 6, 'details hand arrays', hand.keypoints.length, Object.keys(hand.landmarks).length, Object.keys(hand.annotations).length); verify(hand.keypoints.length === 21 && Object.keys(hand.landmarks).length === 5 && Object.keys(hand.annotations).length === 6, 'details hand arrays', hand.keypoints.length, Object.keys(hand.landmarks).length, Object.keys(hand.annotations).length);
} }
verify(res.gesture.length === 5, 'details gesture length', res.gesture.length); verify(res.gesture.length === 6, 'details gesture length', res.gesture.length);
verify(res.gesture[0].gesture === 'facing right', 'details gesture first', res.gesture[0]); verify(res.gesture[0].gesture === 'facing right', 'details gesture first', res.gesture[0]);
verify(res.object.length === 1, 'details object length', res.object.length); verify(res.object.length === 1, 'details object length', res.object.length);
for (const obj of res.object) { for (const obj of res.object) {
@ -236,11 +236,11 @@ async function test(Human, inputConfig) {
else log('state', 'passed: warmup none result match'); else log('state', 'passed: warmup none result match');
config.warmup = 'face'; config.warmup = 'face';
res = await testWarmup(human, 'default'); res = await testWarmup(human, 'default');
if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length !== 7) log('error', 'failed: warmup face result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length < 7) log('error', 'failed: warmup face result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length);
else log('state', 'passed: warmup face result match'); else log('state', 'passed: warmup face result match');
config.warmup = 'body'; config.warmup = 'body';
res = await testWarmup(human, 'default'); res = await testWarmup(human, 'default');
if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length !== 5) log('error', 'failed: warmup body result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length !== 6) log('error', 'failed: warmup body result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length);
else log('state', 'passed: warmup body result match'); else log('state', 'passed: warmup body result match');
log('state', 'details:', { log('state', 'details:', {
face: { boxScore: res.face[0].boxScore, faceScore: res.face[0].faceScore, age: res.face[0].age, gender: res.face[0].gender, genderScore: res.face[0].genderScore }, face: { boxScore: res.face[0].boxScore, faceScore: res.face[0].faceScore, age: res.face[0].age, gender: res.face[0].gender, genderScore: res.face[0].genderScore },
@ -330,7 +330,7 @@ async function test(Human, inputConfig) {
config.body = { minConfidence: 0.0001 }; config.body = { minConfidence: 0.0001 };
config.hand = { minConfidence: 0.0001 }; config.hand = { minConfidence: 0.0001 };
res = await testDetect(human, 'samples/in/ai-body.jpg', 'default'); res = await testDetect(human, 'samples/in/ai-body.jpg', 'default');
if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 2 || res?.gesture?.length !== 7) log('error', 'failed: sensitive result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 2 || res?.gesture?.length !== 8) log('error', 'failed: sensitive result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length);
else log('state', 'passed: sensitive result match'); else log('state', 'passed: sensitive result match');
// test sensitive details face // test sensitive details face

2
wiki

@ -1 +1 @@
Subproject commit 55876f5dbb01d605c862ab2749a2bc11f86d5cba Subproject commit 214e797620e397f16132ae38d6a9d6bd46f2c60d