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 type * as detector from './handposedetector';
|
2021-09-13 19:28:35 +02:00
|
|
|
import type { Tensor, GraphModel } from '../tfjs/types';
|
2021-09-27 19:58:13 +02:00
|
|
|
import { env } from '../util/env';
|
2021-10-22 22:09:52 +02:00
|
|
|
import { now } from '../util/util';
|
2020-11-04 07:11:24 +01:00
|
|
|
|
2021-05-05 16:07:44 +02:00
|
|
|
const palmBoxEnlargeFactor = 5; // default 3
|
|
|
|
const handBoxEnlargeFactor = 1.65; // default 1.65
|
|
|
|
const palmLandmarkIds = [0, 5, 9, 13, 17, 1, 2];
|
|
|
|
const palmLandmarksPalmBase = 0;
|
|
|
|
const palmLandmarksMiddleFingerBase = 2;
|
2021-10-22 22:09:52 +02:00
|
|
|
let lastTime = 0;
|
2020-11-04 07:11:24 +01:00
|
|
|
|
2021-02-08 17:39:09 +01:00
|
|
|
export class HandPipeline {
|
2021-05-22 20:53:51 +02:00
|
|
|
handDetector: detector.HandDetector;
|
2021-05-23 03:47:59 +02:00
|
|
|
handPoseModel: GraphModel;
|
2021-02-08 17:39:09 +01:00
|
|
|
inputSize: number;
|
2021-05-22 20:53:51 +02:00
|
|
|
storedBoxes: Array<{ startPoint: number[]; endPoint: number[]; palmLandmarks: number[]; confidence: number } | null>;
|
2021-02-08 17:39:09 +01:00
|
|
|
skipped: number;
|
|
|
|
detectedHands: number;
|
|
|
|
|
2021-05-22 20:53:51 +02:00
|
|
|
constructor(handDetector, handPoseModel) {
|
2020-11-26 16:37:04 +01:00
|
|
|
this.handDetector = handDetector;
|
2021-05-22 20:53:51 +02:00
|
|
|
this.handPoseModel = handPoseModel;
|
2021-09-19 20:07:53 +02:00
|
|
|
this.inputSize = this.handPoseModel && this.handPoseModel.inputs[0].shape ? this.handPoseModel.inputs[0].shape[2] : 0;
|
2020-11-08 07:17:25 +01:00
|
|
|
this.storedBoxes = [];
|
2020-12-11 16:11:49 +01:00
|
|
|
this.skipped = 0;
|
2020-11-04 20:59:30 +01:00
|
|
|
this.detectedHands = 0;
|
2020-11-04 07:11:24 +01:00
|
|
|
}
|
|
|
|
|
2021-05-05 16:07:44 +02:00
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
|
|
calculateLandmarksBoundingBox(landmarks) {
|
|
|
|
const xs = landmarks.map((d) => d[0]);
|
|
|
|
const ys = landmarks.map((d) => d[1]);
|
|
|
|
const startPoint = [Math.min(...xs), Math.min(...ys)];
|
|
|
|
const endPoint = [Math.max(...xs), Math.max(...ys)];
|
|
|
|
return { startPoint, endPoint };
|
|
|
|
}
|
|
|
|
|
2020-11-04 07:11:24 +01:00
|
|
|
getBoxForPalmLandmarks(palmLandmarks, rotationMatrix) {
|
2020-12-10 21:46:45 +01:00
|
|
|
const rotatedPalmLandmarks = palmLandmarks.map((coord) => util.rotatePoint([...coord, 1], rotationMatrix));
|
2020-11-04 07:11:24 +01:00
|
|
|
const boxAroundPalm = this.calculateLandmarksBoundingBox(rotatedPalmLandmarks);
|
2021-10-20 15:10:57 +02:00
|
|
|
return util.enlargeBox(util.squarifyBox(boxAroundPalm), palmBoxEnlargeFactor);
|
2020-11-04 07:11:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
getBoxForHandLandmarks(landmarks) {
|
|
|
|
const boundingBox = this.calculateLandmarksBoundingBox(landmarks);
|
2021-10-20 15:10:57 +02:00
|
|
|
const boxAroundHand = util.enlargeBox(util.squarifyBox(boundingBox), handBoxEnlargeFactor);
|
2020-12-10 21:46:45 +01:00
|
|
|
boxAroundHand.palmLandmarks = [];
|
2021-05-05 16:07:44 +02:00
|
|
|
for (let i = 0; i < palmLandmarkIds.length; i++) {
|
|
|
|
boxAroundHand.palmLandmarks.push(landmarks[palmLandmarkIds[i]].slice(0, 2));
|
2020-11-04 07:11:24 +01:00
|
|
|
}
|
|
|
|
return boxAroundHand;
|
|
|
|
}
|
|
|
|
|
|
|
|
transformRawCoords(rawCoords, box2, angle, rotationMatrix) {
|
2021-10-20 15:10:57 +02:00
|
|
|
const boxSize = util.getBoxSize(box2);
|
2020-12-17 01:16:54 +01:00
|
|
|
const scaleFactor = [boxSize[0] / this.inputSize, boxSize[1] / this.inputSize, (boxSize[0] + boxSize[1]) / this.inputSize / 2];
|
2020-11-04 07:11:24 +01:00
|
|
|
const coordsScaled = rawCoords.map((coord) => [
|
|
|
|
scaleFactor[0] * (coord[0] - this.inputSize / 2),
|
|
|
|
scaleFactor[1] * (coord[1] - this.inputSize / 2),
|
2020-12-17 01:16:54 +01:00
|
|
|
scaleFactor[2] * coord[2],
|
2020-11-04 07:11:24 +01:00
|
|
|
]);
|
|
|
|
const coordsRotationMatrix = util.buildRotationMatrix(angle, [0, 0]);
|
|
|
|
const coordsRotated = coordsScaled.map((coord) => {
|
|
|
|
const rotated = util.rotatePoint(coord, coordsRotationMatrix);
|
|
|
|
return [...rotated, coord[2]];
|
|
|
|
});
|
|
|
|
const inverseRotationMatrix = util.invertTransformMatrix(rotationMatrix);
|
2021-10-20 15:10:57 +02:00
|
|
|
const boxCenter = [...util.getBoxCenter(box2), 1];
|
2020-11-04 07:11:24 +01:00
|
|
|
const originalBoxCenter = [
|
|
|
|
util.dot(boxCenter, inverseRotationMatrix[0]),
|
|
|
|
util.dot(boxCenter, inverseRotationMatrix[1]),
|
|
|
|
];
|
|
|
|
return coordsRotated.map((coord) => [
|
2021-06-01 14:59:09 +02:00
|
|
|
Math.trunc(coord[0] + originalBoxCenter[0]),
|
|
|
|
Math.trunc(coord[1] + originalBoxCenter[1]),
|
|
|
|
Math.trunc(coord[2]),
|
2020-11-04 07:11:24 +01:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async estimateHands(image, config) {
|
2020-11-08 07:17:25 +01:00
|
|
|
let useFreshBox = false;
|
2020-11-08 15:56:02 +01:00
|
|
|
|
|
|
|
// run new detector every skipFrames unless we only want box to start with
|
|
|
|
let boxes;
|
2021-05-18 17:26:16 +02:00
|
|
|
|
2021-09-02 14:50:16 +02:00
|
|
|
// console.log('handpipeline:estimateHands:skip criteria', this.skipped, config.hand.skipFrames, !config.hand.landmarks, !config.skipFrame); // should skip hand detector?
|
2021-10-22 22:09:52 +02:00
|
|
|
if ((this.skipped === 0) || ((this.skipped > config.hand.skipFrames) && ((config.hand.skipTime || 0) <= (now() - lastTime))) || !config.hand.landmarks || !config.skipFrame) {
|
2020-11-26 16:37:04 +01:00
|
|
|
boxes = await this.handDetector.estimateHandBounds(image, config);
|
2020-12-11 16:11:49 +01:00
|
|
|
this.skipped = 0;
|
2020-11-08 15:56:02 +01:00
|
|
|
}
|
2021-05-18 17:26:16 +02:00
|
|
|
if (config.skipFrame) this.skipped++;
|
2020-11-08 15:56:02 +01:00
|
|
|
|
2020-11-08 07:17:25 +01:00
|
|
|
// if detector result count doesn't match current working set, use it to reset current working set
|
2021-04-25 19:16:04 +02:00
|
|
|
if (boxes && (boxes.length > 0) && ((boxes.length !== this.detectedHands) && (this.detectedHands !== config.hand.maxDetected) || !config.hand.landmarks)) {
|
2020-11-08 07:17:25 +01:00
|
|
|
this.detectedHands = 0;
|
2020-11-26 16:37:04 +01:00
|
|
|
this.storedBoxes = [...boxes];
|
|
|
|
// for (const possible of boxes) this.storedBoxes.push(possible);
|
2020-11-08 07:17:25 +01:00
|
|
|
if (this.storedBoxes.length > 0) useFreshBox = true;
|
2020-11-04 07:11:24 +01:00
|
|
|
}
|
2021-09-21 22:48:16 +02:00
|
|
|
const hands: Array<{ landmarks: number[], confidence: number, boxConfidence: number, fingerConfidence: number, box: { topLeft: number[], bottomRight: number[] } }> = [];
|
2021-03-01 23:20:02 +01:00
|
|
|
|
2020-11-08 07:17:25 +01:00
|
|
|
// go through working set of boxes
|
2020-11-26 16:37:04 +01:00
|
|
|
for (let i = 0; i < this.storedBoxes.length; i++) {
|
2020-11-08 07:17:25 +01:00
|
|
|
const currentBox = this.storedBoxes[i];
|
2020-11-04 07:11:24 +01:00
|
|
|
if (!currentBox) continue;
|
2020-11-17 23:42:44 +01:00
|
|
|
if (config.hand.landmarks) {
|
2021-05-05 16:07:44 +02:00
|
|
|
const angle = config.hand.rotation ? util.computeRotation(currentBox.palmLandmarks[palmLandmarksPalmBase], currentBox.palmLandmarks[palmLandmarksMiddleFingerBase]) : 0;
|
2021-10-20 15:10:57 +02:00
|
|
|
const palmCenter = util.getBoxCenter(currentBox);
|
2020-11-08 15:56:02 +01:00
|
|
|
const palmCenterNormalized = [palmCenter[0] / image.shape[2], palmCenter[1] / image.shape[1]];
|
2021-09-12 19:17:33 +02:00
|
|
|
const rotatedImage = config.hand.rotation && env.kernels.includes('rotatewithoffset') ? tf.image.rotateWithOffset(image, angle, 0, palmCenterNormalized) : image.clone();
|
2020-11-08 15:56:02 +01:00
|
|
|
const rotationMatrix = util.buildRotationMatrix(-angle, palmCenter);
|
|
|
|
const newBox = useFreshBox ? this.getBoxForPalmLandmarks(currentBox.palmLandmarks, rotationMatrix) : currentBox;
|
2021-10-20 15:10:57 +02:00
|
|
|
const croppedInput = util.cutBoxFromImageAndResize(newBox, rotatedImage, [this.inputSize, this.inputSize]);
|
2021-07-29 22:06:03 +02:00
|
|
|
const handImage = tf.div(croppedInput, 255);
|
|
|
|
tf.dispose(croppedInput);
|
|
|
|
tf.dispose(rotatedImage);
|
2021-05-23 03:47:59 +02:00
|
|
|
const [confidenceT, keypoints] = await this.handPoseModel.predict(handImage) as Array<Tensor>;
|
2021-10-22 22:09:52 +02:00
|
|
|
lastTime = now();
|
2021-07-29 22:06:03 +02:00
|
|
|
tf.dispose(handImage);
|
2021-08-14 17:16:26 +02:00
|
|
|
const confidence = (await confidenceT.data())[0];
|
2021-07-29 22:06:03 +02:00
|
|
|
tf.dispose(confidenceT);
|
2021-09-02 14:50:16 +02:00
|
|
|
if (confidence >= config.hand.minConfidence / 4) {
|
2020-11-08 15:56:02 +01:00
|
|
|
const keypointsReshaped = tf.reshape(keypoints, [-1, 3]);
|
2021-08-12 00:59:02 +02:00
|
|
|
const rawCoords = await keypointsReshaped.array();
|
2021-07-29 22:06:03 +02:00
|
|
|
tf.dispose(keypoints);
|
|
|
|
tf.dispose(keypointsReshaped);
|
2020-11-08 15:56:02 +01:00
|
|
|
const coords = this.transformRawCoords(rawCoords, newBox, angle, rotationMatrix);
|
|
|
|
const nextBoundingBox = this.getBoxForHandLandmarks(coords);
|
2021-05-22 20:53:51 +02:00
|
|
|
this.storedBoxes[i] = { ...nextBoundingBox, confidence };
|
2020-11-08 15:56:02 +01:00
|
|
|
const result = {
|
|
|
|
landmarks: coords,
|
2020-11-26 16:37:04 +01:00
|
|
|
confidence,
|
2021-09-21 22:48:16 +02:00
|
|
|
boxConfidence: currentBox.confidence,
|
|
|
|
fingerConfidence: confidence,
|
2021-02-08 18:47:38 +01:00
|
|
|
box: { topLeft: nextBoundingBox.startPoint, bottomRight: nextBoundingBox.endPoint },
|
2020-11-08 15:56:02 +01:00
|
|
|
};
|
|
|
|
hands.push(result);
|
|
|
|
} else {
|
|
|
|
this.storedBoxes[i] = null;
|
|
|
|
}
|
2021-07-29 22:06:03 +02:00
|
|
|
tf.dispose(keypoints);
|
2020-11-04 20:59:30 +01:00
|
|
|
} else {
|
2021-05-05 16:07:44 +02:00
|
|
|
// const enlarged = box.enlargeBox(box.squarifyBox(box.shiftBox(currentBox, HAND_BOX_SHIFT_VECTOR)), handBoxEnlargeFactor);
|
2021-10-20 15:10:57 +02:00
|
|
|
const enlarged = util.enlargeBox(util.squarifyBox(currentBox), handBoxEnlargeFactor);
|
2020-11-04 20:59:30 +01:00
|
|
|
const result = {
|
2020-11-08 15:56:02 +01:00
|
|
|
confidence: currentBox.confidence,
|
2021-09-21 22:48:16 +02:00
|
|
|
boxConfidence: currentBox.confidence,
|
|
|
|
fingerConfidence: 0,
|
2021-02-08 18:47:38 +01:00
|
|
|
box: { topLeft: enlarged.startPoint, bottomRight: enlarged.endPoint },
|
2021-09-16 00:58:54 +02:00
|
|
|
landmarks: [],
|
2020-11-04 20:59:30 +01:00
|
|
|
};
|
|
|
|
hands.push(result);
|
2020-11-04 07:11:24 +01:00
|
|
|
}
|
|
|
|
}
|
2020-11-08 07:17:25 +01:00
|
|
|
this.storedBoxes = this.storedBoxes.filter((a) => a !== null);
|
2020-11-04 20:59:30 +01:00
|
|
|
this.detectedHands = hands.length;
|
2021-09-19 20:07:53 +02:00
|
|
|
if (hands.length > config.hand.maxDetected) hands.length = config.hand.maxDetected;
|
2020-11-04 07:11:24 +01:00
|
|
|
return hands;
|
|
|
|
}
|
|
|
|
}
|