2021-09-25 17:51:15 +02:00
|
|
|
/**
|
|
|
|
* HandPose model implementation
|
|
|
|
* See `handpose.ts` for entry point
|
|
|
|
*/
|
|
|
|
|
2020-11-18 14:26:28 +01:00
|
|
|
import * as tf from '../../dist/tfjs.esm.js';
|
2021-10-20 15:10:57 +02:00
|
|
|
import * as util from './handposeutil';
|
|
|
|
import * as anchors from './handposeanchors';
|
2021-09-13 19:28:35 +02:00
|
|
|
import type { Tensor, GraphModel } from '../tfjs/types';
|
2020-10-12 01:22:43 +02:00
|
|
|
|
2021-02-08 17:39:09 +01:00
|
|
|
export class HandDetector {
|
2021-05-23 03:47:59 +02:00
|
|
|
model: GraphModel;
|
2021-05-22 20:53:51 +02:00
|
|
|
anchors: number[][];
|
2021-05-23 03:54:18 +02:00
|
|
|
anchorsTensor: Tensor;
|
2021-03-11 16:26:14 +01:00
|
|
|
inputSize: number;
|
2021-05-23 03:54:18 +02:00
|
|
|
inputSizeTensor: Tensor;
|
|
|
|
doubleInputSizeTensor: Tensor;
|
2021-02-13 15:21:48 +01:00
|
|
|
|
2021-04-25 22:56:10 +02:00
|
|
|
constructor(model) {
|
2020-10-12 01:22:43 +02:00
|
|
|
this.model = model;
|
2021-04-25 22:56:10 +02:00
|
|
|
this.anchors = anchors.anchors.map((anchor) => [anchor.x, anchor.y]);
|
2020-10-12 01:22:43 +02:00
|
|
|
this.anchorsTensor = tf.tensor2d(this.anchors);
|
2021-08-17 14:51:17 +02:00
|
|
|
this.inputSize = (this.model && this.model.inputs && this.model.inputs[0].shape) ? this.model.inputs[0].shape[2] : 0;
|
2021-04-25 22:56:10 +02:00
|
|
|
this.inputSizeTensor = tf.tensor1d([this.inputSize, this.inputSize]);
|
|
|
|
this.doubleInputSizeTensor = tf.tensor1d([this.inputSize * 2, this.inputSize * 2]);
|
2020-10-12 01:22:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
normalizeBoxes(boxes) {
|
|
|
|
return tf.tidy(() => {
|
|
|
|
const boxOffsets = tf.slice(boxes, [0, 0], [-1, 2]);
|
|
|
|
const boxSizes = tf.slice(boxes, [0, 2], [-1, 2]);
|
|
|
|
const boxCenterPoints = tf.add(tf.div(boxOffsets, this.inputSizeTensor), this.anchorsTensor);
|
|
|
|
const halfBoxSizes = tf.div(boxSizes, this.doubleInputSizeTensor);
|
|
|
|
const startPoints = tf.mul(tf.sub(boxCenterPoints, halfBoxSizes), this.inputSizeTensor);
|
|
|
|
const endPoints = tf.mul(tf.add(boxCenterPoints, halfBoxSizes), this.inputSizeTensor);
|
|
|
|
return tf.concat2d([startPoints, endPoints], 1);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
normalizeLandmarks(rawPalmLandmarks, index) {
|
|
|
|
return tf.tidy(() => {
|
2021-07-29 22:06:03 +02:00
|
|
|
const landmarks = tf.add(tf.div(tf.reshape(rawPalmLandmarks, [-1, 7, 2]), this.inputSizeTensor), this.anchors[index]);
|
2020-10-12 01:22:43 +02:00
|
|
|
return tf.mul(landmarks, this.inputSizeTensor);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-11-08 07:17:25 +01:00
|
|
|
async getBoxes(input, config) {
|
2021-09-02 14:50:16 +02:00
|
|
|
const t: Record<string, Tensor> = {};
|
2021-11-02 16:07:11 +01:00
|
|
|
t.batched = this.model.execute(input) as Tensor;
|
2021-09-02 14:50:16 +02:00
|
|
|
t.predictions = tf.squeeze(t.batched);
|
|
|
|
t.scores = tf.tidy(() => tf.squeeze(tf.sigmoid(tf.slice(t.predictions, [0, 0], [-1, 1]))));
|
|
|
|
const scores = await t.scores.data();
|
|
|
|
t.boxes = tf.slice(t.predictions, [0, 1], [-1, 4]);
|
|
|
|
t.norm = this.normalizeBoxes(t.boxes);
|
2021-09-19 20:07:53 +02:00
|
|
|
// box detection is flaky so we look for 3x boxes than we need results
|
|
|
|
t.nms = await tf.image.nonMaxSuppressionAsync(t.norm, t.scores, 3 * config.hand.maxDetected, config.hand.iouThreshold, config.hand.minConfidence);
|
2021-09-02 14:50:16 +02:00
|
|
|
const nms = await t.nms.array() as Array<number>;
|
2021-05-23 03:47:59 +02:00
|
|
|
const hands: Array<{ box: Tensor, palmLandmarks: Tensor, confidence: number }> = [];
|
2021-09-02 14:50:16 +02:00
|
|
|
for (const index of nms) {
|
|
|
|
const palmBox = tf.slice(t.norm, [index, 0], [1, -1]);
|
|
|
|
const palmLandmarks = tf.tidy(() => tf.reshape(this.normalizeLandmarks(tf.slice(t.predictions, [index, 5], [1, 14]), index), [-1, 2]));
|
|
|
|
hands.push({ box: palmBox, palmLandmarks, confidence: scores[index] });
|
2020-11-04 07:11:24 +01:00
|
|
|
}
|
2021-09-02 14:50:16 +02:00
|
|
|
for (const tensor of Object.keys(t)) tf.dispose(t[tensor]); // dispose all
|
2020-11-04 07:11:24 +01:00
|
|
|
return hands;
|
2020-10-12 01:22:43 +02:00
|
|
|
}
|
|
|
|
|
2021-05-22 20:53:51 +02:00
|
|
|
async estimateHandBounds(input, config): Promise<{ startPoint: number[]; endPoint: number[]; palmLandmarks: number[]; confidence: number }[]> {
|
2020-11-04 07:11:24 +01:00
|
|
|
const inputHeight = input.shape[1];
|
|
|
|
const inputWidth = input.shape[2];
|
2021-07-29 22:06:03 +02:00
|
|
|
const image = tf.tidy(() => tf.sub(tf.div(tf.image.resizeBilinear(input, [this.inputSize, this.inputSize]), 127.5), 1));
|
2020-11-08 07:17:25 +01:00
|
|
|
const predictions = await this.getBoxes(image, config);
|
2021-07-29 22:06:03 +02:00
|
|
|
tf.dispose(image);
|
2021-05-22 20:53:51 +02:00
|
|
|
const hands: Array<{ startPoint: number[]; endPoint: number[]; palmLandmarks: number[]; confidence: number }> = [];
|
2020-12-11 16:11:49 +01:00
|
|
|
if (!predictions || predictions.length === 0) return hands;
|
2020-11-04 07:11:24 +01:00
|
|
|
for (const prediction of predictions) {
|
2021-08-12 15:31:16 +02:00
|
|
|
const boxes = await prediction.box.data();
|
2020-11-08 15:56:02 +01:00
|
|
|
const startPoint = boxes.slice(0, 2);
|
|
|
|
const endPoint = boxes.slice(2, 4);
|
2021-08-12 00:59:02 +02:00
|
|
|
const palmLandmarks = await prediction.palmLandmarks.array();
|
2021-07-29 22:06:03 +02:00
|
|
|
tf.dispose(prediction.box);
|
|
|
|
tf.dispose(prediction.palmLandmarks);
|
2021-10-20 15:10:57 +02:00
|
|
|
hands.push(util.scaleBoxCoordinates({ startPoint, endPoint, palmLandmarks, confidence: prediction.confidence }, [inputWidth / this.inputSize, inputHeight / this.inputSize]));
|
2020-10-14 17:43:33 +02:00
|
|
|
}
|
|
|
|
return hands;
|
2020-10-12 01:22:43 +02:00
|
|
|
}
|
|
|
|
}
|