mirror of https://github.com/vladmandic/human
5296 lines
128 KiB
JavaScript
5296 lines
128 KiB
JavaScript
var __defineProperty = Object.defineProperty;
|
|
var __commonJS = (callback, module) => () => {
|
|
if (!module) {
|
|
module = {exports: {}};
|
|
callback(module.exports, module);
|
|
}
|
|
return module.exports;
|
|
};
|
|
var __markAsModule = (target) => {
|
|
return __defineProperty(target, "__esModule", {value: true});
|
|
};
|
|
var __export = (target, all) => {
|
|
__markAsModule(target);
|
|
for (var name in all)
|
|
__defineProperty(target, name, {get: all[name], enumerable: true});
|
|
};
|
|
|
|
// src/blazeface/box.js
|
|
var require_box = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
exports.disposeBox = (box) => {
|
|
box.startEndTensor.dispose();
|
|
box.startPoint.dispose();
|
|
box.endPoint.dispose();
|
|
};
|
|
exports.createBox = (startEndTensor) => ({
|
|
startEndTensor,
|
|
startPoint: tf.slice(startEndTensor, [0, 0], [-1, 2]),
|
|
endPoint: tf.slice(startEndTensor, [0, 2], [-1, 2])
|
|
});
|
|
exports.scaleBox = (box, factors) => {
|
|
const starts = tf.mul(box.startPoint, factors);
|
|
const ends = tf.mul(box.endPoint, factors);
|
|
const newCoordinates = tf.concat2d([starts, ends], 1);
|
|
return exports.createBox(newCoordinates);
|
|
};
|
|
});
|
|
|
|
// src/blazeface/face.js
|
|
var require_face = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const bounding = require_box();
|
|
const ANCHORS_CONFIG = {
|
|
strides: [8, 16],
|
|
anchors: [2, 6]
|
|
};
|
|
const NUM_LANDMARKS = 6;
|
|
function generateAnchors(width, height, outputSpec) {
|
|
const anchors = [];
|
|
for (let i = 0; i < outputSpec.strides.length; i++) {
|
|
const stride = outputSpec.strides[i];
|
|
const gridRows = Math.floor((height + stride - 1) / stride);
|
|
const gridCols = Math.floor((width + stride - 1) / stride);
|
|
const anchorsNum = outputSpec.anchors[i];
|
|
for (let gridY = 0; gridY < gridRows; gridY++) {
|
|
const anchorY = stride * (gridY + 0.5);
|
|
for (let gridX = 0; gridX < gridCols; gridX++) {
|
|
const anchorX = stride * (gridX + 0.5);
|
|
for (let n = 0; n < anchorsNum; n++) {
|
|
anchors.push([anchorX, anchorY]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return anchors;
|
|
}
|
|
function decodeBounds(boxOutputs, anchors, inputSize) {
|
|
const boxStarts = tf.slice(boxOutputs, [0, 1], [-1, 2]);
|
|
const centers = tf.add(boxStarts, anchors);
|
|
const boxSizes = tf.slice(boxOutputs, [0, 3], [-1, 2]);
|
|
const boxSizesNormalized = tf.div(boxSizes, inputSize);
|
|
const centersNormalized = tf.div(centers, inputSize);
|
|
const halfBoxSize = tf.div(boxSizesNormalized, 2);
|
|
const starts = tf.sub(centersNormalized, halfBoxSize);
|
|
const ends = tf.add(centersNormalized, halfBoxSize);
|
|
const startNormalized = tf.mul(starts, inputSize);
|
|
const endNormalized = tf.mul(ends, inputSize);
|
|
const concatAxis = 1;
|
|
return tf.concat2d([startNormalized, endNormalized], concatAxis);
|
|
}
|
|
function scaleBoxFromPrediction(face, scaleFactor) {
|
|
return tf.tidy(() => {
|
|
const box = face["box"] ? face["box"] : face;
|
|
return bounding.scaleBox(box, scaleFactor).startEndTensor.squeeze();
|
|
});
|
|
}
|
|
class BlazeFaceModel {
|
|
constructor(model, config) {
|
|
this.blazeFaceModel = model;
|
|
this.width = config.detector.inputSize;
|
|
this.height = config.detector.inputSize;
|
|
this.maxFaces = config.detector.maxFaces;
|
|
this.anchorsData = generateAnchors(config.detector.inputSize, config.detector.inputSize, ANCHORS_CONFIG);
|
|
this.anchors = tf.tensor2d(this.anchorsData);
|
|
this.inputSizeData = [config.detector.inputSize, config.detector.inputSize];
|
|
this.inputSize = tf.tensor1d([config.detector.inputSize, config.detector.inputSize]);
|
|
this.iouThreshold = config.detector.iouThreshold;
|
|
this.scoreThreshold = config.detector.scoreThreshold;
|
|
}
|
|
async getBoundingBoxes(inputImage, returnTensors, annotateBoxes = true) {
|
|
const [detectedOutputs, boxes, scores] = tf.tidy(() => {
|
|
const resizedImage = inputImage.resizeBilinear([this.width, this.height]);
|
|
const normalizedImage = tf.mul(tf.sub(resizedImage.div(255), 0.5), 2);
|
|
const batchedPrediction = this.blazeFaceModel.predict(normalizedImage);
|
|
const prediction = batchedPrediction.squeeze();
|
|
const decodedBounds = decodeBounds(prediction, this.anchors, this.inputSize);
|
|
const logits = tf.slice(prediction, [0, 0], [-1, 1]);
|
|
const scoresOut = tf.sigmoid(logits).squeeze();
|
|
return [prediction, decodedBounds, scoresOut];
|
|
});
|
|
const boxIndicesTensor = await tf.image.nonMaxSuppressionAsync(boxes, scores, this.maxFaces, this.iouThreshold, this.scoreThreshold);
|
|
const boxIndices = await boxIndicesTensor.array();
|
|
boxIndicesTensor.dispose();
|
|
let boundingBoxes = boxIndices.map((boxIndex) => tf.slice(boxes, [boxIndex, 0], [1, -1]));
|
|
if (!returnTensors) {
|
|
boundingBoxes = await Promise.all(boundingBoxes.map(async (boundingBox) => {
|
|
const vals = await boundingBox.array();
|
|
boundingBox.dispose();
|
|
return vals;
|
|
}));
|
|
}
|
|
const originalHeight = inputImage.shape[1];
|
|
const originalWidth = inputImage.shape[2];
|
|
let scaleFactor;
|
|
if (returnTensors) {
|
|
scaleFactor = tf.div([originalWidth, originalHeight], this.inputSize);
|
|
} else {
|
|
scaleFactor = [
|
|
originalWidth / this.inputSizeData[0],
|
|
originalHeight / this.inputSizeData[1]
|
|
];
|
|
}
|
|
const annotatedBoxes = [];
|
|
for (let i = 0; i < boundingBoxes.length; i++) {
|
|
const boundingBox = boundingBoxes[i];
|
|
const annotatedBox = tf.tidy(() => {
|
|
const box = boundingBox instanceof tf.Tensor ? bounding.createBox(boundingBox) : bounding.createBox(tf.tensor2d(boundingBox));
|
|
if (!annotateBoxes) {
|
|
return box;
|
|
}
|
|
const boxIndex = boxIndices[i];
|
|
let anchor;
|
|
if (returnTensors) {
|
|
anchor = this.anchors.slice([boxIndex, 0], [1, 2]);
|
|
} else {
|
|
anchor = this.anchorsData[boxIndex];
|
|
}
|
|
const landmarks = tf.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]).squeeze().reshape([NUM_LANDMARKS, -1]);
|
|
const probability = tf.slice(scores, [boxIndex], [1]);
|
|
return {box, landmarks, probability, anchor};
|
|
});
|
|
annotatedBoxes.push(annotatedBox);
|
|
}
|
|
boxes.dispose();
|
|
scores.dispose();
|
|
detectedOutputs.dispose();
|
|
return {
|
|
boxes: annotatedBoxes,
|
|
scaleFactor
|
|
};
|
|
}
|
|
async estimateFaces(input, returnTensors = false, annotateBoxes = true) {
|
|
const image = tf.tidy(() => {
|
|
if (!(input instanceof tf.Tensor)) {
|
|
input = tf.browser.fromPixels(input);
|
|
}
|
|
return input.toFloat().expandDims(0);
|
|
});
|
|
const {boxes, scaleFactor} = await this.getBoundingBoxes(image, returnTensors, annotateBoxes);
|
|
image.dispose();
|
|
if (returnTensors) {
|
|
return boxes.map((face) => {
|
|
const scaledBox = scaleBoxFromPrediction(face, scaleFactor);
|
|
const normalizedFace = {
|
|
topLeft: scaledBox.slice([0], [2]),
|
|
bottomRight: scaledBox.slice([2], [2])
|
|
};
|
|
if (annotateBoxes) {
|
|
const {landmarks, probability, anchor} = face;
|
|
const normalizedLandmarks = landmarks.add(anchor).mul(scaleFactor);
|
|
normalizedFace.landmarks = normalizedLandmarks;
|
|
normalizedFace.probability = probability;
|
|
}
|
|
return normalizedFace;
|
|
});
|
|
}
|
|
return Promise.all(boxes.map(async (face) => {
|
|
const scaledBox = scaleBoxFromPrediction(face, scaleFactor);
|
|
let normalizedFace;
|
|
if (!annotateBoxes) {
|
|
const boxData = await scaledBox.array();
|
|
normalizedFace = {
|
|
topLeft: boxData.slice(0, 2),
|
|
bottomRight: boxData.slice(2)
|
|
};
|
|
} else {
|
|
const [landmarkData, boxData, probabilityData] = await Promise.all([face.landmarks, scaledBox, face.probability].map(async (d) => d.array()));
|
|
const anchor = face.anchor;
|
|
const [scaleFactorX, scaleFactorY] = scaleFactor;
|
|
const scaledLandmarks = landmarkData.map((landmark) => [
|
|
(landmark[0] + anchor[0]) * scaleFactorX,
|
|
(landmark[1] + anchor[1]) * scaleFactorY
|
|
]);
|
|
normalizedFace = {
|
|
topLeft: boxData.slice(0, 2),
|
|
bottomRight: boxData.slice(2),
|
|
landmarks: scaledLandmarks,
|
|
probability: probabilityData
|
|
};
|
|
bounding.disposeBox(face.box);
|
|
face.landmarks.dispose();
|
|
face.probability.dispose();
|
|
}
|
|
scaledBox.dispose();
|
|
return normalizedFace;
|
|
}));
|
|
}
|
|
}
|
|
exports.BlazeFaceModel = BlazeFaceModel;
|
|
});
|
|
|
|
// src/blazeface/index.js
|
|
var require_blazeface = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const face = require_face();
|
|
async function load(config) {
|
|
const blazeface = await tf.loadGraphModel(config.detector.modelPath, {fromTFHub: config.detector.modelPath.includes("tfhub.dev")});
|
|
const model = new face.BlazeFaceModel(blazeface, config);
|
|
return model;
|
|
}
|
|
exports.load = load;
|
|
const face_2 = require_face();
|
|
Object.defineProperty(exports, "BlazeFaceModel", {enumerable: true, get() {
|
|
return face_2.BlazeFaceModel;
|
|
}});
|
|
});
|
|
|
|
// src/facemesh/keypoints.js
|
|
var require_keypoints = __commonJS((exports) => {
|
|
exports.MESH_ANNOTATIONS = {
|
|
silhouette: [
|
|
10,
|
|
338,
|
|
297,
|
|
332,
|
|
284,
|
|
251,
|
|
389,
|
|
356,
|
|
454,
|
|
323,
|
|
361,
|
|
288,
|
|
397,
|
|
365,
|
|
379,
|
|
378,
|
|
400,
|
|
377,
|
|
152,
|
|
148,
|
|
176,
|
|
149,
|
|
150,
|
|
136,
|
|
172,
|
|
58,
|
|
132,
|
|
93,
|
|
234,
|
|
127,
|
|
162,
|
|
21,
|
|
54,
|
|
103,
|
|
67,
|
|
109
|
|
],
|
|
lipsUpperOuter: [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291],
|
|
lipsLowerOuter: [146, 91, 181, 84, 17, 314, 405, 321, 375, 291],
|
|
lipsUpperInner: [78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308],
|
|
lipsLowerInner: [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308],
|
|
rightEyeUpper0: [246, 161, 160, 159, 158, 157, 173],
|
|
rightEyeLower0: [33, 7, 163, 144, 145, 153, 154, 155, 133],
|
|
rightEyeUpper1: [247, 30, 29, 27, 28, 56, 190],
|
|
rightEyeLower1: [130, 25, 110, 24, 23, 22, 26, 112, 243],
|
|
rightEyeUpper2: [113, 225, 224, 223, 222, 221, 189],
|
|
rightEyeLower2: [226, 31, 228, 229, 230, 231, 232, 233, 244],
|
|
rightEyeLower3: [143, 111, 117, 118, 119, 120, 121, 128, 245],
|
|
rightEyebrowUpper: [156, 70, 63, 105, 66, 107, 55, 193],
|
|
rightEyebrowLower: [35, 124, 46, 53, 52, 65],
|
|
rightEyeIris: [473, 474, 475, 476, 477],
|
|
leftEyeUpper0: [466, 388, 387, 386, 385, 384, 398],
|
|
leftEyeLower0: [263, 249, 390, 373, 374, 380, 381, 382, 362],
|
|
leftEyeUpper1: [467, 260, 259, 257, 258, 286, 414],
|
|
leftEyeLower1: [359, 255, 339, 254, 253, 252, 256, 341, 463],
|
|
leftEyeUpper2: [342, 445, 444, 443, 442, 441, 413],
|
|
leftEyeLower2: [446, 261, 448, 449, 450, 451, 452, 453, 464],
|
|
leftEyeLower3: [372, 340, 346, 347, 348, 349, 350, 357, 465],
|
|
leftEyebrowUpper: [383, 300, 293, 334, 296, 336, 285, 417],
|
|
leftEyebrowLower: [265, 353, 276, 283, 282, 295],
|
|
leftEyeIris: [468, 469, 470, 471, 472],
|
|
midwayBetweenEyes: [168],
|
|
noseTip: [1],
|
|
noseBottom: [2],
|
|
noseRightCorner: [98],
|
|
noseLeftCorner: [327],
|
|
rightCheek: [205],
|
|
leftCheek: [425]
|
|
};
|
|
});
|
|
|
|
// src/facemesh/box.js
|
|
var require_box2 = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
function scaleBoxCoordinates(box, factor) {
|
|
const startPoint = [box.startPoint[0] * factor[0], box.startPoint[1] * factor[1]];
|
|
const endPoint = [box.endPoint[0] * factor[0], box.endPoint[1] * factor[1]];
|
|
return {startPoint, endPoint};
|
|
}
|
|
exports.scaleBoxCoordinates = scaleBoxCoordinates;
|
|
function getBoxSize(box) {
|
|
return [
|
|
Math.abs(box.endPoint[0] - box.startPoint[0]),
|
|
Math.abs(box.endPoint[1] - box.startPoint[1])
|
|
];
|
|
}
|
|
exports.getBoxSize = getBoxSize;
|
|
function getBoxCenter(box) {
|
|
return [
|
|
box.startPoint[0] + (box.endPoint[0] - box.startPoint[0]) / 2,
|
|
box.startPoint[1] + (box.endPoint[1] - box.startPoint[1]) / 2
|
|
];
|
|
}
|
|
exports.getBoxCenter = getBoxCenter;
|
|
function cutBoxFromImageAndResize(box, image, cropSize) {
|
|
const h = image.shape[1];
|
|
const w = image.shape[2];
|
|
const boxes = [[
|
|
box.startPoint[1] / h,
|
|
box.startPoint[0] / w,
|
|
box.endPoint[1] / h,
|
|
box.endPoint[0] / w
|
|
]];
|
|
return tf.image.cropAndResize(image, boxes, [0], cropSize);
|
|
}
|
|
exports.cutBoxFromImageAndResize = cutBoxFromImageAndResize;
|
|
function enlargeBox(box, factor = 1.5) {
|
|
const center = getBoxCenter(box);
|
|
const size = getBoxSize(box);
|
|
const newHalfSize = [factor * size[0] / 2, factor * size[1] / 2];
|
|
const startPoint = [center[0] - newHalfSize[0], center[1] - newHalfSize[1]];
|
|
const endPoint = [center[0] + newHalfSize[0], center[1] + newHalfSize[1]];
|
|
return {startPoint, endPoint, landmarks: box.landmarks};
|
|
}
|
|
exports.enlargeBox = enlargeBox;
|
|
function squarifyBox(box) {
|
|
const centers = getBoxCenter(box);
|
|
const size = getBoxSize(box);
|
|
const maxEdge = Math.max(...size);
|
|
const halfSize = maxEdge / 2;
|
|
const startPoint = [centers[0] - halfSize, centers[1] - halfSize];
|
|
const endPoint = [centers[0] + halfSize, centers[1] + halfSize];
|
|
return {startPoint, endPoint, landmarks: box.landmarks};
|
|
}
|
|
exports.squarifyBox = squarifyBox;
|
|
});
|
|
|
|
// src/facemesh/util.js
|
|
var require_util = __commonJS((exports) => {
|
|
exports.IDENTITY_MATRIX = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
|
|
function normalizeRadians(angle) {
|
|
return angle - 2 * Math.PI * Math.floor((angle + Math.PI) / (2 * Math.PI));
|
|
}
|
|
exports.normalizeRadians = normalizeRadians;
|
|
function computeRotation(point1, point2) {
|
|
const radians = Math.PI / 2 - Math.atan2(-(point2[1] - point1[1]), point2[0] - point1[0]);
|
|
return normalizeRadians(radians);
|
|
}
|
|
exports.computeRotation = computeRotation;
|
|
function radToDegrees(rad) {
|
|
return rad * 180 / Math.PI;
|
|
}
|
|
exports.radToDegrees = radToDegrees;
|
|
function buildTranslationMatrix(x, y) {
|
|
return [[1, 0, x], [0, 1, y], [0, 0, 1]];
|
|
}
|
|
function dot(v1, v2) {
|
|
let product = 0;
|
|
for (let i = 0; i < v1.length; i++) {
|
|
product += v1[i] * v2[i];
|
|
}
|
|
return product;
|
|
}
|
|
exports.dot = dot;
|
|
function getColumnFrom2DArr(arr, columnIndex) {
|
|
const column = [];
|
|
for (let i = 0; i < arr.length; i++) {
|
|
column.push(arr[i][columnIndex]);
|
|
}
|
|
return column;
|
|
}
|
|
exports.getColumnFrom2DArr = getColumnFrom2DArr;
|
|
function multiplyTransformMatrices(mat1, mat2) {
|
|
const product = [];
|
|
const size = mat1.length;
|
|
for (let row = 0; row < size; row++) {
|
|
product.push([]);
|
|
for (let col = 0; col < size; col++) {
|
|
product[row].push(dot(mat1[row], getColumnFrom2DArr(mat2, col)));
|
|
}
|
|
}
|
|
return product;
|
|
}
|
|
function buildRotationMatrix(rotation, center) {
|
|
const cosA = Math.cos(rotation);
|
|
const sinA = Math.sin(rotation);
|
|
const rotationMatrix = [[cosA, -sinA, 0], [sinA, cosA, 0], [0, 0, 1]];
|
|
const translationMatrix = buildTranslationMatrix(center[0], center[1]);
|
|
const translationTimesRotation = multiplyTransformMatrices(translationMatrix, rotationMatrix);
|
|
const negativeTranslationMatrix = buildTranslationMatrix(-center[0], -center[1]);
|
|
return multiplyTransformMatrices(translationTimesRotation, negativeTranslationMatrix);
|
|
}
|
|
exports.buildRotationMatrix = buildRotationMatrix;
|
|
function invertTransformMatrix(matrix) {
|
|
const rotationComponent = [[matrix[0][0], matrix[1][0]], [matrix[0][1], matrix[1][1]]];
|
|
const translationComponent = [matrix[0][2], matrix[1][2]];
|
|
const invertedTranslation = [
|
|
-dot(rotationComponent[0], translationComponent),
|
|
-dot(rotationComponent[1], translationComponent)
|
|
];
|
|
return [
|
|
rotationComponent[0].concat(invertedTranslation[0]),
|
|
rotationComponent[1].concat(invertedTranslation[1]),
|
|
[0, 0, 1]
|
|
];
|
|
}
|
|
exports.invertTransformMatrix = invertTransformMatrix;
|
|
function rotatePoint(homogeneousCoordinate, rotationMatrix) {
|
|
return [
|
|
dot(homogeneousCoordinate, rotationMatrix[0]),
|
|
dot(homogeneousCoordinate, rotationMatrix[1])
|
|
];
|
|
}
|
|
exports.rotatePoint = rotatePoint;
|
|
function xyDistanceBetweenPoints(a, b) {
|
|
return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2);
|
|
}
|
|
exports.xyDistanceBetweenPoints = xyDistanceBetweenPoints;
|
|
});
|
|
|
|
// src/facemesh/pipeline.js
|
|
var require_pipeline = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const bounding = require_box2();
|
|
const keypoints = require_keypoints();
|
|
const util = require_util();
|
|
const LANDMARKS_COUNT = 468;
|
|
const UPDATE_REGION_OF_INTEREST_IOU_THRESHOLD = 0.25;
|
|
const MESH_MOUTH_INDEX = 13;
|
|
const MESH_KEYPOINTS_LINE_OF_SYMMETRY_INDICES = [MESH_MOUTH_INDEX, keypoints.MESH_ANNOTATIONS["midwayBetweenEyes"][0]];
|
|
const BLAZEFACE_MOUTH_INDEX = 3;
|
|
const BLAZEFACE_NOSE_INDEX = 2;
|
|
const BLAZEFACE_KEYPOINTS_LINE_OF_SYMMETRY_INDICES = [BLAZEFACE_MOUTH_INDEX, BLAZEFACE_NOSE_INDEX];
|
|
const LEFT_EYE_OUTLINE = keypoints.MESH_ANNOTATIONS["leftEyeLower0"];
|
|
const LEFT_EYE_BOUNDS = [LEFT_EYE_OUTLINE[0], LEFT_EYE_OUTLINE[LEFT_EYE_OUTLINE.length - 1]];
|
|
const RIGHT_EYE_OUTLINE = keypoints.MESH_ANNOTATIONS["rightEyeLower0"];
|
|
const RIGHT_EYE_BOUNDS = [RIGHT_EYE_OUTLINE[0], RIGHT_EYE_OUTLINE[RIGHT_EYE_OUTLINE.length - 1]];
|
|
const IRIS_UPPER_CENTER_INDEX = 3;
|
|
const IRIS_LOWER_CENTER_INDEX = 4;
|
|
const IRIS_IRIS_INDEX = 71;
|
|
const IRIS_NUM_COORDINATES = 76;
|
|
const ENLARGE_EYE_RATIO = 2.3;
|
|
const IRIS_MODEL_INPUT_SIZE = 64;
|
|
const MESH_TO_IRIS_INDICES_MAP = [
|
|
{key: "EyeUpper0", indices: [9, 10, 11, 12, 13, 14, 15]},
|
|
{key: "EyeUpper1", indices: [25, 26, 27, 28, 29, 30, 31]},
|
|
{key: "EyeUpper2", indices: [41, 42, 43, 44, 45, 46, 47]},
|
|
{key: "EyeLower0", indices: [0, 1, 2, 3, 4, 5, 6, 7, 8]},
|
|
{key: "EyeLower1", indices: [16, 17, 18, 19, 20, 21, 22, 23, 24]},
|
|
{key: "EyeLower2", indices: [32, 33, 34, 35, 36, 37, 38, 39, 40]},
|
|
{key: "EyeLower3", indices: [54, 55, 56, 57, 58, 59, 60, 61, 62]},
|
|
{key: "EyebrowUpper", indices: [63, 64, 65, 66, 67, 68, 69, 70]},
|
|
{key: "EyebrowLower", indices: [48, 49, 50, 51, 52, 53]}
|
|
];
|
|
function replaceRawCoordinates(rawCoords, newCoords, prefix, keys) {
|
|
for (let i = 0; i < MESH_TO_IRIS_INDICES_MAP.length; i++) {
|
|
const {key, indices} = MESH_TO_IRIS_INDICES_MAP[i];
|
|
const originalIndices = keypoints.MESH_ANNOTATIONS[`${prefix}${key}`];
|
|
const shouldReplaceAllKeys = keys == null;
|
|
if (shouldReplaceAllKeys || keys.includes(key)) {
|
|
for (let j = 0; j < indices.length; j++) {
|
|
const index = indices[j];
|
|
rawCoords[originalIndices[j]] = [
|
|
newCoords[index][0],
|
|
newCoords[index][1],
|
|
(newCoords[index][2] + rawCoords[originalIndices[j]][2]) / 2
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
class Pipeline {
|
|
constructor(boundingBoxDetector, meshDetector, irisModel, config) {
|
|
this.regionsOfInterest = [];
|
|
this.runsWithoutFaceDetector = 0;
|
|
this.boundingBoxDetector = boundingBoxDetector;
|
|
this.meshDetector = meshDetector;
|
|
this.irisModel = irisModel;
|
|
this.meshWidth = config.mesh.inputSize;
|
|
this.meshHeight = config.mesh.inputSize;
|
|
this.skipFrames = config.detector.skipFrames;
|
|
this.maxFaces = config.detector.maxFaces;
|
|
}
|
|
transformRawCoords(rawCoords, box, angle, rotationMatrix) {
|
|
const boxSize = bounding.getBoxSize({startPoint: box.startPoint, endPoint: box.endPoint});
|
|
const scaleFactor = [boxSize[0] / this.meshWidth, boxSize[1] / this.meshHeight];
|
|
const coordsScaled = rawCoords.map((coord) => [
|
|
scaleFactor[0] * (coord[0] - this.meshWidth / 2),
|
|
scaleFactor[1] * (coord[1] - this.meshHeight / 2),
|
|
coord[2]
|
|
]);
|
|
const coordsRotationMatrix = util.buildRotationMatrix(angle, [0, 0]);
|
|
const coordsRotated = coordsScaled.map((coord) => [...util.rotatePoint(coord, coordsRotationMatrix), coord[2]]);
|
|
const inverseRotationMatrix = util.invertTransformMatrix(rotationMatrix);
|
|
const boxCenter = [...bounding.getBoxCenter({startPoint: box.startPoint, endPoint: box.endPoint}), 1];
|
|
const originalBoxCenter = [
|
|
util.dot(boxCenter, inverseRotationMatrix[0]),
|
|
util.dot(boxCenter, inverseRotationMatrix[1])
|
|
];
|
|
return coordsRotated.map((coord) => [
|
|
coord[0] + originalBoxCenter[0],
|
|
coord[1] + originalBoxCenter[1],
|
|
coord[2]
|
|
]);
|
|
}
|
|
getLeftToRightEyeDepthDifference(rawCoords) {
|
|
const leftEyeZ = rawCoords[LEFT_EYE_BOUNDS[0]][2];
|
|
const rightEyeZ = rawCoords[RIGHT_EYE_BOUNDS[0]][2];
|
|
return leftEyeZ - rightEyeZ;
|
|
}
|
|
getEyeBox(rawCoords, face, eyeInnerCornerIndex, eyeOuterCornerIndex, flip = false) {
|
|
const box = bounding.squarifyBox(bounding.enlargeBox(this.calculateLandmarksBoundingBox([rawCoords[eyeInnerCornerIndex], rawCoords[eyeOuterCornerIndex]]), ENLARGE_EYE_RATIO));
|
|
const boxSize = bounding.getBoxSize(box);
|
|
let crop = tf.image.cropAndResize(face, [[
|
|
box.startPoint[1] / this.meshHeight,
|
|
box.startPoint[0] / this.meshWidth,
|
|
box.endPoint[1] / this.meshHeight,
|
|
box.endPoint[0] / this.meshWidth
|
|
]], [0], [IRIS_MODEL_INPUT_SIZE, IRIS_MODEL_INPUT_SIZE]);
|
|
if (flip) {
|
|
crop = tf.image.flipLeftRight(crop);
|
|
}
|
|
return {box, boxSize, crop};
|
|
}
|
|
getEyeCoords(eyeData, eyeBox, eyeBoxSize, flip = false) {
|
|
const eyeRawCoords = [];
|
|
for (let i = 0; i < IRIS_NUM_COORDINATES; i++) {
|
|
const x = eyeData[i * 3];
|
|
const y = eyeData[i * 3 + 1];
|
|
const z = eyeData[i * 3 + 2];
|
|
eyeRawCoords.push([
|
|
(flip ? 1 - x / IRIS_MODEL_INPUT_SIZE : x / IRIS_MODEL_INPUT_SIZE) * eyeBoxSize[0] + eyeBox.startPoint[0],
|
|
y / IRIS_MODEL_INPUT_SIZE * eyeBoxSize[1] + eyeBox.startPoint[1],
|
|
z
|
|
]);
|
|
}
|
|
return {rawCoords: eyeRawCoords, iris: eyeRawCoords.slice(IRIS_IRIS_INDEX)};
|
|
}
|
|
getAdjustedIrisCoords(rawCoords, irisCoords, direction) {
|
|
const upperCenterZ = rawCoords[keypoints.MESH_ANNOTATIONS[`${direction}EyeUpper0`][IRIS_UPPER_CENTER_INDEX]][2];
|
|
const lowerCenterZ = rawCoords[keypoints.MESH_ANNOTATIONS[`${direction}EyeLower0`][IRIS_LOWER_CENTER_INDEX]][2];
|
|
const averageZ = (upperCenterZ + lowerCenterZ) / 2;
|
|
return irisCoords.map((coord, i) => {
|
|
let z = averageZ;
|
|
if (i === 2) {
|
|
z = upperCenterZ;
|
|
} else if (i === 4) {
|
|
z = lowerCenterZ;
|
|
}
|
|
return [coord[0], coord[1], z];
|
|
});
|
|
}
|
|
async predict(input, predictIrises, predictMesh) {
|
|
if (this.shouldUpdateRegionsOfInterest()) {
|
|
const returnTensors = false;
|
|
const annotateFace = true;
|
|
const {boxes, scaleFactor} = await this.boundingBoxDetector.getBoundingBoxes(input, returnTensors, annotateFace);
|
|
if (boxes.length === 0) {
|
|
this.regionsOfInterest = [];
|
|
return null;
|
|
}
|
|
const scaledBoxes = boxes.map((prediction) => {
|
|
const predictionBoxCPU = {
|
|
startPoint: prediction.box.startPoint.squeeze().arraySync(),
|
|
endPoint: prediction.box.endPoint.squeeze().arraySync()
|
|
};
|
|
const scaledBox = bounding.scaleBoxCoordinates(predictionBoxCPU, scaleFactor);
|
|
const enlargedBox = bounding.enlargeBox(scaledBox);
|
|
return {
|
|
...enlargedBox,
|
|
landmarks: prediction.landmarks.arraySync()
|
|
};
|
|
});
|
|
boxes.forEach((box) => {
|
|
if (box != null && box.startPoint != null) {
|
|
box.startEndTensor.dispose();
|
|
box.startPoint.dispose();
|
|
box.endPoint.dispose();
|
|
}
|
|
});
|
|
this.updateRegionsOfInterest(scaledBoxes);
|
|
this.runsWithoutFaceDetector = 0;
|
|
} else {
|
|
this.runsWithoutFaceDetector++;
|
|
}
|
|
return tf.tidy(() => this.regionsOfInterest.map((box, i) => {
|
|
let angle = 0;
|
|
const boxLandmarksFromMeshModel = box.landmarks.length >= LANDMARKS_COUNT;
|
|
let [indexOfMouth, indexOfForehead] = MESH_KEYPOINTS_LINE_OF_SYMMETRY_INDICES;
|
|
if (boxLandmarksFromMeshModel === false) {
|
|
[indexOfMouth, indexOfForehead] = BLAZEFACE_KEYPOINTS_LINE_OF_SYMMETRY_INDICES;
|
|
}
|
|
angle = util.computeRotation(box.landmarks[indexOfMouth], box.landmarks[indexOfForehead]);
|
|
const faceCenter = bounding.getBoxCenter({startPoint: box.startPoint, endPoint: box.endPoint});
|
|
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
|
let rotatedImage = input;
|
|
let rotationMatrix = util.IDENTITY_MATRIX;
|
|
if (angle !== 0) {
|
|
rotatedImage = tf.image.rotateWithOffset(input, angle, 0, faceCenterNormalized);
|
|
rotationMatrix = util.buildRotationMatrix(-angle, faceCenter);
|
|
}
|
|
const boxCPU = {startPoint: box.startPoint, endPoint: box.endPoint};
|
|
const face = bounding.cutBoxFromImageAndResize(boxCPU, rotatedImage, [this.meshHeight, this.meshWidth]).div(255);
|
|
const [, flag, coords] = this.meshDetector.predict(face);
|
|
const coordsReshaped = tf.reshape(coords, [-1, 3]);
|
|
let rawCoords = coordsReshaped.arraySync();
|
|
if (predictIrises) {
|
|
const {box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop} = this.getEyeBox(rawCoords, face, LEFT_EYE_BOUNDS[0], LEFT_EYE_BOUNDS[1], true);
|
|
const {box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop} = this.getEyeBox(rawCoords, face, RIGHT_EYE_BOUNDS[0], RIGHT_EYE_BOUNDS[1]);
|
|
const eyePredictions = this.irisModel.predict(tf.concat([leftEyeCrop, rightEyeCrop]));
|
|
const eyePredictionsData = eyePredictions.dataSync();
|
|
const leftEyeData = eyePredictionsData.slice(0, IRIS_NUM_COORDINATES * 3);
|
|
const {rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords} = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
|
const rightEyeData = eyePredictionsData.slice(IRIS_NUM_COORDINATES * 3);
|
|
const {rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords} = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
|
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
|
if (Math.abs(leftToRightEyeDepthDifference) < 30) {
|
|
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left");
|
|
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right");
|
|
} else if (leftToRightEyeDepthDifference < 1) {
|
|
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", ["EyeUpper0", "EyeLower0"]);
|
|
} else {
|
|
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", ["EyeUpper0", "EyeLower0"]);
|
|
}
|
|
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, "left");
|
|
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, "right");
|
|
rawCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
|
}
|
|
const transformedCoordsData = this.transformRawCoords(rawCoords, box, angle, rotationMatrix);
|
|
tf.dispose(rawCoords);
|
|
const landmarksBox = bounding.enlargeBox(this.calculateLandmarksBoundingBox(transformedCoordsData));
|
|
if (predictMesh) {
|
|
const transformedCoords = tf.tensor2d(transformedCoordsData);
|
|
this.regionsOfInterest[i] = {...landmarksBox, landmarks: transformedCoords.arraySync()};
|
|
const prediction2 = {
|
|
coords: transformedCoords,
|
|
box: landmarksBox,
|
|
confidence: flag.squeeze(),
|
|
image: face
|
|
};
|
|
return prediction2;
|
|
}
|
|
const prediction = {
|
|
coords: null,
|
|
box: landmarksBox,
|
|
confidence: flag.squeeze(),
|
|
image: face
|
|
};
|
|
return prediction;
|
|
}));
|
|
}
|
|
updateRegionsOfInterest(boxes) {
|
|
for (let i = 0; i < boxes.length; i++) {
|
|
const box = boxes[i];
|
|
const previousBox = this.regionsOfInterest[i];
|
|
let iou = 0;
|
|
if (previousBox && previousBox.startPoint) {
|
|
const [boxStartX, boxStartY] = box.startPoint;
|
|
const [boxEndX, boxEndY] = box.endPoint;
|
|
const [previousBoxStartX, previousBoxStartY] = previousBox.startPoint;
|
|
const [previousBoxEndX, previousBoxEndY] = previousBox.endPoint;
|
|
const xStartMax = Math.max(boxStartX, previousBoxStartX);
|
|
const yStartMax = Math.max(boxStartY, previousBoxStartY);
|
|
const xEndMin = Math.min(boxEndX, previousBoxEndX);
|
|
const yEndMin = Math.min(boxEndY, previousBoxEndY);
|
|
const intersection = (xEndMin - xStartMax) * (yEndMin - yStartMax);
|
|
const boxArea = (boxEndX - boxStartX) * (boxEndY - boxStartY);
|
|
const previousBoxArea = (previousBoxEndX - previousBoxStartX) * (previousBoxEndY - boxStartY);
|
|
iou = intersection / (boxArea + previousBoxArea - intersection);
|
|
}
|
|
if (iou < UPDATE_REGION_OF_INTEREST_IOU_THRESHOLD) {
|
|
this.regionsOfInterest[i] = box;
|
|
}
|
|
}
|
|
this.regionsOfInterest = this.regionsOfInterest.slice(0, boxes.length);
|
|
}
|
|
clearRegionOfInterest(index) {
|
|
if (this.regionsOfInterest[index] != null) {
|
|
this.regionsOfInterest = [
|
|
...this.regionsOfInterest.slice(0, index),
|
|
...this.regionsOfInterest.slice(index + 1)
|
|
];
|
|
}
|
|
}
|
|
shouldUpdateRegionsOfInterest() {
|
|
const roisCount = this.regionsOfInterest.length;
|
|
const noROIs = roisCount === 0;
|
|
if (this.maxFaces === 1 || noROIs) {
|
|
return noROIs;
|
|
}
|
|
return roisCount !== this.maxFaces && this.runsWithoutFaceDetector >= this.skipFrames;
|
|
}
|
|
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};
|
|
}
|
|
}
|
|
exports.Pipeline = Pipeline;
|
|
});
|
|
|
|
// src/facemesh/uvcoords.js
|
|
var require_uvcoords = __commonJS((exports) => {
|
|
exports.UV_COORDS = [
|
|
[0.499976992607117, 0.652534008026123],
|
|
[0.500025987625122, 0.547487020492554],
|
|
[0.499974012374878, 0.602371990680695],
|
|
[0.482113003730774, 0.471979022026062],
|
|
[0.500150978565216, 0.527155995368958],
|
|
[0.499909996986389, 0.498252987861633],
|
|
[0.499523013830185, 0.40106201171875],
|
|
[0.289712011814117, 0.380764007568359],
|
|
[0.499954998493195, 0.312398016452789],
|
|
[0.499987006187439, 0.269918978214264],
|
|
[0.500023007392883, 0.107050001621246],
|
|
[0.500023007392883, 0.666234016418457],
|
|
[0.5000159740448, 0.679224014282227],
|
|
[0.500023007392883, 0.692348003387451],
|
|
[0.499976992607117, 0.695277988910675],
|
|
[0.499976992607117, 0.70593398809433],
|
|
[0.499976992607117, 0.719385027885437],
|
|
[0.499976992607117, 0.737019002437592],
|
|
[0.499967992305756, 0.781370997428894],
|
|
[0.499816000461578, 0.562981009483337],
|
|
[0.473773002624512, 0.573909997940063],
|
|
[0.104906998574734, 0.254140973091125],
|
|
[0.365929991006851, 0.409575998783112],
|
|
[0.338757991790771, 0.41302502155304],
|
|
[0.311120003461838, 0.409460008144379],
|
|
[0.274657994508743, 0.389131009578705],
|
|
[0.393361985683441, 0.403706014156342],
|
|
[0.345234006643295, 0.344011008739471],
|
|
[0.370094001293182, 0.346076011657715],
|
|
[0.319321990013123, 0.347265005111694],
|
|
[0.297903001308441, 0.353591024875641],
|
|
[0.24779200553894, 0.410809993743896],
|
|
[0.396889001131058, 0.842755019664764],
|
|
[0.280097991228104, 0.375599980354309],
|
|
[0.106310002505779, 0.399955987930298],
|
|
[0.2099249958992, 0.391353011131287],
|
|
[0.355807989835739, 0.534406006336212],
|
|
[0.471751004457474, 0.65040397644043],
|
|
[0.474155008792877, 0.680191993713379],
|
|
[0.439785003662109, 0.657229006290436],
|
|
[0.414617002010345, 0.66654098033905],
|
|
[0.450374007225037, 0.680860996246338],
|
|
[0.428770989179611, 0.682690978050232],
|
|
[0.374971002340317, 0.727805018424988],
|
|
[0.486716985702515, 0.547628998756409],
|
|
[0.485300987958908, 0.527395009994507],
|
|
[0.257764995098114, 0.314490020275116],
|
|
[0.401223003864288, 0.455172002315521],
|
|
[0.429818987846375, 0.548614978790283],
|
|
[0.421351999044418, 0.533740997314453],
|
|
[0.276895999908447, 0.532056987285614],
|
|
[0.483370006084442, 0.499586999416351],
|
|
[0.33721199631691, 0.282882988452911],
|
|
[0.296391993761063, 0.293242990970612],
|
|
[0.169294998049736, 0.193813979625702],
|
|
[0.447580009698868, 0.302609980106354],
|
|
[0.392390012741089, 0.353887975215912],
|
|
[0.354490011930466, 0.696784019470215],
|
|
[0.067304998636246, 0.730105042457581],
|
|
[0.442739009857178, 0.572826027870178],
|
|
[0.457098007202148, 0.584792017936707],
|
|
[0.381974011659622, 0.694710969924927],
|
|
[0.392388999462128, 0.694203019142151],
|
|
[0.277076005935669, 0.271932005882263],
|
|
[0.422551989555359, 0.563233017921448],
|
|
[0.385919004678726, 0.281364023685455],
|
|
[0.383103013038635, 0.255840003490448],
|
|
[0.331431001424789, 0.119714021682739],
|
|
[0.229923993349075, 0.232002973556519],
|
|
[0.364500999450684, 0.189113974571228],
|
|
[0.229622006416321, 0.299540996551514],
|
|
[0.173287004232407, 0.278747975826263],
|
|
[0.472878992557526, 0.666198015213013],
|
|
[0.446828007698059, 0.668527007102966],
|
|
[0.422762006521225, 0.673889994621277],
|
|
[0.445307999849319, 0.580065965652466],
|
|
[0.388103008270264, 0.693961024284363],
|
|
[0.403039008378983, 0.706539988517761],
|
|
[0.403629004955292, 0.693953037261963],
|
|
[0.460041999816895, 0.557139039039612],
|
|
[0.431158006191254, 0.692366003990173],
|
|
[0.452181994915009, 0.692366003990173],
|
|
[0.475387006998062, 0.692366003990173],
|
|
[0.465828001499176, 0.779190003871918],
|
|
[0.472328990697861, 0.736225962638855],
|
|
[0.473087012767792, 0.717857003211975],
|
|
[0.473122000694275, 0.704625964164734],
|
|
[0.473033010959625, 0.695277988910675],
|
|
[0.427942007780075, 0.695277988910675],
|
|
[0.426479011774063, 0.703539967536926],
|
|
[0.423162013292313, 0.711845993995667],
|
|
[0.4183090031147, 0.720062971115112],
|
|
[0.390094995498657, 0.639572978019714],
|
|
[0.013953999616206, 0.560034036636353],
|
|
[0.499913990497589, 0.58014702796936],
|
|
[0.413199990987778, 0.69539999961853],
|
|
[0.409626007080078, 0.701822996139526],
|
|
[0.468080013990402, 0.601534962654114],
|
|
[0.422728985548019, 0.585985004901886],
|
|
[0.463079988956451, 0.593783974647522],
|
|
[0.37211999297142, 0.47341400384903],
|
|
[0.334562003612518, 0.496073007583618],
|
|
[0.411671012639999, 0.546965003013611],
|
|
[0.242175996303558, 0.14767599105835],
|
|
[0.290776997804642, 0.201445996761322],
|
|
[0.327338010072708, 0.256527006626129],
|
|
[0.399509996175766, 0.748921036720276],
|
|
[0.441727995872498, 0.261676013469696],
|
|
[0.429764986038208, 0.187834024429321],
|
|
[0.412198007106781, 0.108901023864746],
|
|
[0.288955003023148, 0.398952007293701],
|
|
[0.218936994671822, 0.435410976409912],
|
|
[0.41278201341629, 0.398970007896423],
|
|
[0.257135003805161, 0.355440020561218],
|
|
[0.427684992551804, 0.437960982322693],
|
|
[0.448339998722076, 0.536936044692993],
|
|
[0.178560003638268, 0.45755398273468],
|
|
[0.247308000922203, 0.457193970680237],
|
|
[0.286267012357712, 0.467674970626831],
|
|
[0.332827985286713, 0.460712015628815],
|
|
[0.368755996227264, 0.447206974029541],
|
|
[0.398963987827301, 0.432654976844788],
|
|
[0.476410001516342, 0.405806005001068],
|
|
[0.189241006970406, 0.523923993110657],
|
|
[0.228962004184723, 0.348950982093811],
|
|
[0.490725994110107, 0.562400996685028],
|
|
[0.404670000076294, 0.485132992267609],
|
|
[0.019469000399113, 0.401564002037048],
|
|
[0.426243007183075, 0.420431017875671],
|
|
[0.396993011236191, 0.548797011375427],
|
|
[0.266469985246658, 0.376977026462555],
|
|
[0.439121007919312, 0.51895797252655],
|
|
[0.032313998788595, 0.644356966018677],
|
|
[0.419054001569748, 0.387154996395111],
|
|
[0.462783008813858, 0.505746960639954],
|
|
[0.238978996872902, 0.779744982719421],
|
|
[0.198220998048782, 0.831938028335571],
|
|
[0.107550002634525, 0.540755033493042],
|
|
[0.183610007166862, 0.740257024765015],
|
|
[0.134409993886948, 0.333683013916016],
|
|
[0.385764002799988, 0.883153975009918],
|
|
[0.490967005491257, 0.579378008842468],
|
|
[0.382384985685349, 0.508572995662689],
|
|
[0.174399003386497, 0.397670984268188],
|
|
[0.318785011768341, 0.39623498916626],
|
|
[0.343364000320435, 0.400596976280212],
|
|
[0.396100014448166, 0.710216999053955],
|
|
[0.187885001301765, 0.588537991046906],
|
|
[0.430987000465393, 0.944064974784851],
|
|
[0.318993002176285, 0.898285031318665],
|
|
[0.266247987747192, 0.869701027870178],
|
|
[0.500023007392883, 0.190576016902924],
|
|
[0.499976992607117, 0.954452991485596],
|
|
[0.366169989109039, 0.398822009563446],
|
|
[0.393207013607025, 0.39553701877594],
|
|
[0.410373002290726, 0.391080021858215],
|
|
[0.194993004202843, 0.342101991176605],
|
|
[0.388664990663528, 0.362284004688263],
|
|
[0.365961998701096, 0.355970978736877],
|
|
[0.343364000320435, 0.355356991291046],
|
|
[0.318785011768341, 0.35834002494812],
|
|
[0.301414996385574, 0.363156020641327],
|
|
[0.058132998645306, 0.319076001644135],
|
|
[0.301414996385574, 0.387449026107788],
|
|
[0.499987989664078, 0.618434011936188],
|
|
[0.415838003158569, 0.624195992946625],
|
|
[0.445681989192963, 0.566076993942261],
|
|
[0.465844005346298, 0.620640993118286],
|
|
[0.49992299079895, 0.351523995399475],
|
|
[0.288718998432159, 0.819945991039276],
|
|
[0.335278987884521, 0.852819979190826],
|
|
[0.440512001514435, 0.902418971061707],
|
|
[0.128294005990028, 0.791940987110138],
|
|
[0.408771991729736, 0.373893976211548],
|
|
[0.455606997013092, 0.451801002025604],
|
|
[0.499877005815506, 0.908990025520325],
|
|
[0.375436991453171, 0.924192011356354],
|
|
[0.11421000212431, 0.615022003650665],
|
|
[0.448662012815475, 0.695277988910675],
|
|
[0.4480200111866, 0.704632043838501],
|
|
[0.447111994028091, 0.715808033943176],
|
|
[0.444831997156143, 0.730794012546539],
|
|
[0.430011987686157, 0.766808986663818],
|
|
[0.406787008047104, 0.685672998428345],
|
|
[0.400738000869751, 0.681069016456604],
|
|
[0.392399996519089, 0.677703022956848],
|
|
[0.367855995893478, 0.663918972015381],
|
|
[0.247923001646996, 0.601333022117615],
|
|
[0.452769994735718, 0.420849978923798],
|
|
[0.43639200925827, 0.359887003898621],
|
|
[0.416164010763168, 0.368713974952698],
|
|
[0.413385987281799, 0.692366003990173],
|
|
[0.228018000721931, 0.683571994304657],
|
|
[0.468268007040024, 0.352671027183533],
|
|
[0.411361992359161, 0.804327011108398],
|
|
[0.499989002943039, 0.469825029373169],
|
|
[0.479153990745544, 0.442654013633728],
|
|
[0.499974012374878, 0.439637005329132],
|
|
[0.432112008333206, 0.493588984012604],
|
|
[0.499886006116867, 0.866917014122009],
|
|
[0.49991300702095, 0.821729004383087],
|
|
[0.456548988819122, 0.819200992584229],
|
|
[0.344549000263214, 0.745438992977142],
|
|
[0.37890899181366, 0.574010014533997],
|
|
[0.374292999505997, 0.780184984207153],
|
|
[0.319687992334366, 0.570737957954407],
|
|
[0.357154995203018, 0.604269981384277],
|
|
[0.295284003019333, 0.621580958366394],
|
|
[0.447750002145767, 0.862477004528046],
|
|
[0.410986006259918, 0.508723020553589],
|
|
[0.31395098567009, 0.775308012962341],
|
|
[0.354128003120422, 0.812552988529205],
|
|
[0.324548006057739, 0.703992962837219],
|
|
[0.189096003770828, 0.646299958229065],
|
|
[0.279776990413666, 0.71465802192688],
|
|
[0.1338230073452, 0.682700991630554],
|
|
[0.336768001317978, 0.644733011722565],
|
|
[0.429883986711502, 0.466521978378296],
|
|
[0.455527991056442, 0.548622965812683],
|
|
[0.437114000320435, 0.558896005153656],
|
|
[0.467287987470627, 0.529924988746643],
|
|
[0.414712011814117, 0.335219979286194],
|
|
[0.37704598903656, 0.322777986526489],
|
|
[0.344107985496521, 0.320150971412659],
|
|
[0.312875986099243, 0.32233202457428],
|
|
[0.283526003360748, 0.333190023899078],
|
|
[0.241245999932289, 0.382785975933075],
|
|
[0.102986000478268, 0.468762993812561],
|
|
[0.267612010240555, 0.424560010433197],
|
|
[0.297879010438919, 0.433175981044769],
|
|
[0.333433985710144, 0.433878004550934],
|
|
[0.366427004337311, 0.426115989685059],
|
|
[0.396012008190155, 0.416696012020111],
|
|
[0.420121014118195, 0.41022801399231],
|
|
[0.007561000064015, 0.480777025222778],
|
|
[0.432949006557465, 0.569517970085144],
|
|
[0.458638995885849, 0.479089021682739],
|
|
[0.473466008901596, 0.545744001865387],
|
|
[0.476087987422943, 0.563830018043518],
|
|
[0.468472003936768, 0.555056989192963],
|
|
[0.433990985155106, 0.582361996173859],
|
|
[0.483518004417419, 0.562983989715576],
|
|
[0.482482999563217, 0.57784903049469],
|
|
[0.42645001411438, 0.389798998832703],
|
|
[0.438998997211456, 0.39649498462677],
|
|
[0.450067013502121, 0.400434017181396],
|
|
[0.289712011814117, 0.368252992630005],
|
|
[0.276670008897781, 0.363372981548309],
|
|
[0.517862021923065, 0.471948027610779],
|
|
[0.710287988185883, 0.380764007568359],
|
|
[0.526226997375488, 0.573909997940063],
|
|
[0.895093023777008, 0.254140973091125],
|
|
[0.634069979190826, 0.409575998783112],
|
|
[0.661242008209229, 0.41302502155304],
|
|
[0.688880026340485, 0.409460008144379],
|
|
[0.725341975688934, 0.389131009578705],
|
|
[0.606630027294159, 0.40370500087738],
|
|
[0.654766023159027, 0.344011008739471],
|
|
[0.629905998706818, 0.346076011657715],
|
|
[0.680678009986877, 0.347265005111694],
|
|
[0.702096998691559, 0.353591024875641],
|
|
[0.75221198797226, 0.410804986953735],
|
|
[0.602918028831482, 0.842862963676453],
|
|
[0.719901978969574, 0.375599980354309],
|
|
[0.893692970275879, 0.399959981441498],
|
|
[0.790081977844238, 0.391354024410248],
|
|
[0.643998026847839, 0.534487962722778],
|
|
[0.528249025344849, 0.65040397644043],
|
|
[0.525849997997284, 0.680191040039062],
|
|
[0.560214996337891, 0.657229006290436],
|
|
[0.585384011268616, 0.66654098033905],
|
|
[0.549625992774963, 0.680860996246338],
|
|
[0.57122802734375, 0.682691991329193],
|
|
[0.624852001667023, 0.72809898853302],
|
|
[0.513050019741058, 0.547281980514526],
|
|
[0.51509702205658, 0.527251958847046],
|
|
[0.742246985435486, 0.314507007598877],
|
|
[0.598631024360657, 0.454979002475739],
|
|
[0.570338010787964, 0.548575043678284],
|
|
[0.578631997108459, 0.533622980117798],
|
|
[0.723087012767792, 0.532054007053375],
|
|
[0.516445994377136, 0.499638974666595],
|
|
[0.662801027297974, 0.282917976379395],
|
|
[0.70362401008606, 0.293271005153656],
|
|
[0.830704987049103, 0.193813979625702],
|
|
[0.552385985851288, 0.302568018436432],
|
|
[0.607609987258911, 0.353887975215912],
|
|
[0.645429015159607, 0.696707010269165],
|
|
[0.932694971561432, 0.730105042457581],
|
|
[0.557260990142822, 0.572826027870178],
|
|
[0.542901992797852, 0.584792017936707],
|
|
[0.6180260181427, 0.694710969924927],
|
|
[0.607590973377228, 0.694203019142151],
|
|
[0.722943007946014, 0.271963000297546],
|
|
[0.577413976192474, 0.563166975975037],
|
|
[0.614082992076874, 0.281386971473694],
|
|
[0.616907000541687, 0.255886018276215],
|
|
[0.668509006500244, 0.119913995265961],
|
|
[0.770092010498047, 0.232020974159241],
|
|
[0.635536015033722, 0.189248979091644],
|
|
[0.77039098739624, 0.299556016921997],
|
|
[0.826722025871277, 0.278755009174347],
|
|
[0.527121007442474, 0.666198015213013],
|
|
[0.553171992301941, 0.668527007102966],
|
|
[0.577238023281097, 0.673889994621277],
|
|
[0.554691970348358, 0.580065965652466],
|
|
[0.611896991729736, 0.693961024284363],
|
|
[0.59696102142334, 0.706539988517761],
|
|
[0.596370995044708, 0.693953037261963],
|
|
[0.539958000183105, 0.557139039039612],
|
|
[0.568841993808746, 0.692366003990173],
|
|
[0.547818005084991, 0.692366003990173],
|
|
[0.52461302280426, 0.692366003990173],
|
|
[0.534089982509613, 0.779141008853912],
|
|
[0.527670979499817, 0.736225962638855],
|
|
[0.526912987232208, 0.717857003211975],
|
|
[0.526877999305725, 0.704625964164734],
|
|
[0.526966989040375, 0.695277988910675],
|
|
[0.572058022022247, 0.695277988910675],
|
|
[0.573521018028259, 0.703539967536926],
|
|
[0.57683801651001, 0.711845993995667],
|
|
[0.581691026687622, 0.720062971115112],
|
|
[0.609944999217987, 0.639909982681274],
|
|
[0.986046016216278, 0.560034036636353],
|
|
[0.5867999792099, 0.69539999961853],
|
|
[0.590372025966644, 0.701822996139526],
|
|
[0.531915009021759, 0.601536989212036],
|
|
[0.577268004417419, 0.585934996604919],
|
|
[0.536915004253387, 0.593786001205444],
|
|
[0.627542972564697, 0.473352015018463],
|
|
[0.665585994720459, 0.495950996875763],
|
|
[0.588353991508484, 0.546862006187439],
|
|
[0.757824003696442, 0.14767599105835],
|
|
[0.709249973297119, 0.201507985591888],
|
|
[0.672684013843536, 0.256581008434296],
|
|
[0.600408971309662, 0.74900496006012],
|
|
[0.55826598405838, 0.261672019958496],
|
|
[0.570303976535797, 0.187870979309082],
|
|
[0.588165998458862, 0.109044015407562],
|
|
[0.711045026779175, 0.398952007293701],
|
|
[0.781069993972778, 0.435405015945435],
|
|
[0.587247014045715, 0.398931980133057],
|
|
[0.742869973182678, 0.355445981025696],
|
|
[0.572156012058258, 0.437651991844177],
|
|
[0.55186802148819, 0.536570012569427],
|
|
[0.821442008018494, 0.457556009292603],
|
|
[0.752701997756958, 0.457181990146637],
|
|
[0.71375697851181, 0.467626988887787],
|
|
[0.66711300611496, 0.460672974586487],
|
|
[0.631101012229919, 0.447153985500336],
|
|
[0.6008620262146, 0.432473003864288],
|
|
[0.523481011390686, 0.405627012252808],
|
|
[0.810747981071472, 0.523926019668579],
|
|
[0.771045982837677, 0.348959028720856],
|
|
[0.509127020835876, 0.562718033790588],
|
|
[0.595292985439301, 0.485023975372314],
|
|
[0.980530977249146, 0.401564002037048],
|
|
[0.573499977588654, 0.420000016689301],
|
|
[0.602994978427887, 0.548687994480133],
|
|
[0.733529984951019, 0.376977026462555],
|
|
[0.560611009597778, 0.519016981124878],
|
|
[0.967685997486115, 0.644356966018677],
|
|
[0.580985009670258, 0.387160003185272],
|
|
[0.537728011608124, 0.505385041236877],
|
|
[0.760966002941132, 0.779752969741821],
|
|
[0.801778972148895, 0.831938028335571],
|
|
[0.892440974712372, 0.54076099395752],
|
|
[0.816350996494293, 0.740260004997253],
|
|
[0.865594983100891, 0.333687007427216],
|
|
[0.614073991775513, 0.883246004581451],
|
|
[0.508952975273132, 0.579437971115112],
|
|
[0.617941975593567, 0.508316040039062],
|
|
[0.825608015060425, 0.397674977779388],
|
|
[0.681214988231659, 0.39623498916626],
|
|
[0.656635999679565, 0.400596976280212],
|
|
[0.603900015354156, 0.710216999053955],
|
|
[0.81208598613739, 0.588539004325867],
|
|
[0.56801301240921, 0.944564998149872],
|
|
[0.681007981300354, 0.898285031318665],
|
|
[0.733752012252808, 0.869701027870178],
|
|
[0.633830010890961, 0.398822009563446],
|
|
[0.606792986392975, 0.39553701877594],
|
|
[0.589659988880157, 0.391062021255493],
|
|
[0.805015981197357, 0.342108011245728],
|
|
[0.611334979534149, 0.362284004688263],
|
|
[0.634037971496582, 0.355970978736877],
|
|
[0.656635999679565, 0.355356991291046],
|
|
[0.681214988231659, 0.35834002494812],
|
|
[0.698584973812103, 0.363156020641327],
|
|
[0.941866993904114, 0.319076001644135],
|
|
[0.698584973812103, 0.387449026107788],
|
|
[0.584177017211914, 0.624107003211975],
|
|
[0.554318010807037, 0.566076993942261],
|
|
[0.534153997898102, 0.62064003944397],
|
|
[0.711217999458313, 0.819975018501282],
|
|
[0.664629995822906, 0.852871000766754],
|
|
[0.559099972248077, 0.902631998062134],
|
|
[0.871706008911133, 0.791940987110138],
|
|
[0.591234028339386, 0.373893976211548],
|
|
[0.544341027736664, 0.451583981513977],
|
|
[0.624562978744507, 0.924192011356354],
|
|
[0.88577002286911, 0.615028977394104],
|
|
[0.551338016986847, 0.695277988910675],
|
|
[0.551980018615723, 0.704632043838501],
|
|
[0.552887976169586, 0.715808033943176],
|
|
[0.555167973041534, 0.730794012546539],
|
|
[0.569944024085999, 0.767035007476807],
|
|
[0.593203008174896, 0.685675978660583],
|
|
[0.599261999130249, 0.681069016456604],
|
|
[0.607599973678589, 0.677703022956848],
|
|
[0.631937980651855, 0.663500010967255],
|
|
[0.752032995223999, 0.601315021514893],
|
|
[0.547226011753082, 0.420395016670227],
|
|
[0.563543975353241, 0.359827995300293],
|
|
[0.583841025829315, 0.368713974952698],
|
|
[0.586614012718201, 0.692366003990173],
|
|
[0.771915018558502, 0.683578014373779],
|
|
[0.531597018241882, 0.352482974529266],
|
|
[0.588370978832245, 0.804440975189209],
|
|
[0.52079701423645, 0.442565023899078],
|
|
[0.567984998226166, 0.493479013442993],
|
|
[0.543282985687256, 0.819254994392395],
|
|
[0.655317008495331, 0.745514988899231],
|
|
[0.621008992195129, 0.574018001556396],
|
|
[0.625559985637665, 0.78031200170517],
|
|
[0.680198013782501, 0.570719003677368],
|
|
[0.64276397228241, 0.604337990283966],
|
|
[0.704662978649139, 0.621529996395111],
|
|
[0.552012026309967, 0.862591981887817],
|
|
[0.589071989059448, 0.508637011051178],
|
|
[0.685944974422455, 0.775357007980347],
|
|
[0.645735025405884, 0.812640011310577],
|
|
[0.675342977046967, 0.703978002071381],
|
|
[0.810858011245728, 0.646304965019226],
|
|
[0.72012197971344, 0.714666962623596],
|
|
[0.866151988506317, 0.682704985141754],
|
|
[0.663187026977539, 0.644596993923187],
|
|
[0.570082008838654, 0.466325998306274],
|
|
[0.544561982154846, 0.548375964164734],
|
|
[0.562758982181549, 0.558784961700439],
|
|
[0.531987011432648, 0.530140042304993],
|
|
[0.585271000862122, 0.335177004337311],
|
|
[0.622952997684479, 0.32277899980545],
|
|
[0.655896008014679, 0.320163011550903],
|
|
[0.687132000923157, 0.322345972061157],
|
|
[0.716481983661652, 0.333200991153717],
|
|
[0.758756995201111, 0.382786989212036],
|
|
[0.897013008594513, 0.468769013881683],
|
|
[0.732392013072968, 0.424547016620636],
|
|
[0.70211398601532, 0.433162987232208],
|
|
[0.66652500629425, 0.433866024017334],
|
|
[0.633504986763, 0.426087975502014],
|
|
[0.603875994682312, 0.416586995124817],
|
|
[0.579657971858978, 0.409945011138916],
|
|
[0.992439985275269, 0.480777025222778],
|
|
[0.567192018032074, 0.569419980049133],
|
|
[0.54136598110199, 0.478899002075195],
|
|
[0.526564002037048, 0.546118021011353],
|
|
[0.523913025856018, 0.563830018043518],
|
|
[0.531529009342194, 0.555056989192963],
|
|
[0.566035985946655, 0.582329034805298],
|
|
[0.51631098985672, 0.563053965568542],
|
|
[0.5174720287323, 0.577877044677734],
|
|
[0.573594987392426, 0.389806985855103],
|
|
[0.560697972774506, 0.395331978797913],
|
|
[0.549755990505219, 0.399751007556915],
|
|
[0.710287988185883, 0.368252992630005],
|
|
[0.723330020904541, 0.363372981548309]
|
|
];
|
|
});
|
|
|
|
// src/facemesh/triangulation.js
|
|
var require_triangulation = __commonJS((exports) => {
|
|
__export(exports, {
|
|
default: () => triangulation_default
|
|
});
|
|
var triangulation_default = [
|
|
127,
|
|
34,
|
|
139,
|
|
11,
|
|
0,
|
|
37,
|
|
232,
|
|
231,
|
|
120,
|
|
72,
|
|
37,
|
|
39,
|
|
128,
|
|
121,
|
|
47,
|
|
232,
|
|
121,
|
|
128,
|
|
104,
|
|
69,
|
|
67,
|
|
175,
|
|
171,
|
|
148,
|
|
157,
|
|
154,
|
|
155,
|
|
118,
|
|
50,
|
|
101,
|
|
73,
|
|
39,
|
|
40,
|
|
9,
|
|
151,
|
|
108,
|
|
48,
|
|
115,
|
|
131,
|
|
194,
|
|
204,
|
|
211,
|
|
74,
|
|
40,
|
|
185,
|
|
80,
|
|
42,
|
|
183,
|
|
40,
|
|
92,
|
|
186,
|
|
230,
|
|
229,
|
|
118,
|
|
202,
|
|
212,
|
|
214,
|
|
83,
|
|
18,
|
|
17,
|
|
76,
|
|
61,
|
|
146,
|
|
160,
|
|
29,
|
|
30,
|
|
56,
|
|
157,
|
|
173,
|
|
106,
|
|
204,
|
|
194,
|
|
135,
|
|
214,
|
|
192,
|
|
203,
|
|
165,
|
|
98,
|
|
21,
|
|
71,
|
|
68,
|
|
51,
|
|
45,
|
|
4,
|
|
144,
|
|
24,
|
|
23,
|
|
77,
|
|
146,
|
|
91,
|
|
205,
|
|
50,
|
|
187,
|
|
201,
|
|
200,
|
|
18,
|
|
91,
|
|
106,
|
|
182,
|
|
90,
|
|
91,
|
|
181,
|
|
85,
|
|
84,
|
|
17,
|
|
206,
|
|
203,
|
|
36,
|
|
148,
|
|
171,
|
|
140,
|
|
92,
|
|
40,
|
|
39,
|
|
193,
|
|
189,
|
|
244,
|
|
159,
|
|
158,
|
|
28,
|
|
247,
|
|
246,
|
|
161,
|
|
236,
|
|
3,
|
|
196,
|
|
54,
|
|
68,
|
|
104,
|
|
193,
|
|
168,
|
|
8,
|
|
117,
|
|
228,
|
|
31,
|
|
189,
|
|
193,
|
|
55,
|
|
98,
|
|
97,
|
|
99,
|
|
126,
|
|
47,
|
|
100,
|
|
166,
|
|
79,
|
|
218,
|
|
155,
|
|
154,
|
|
26,
|
|
209,
|
|
49,
|
|
131,
|
|
135,
|
|
136,
|
|
150,
|
|
47,
|
|
126,
|
|
217,
|
|
223,
|
|
52,
|
|
53,
|
|
45,
|
|
51,
|
|
134,
|
|
211,
|
|
170,
|
|
140,
|
|
67,
|
|
69,
|
|
108,
|
|
43,
|
|
106,
|
|
91,
|
|
230,
|
|
119,
|
|
120,
|
|
226,
|
|
130,
|
|
247,
|
|
63,
|
|
53,
|
|
52,
|
|
238,
|
|
20,
|
|
242,
|
|
46,
|
|
70,
|
|
156,
|
|
78,
|
|
62,
|
|
96,
|
|
46,
|
|
53,
|
|
63,
|
|
143,
|
|
34,
|
|
227,
|
|
173,
|
|
155,
|
|
133,
|
|
123,
|
|
117,
|
|
111,
|
|
44,
|
|
125,
|
|
19,
|
|
236,
|
|
134,
|
|
51,
|
|
216,
|
|
206,
|
|
205,
|
|
154,
|
|
153,
|
|
22,
|
|
39,
|
|
37,
|
|
167,
|
|
200,
|
|
201,
|
|
208,
|
|
36,
|
|
142,
|
|
100,
|
|
57,
|
|
212,
|
|
202,
|
|
20,
|
|
60,
|
|
99,
|
|
28,
|
|
158,
|
|
157,
|
|
35,
|
|
226,
|
|
113,
|
|
160,
|
|
159,
|
|
27,
|
|
204,
|
|
202,
|
|
210,
|
|
113,
|
|
225,
|
|
46,
|
|
43,
|
|
202,
|
|
204,
|
|
62,
|
|
76,
|
|
77,
|
|
137,
|
|
123,
|
|
116,
|
|
41,
|
|
38,
|
|
72,
|
|
203,
|
|
129,
|
|
142,
|
|
64,
|
|
98,
|
|
240,
|
|
49,
|
|
102,
|
|
64,
|
|
41,
|
|
73,
|
|
74,
|
|
212,
|
|
216,
|
|
207,
|
|
42,
|
|
74,
|
|
184,
|
|
169,
|
|
170,
|
|
211,
|
|
170,
|
|
149,
|
|
176,
|
|
105,
|
|
66,
|
|
69,
|
|
122,
|
|
6,
|
|
168,
|
|
123,
|
|
147,
|
|
187,
|
|
96,
|
|
77,
|
|
90,
|
|
65,
|
|
55,
|
|
107,
|
|
89,
|
|
90,
|
|
180,
|
|
101,
|
|
100,
|
|
120,
|
|
63,
|
|
105,
|
|
104,
|
|
93,
|
|
137,
|
|
227,
|
|
15,
|
|
86,
|
|
85,
|
|
129,
|
|
102,
|
|
49,
|
|
14,
|
|
87,
|
|
86,
|
|
55,
|
|
8,
|
|
9,
|
|
100,
|
|
47,
|
|
121,
|
|
145,
|
|
23,
|
|
22,
|
|
88,
|
|
89,
|
|
179,
|
|
6,
|
|
122,
|
|
196,
|
|
88,
|
|
95,
|
|
96,
|
|
138,
|
|
172,
|
|
136,
|
|
215,
|
|
58,
|
|
172,
|
|
115,
|
|
48,
|
|
219,
|
|
42,
|
|
80,
|
|
81,
|
|
195,
|
|
3,
|
|
51,
|
|
43,
|
|
146,
|
|
61,
|
|
171,
|
|
175,
|
|
199,
|
|
81,
|
|
82,
|
|
38,
|
|
53,
|
|
46,
|
|
225,
|
|
144,
|
|
163,
|
|
110,
|
|
246,
|
|
33,
|
|
7,
|
|
52,
|
|
65,
|
|
66,
|
|
229,
|
|
228,
|
|
117,
|
|
34,
|
|
127,
|
|
234,
|
|
107,
|
|
108,
|
|
69,
|
|
109,
|
|
108,
|
|
151,
|
|
48,
|
|
64,
|
|
235,
|
|
62,
|
|
78,
|
|
191,
|
|
129,
|
|
209,
|
|
126,
|
|
111,
|
|
35,
|
|
143,
|
|
163,
|
|
161,
|
|
246,
|
|
117,
|
|
123,
|
|
50,
|
|
222,
|
|
65,
|
|
52,
|
|
19,
|
|
125,
|
|
141,
|
|
221,
|
|
55,
|
|
65,
|
|
3,
|
|
195,
|
|
197,
|
|
25,
|
|
7,
|
|
33,
|
|
220,
|
|
237,
|
|
44,
|
|
70,
|
|
71,
|
|
139,
|
|
122,
|
|
193,
|
|
245,
|
|
247,
|
|
130,
|
|
33,
|
|
71,
|
|
21,
|
|
162,
|
|
153,
|
|
158,
|
|
159,
|
|
170,
|
|
169,
|
|
150,
|
|
188,
|
|
174,
|
|
196,
|
|
216,
|
|
186,
|
|
92,
|
|
144,
|
|
160,
|
|
161,
|
|
2,
|
|
97,
|
|
167,
|
|
141,
|
|
125,
|
|
241,
|
|
164,
|
|
167,
|
|
37,
|
|
72,
|
|
38,
|
|
12,
|
|
145,
|
|
159,
|
|
160,
|
|
38,
|
|
82,
|
|
13,
|
|
63,
|
|
68,
|
|
71,
|
|
226,
|
|
35,
|
|
111,
|
|
158,
|
|
153,
|
|
154,
|
|
101,
|
|
50,
|
|
205,
|
|
206,
|
|
92,
|
|
165,
|
|
209,
|
|
198,
|
|
217,
|
|
165,
|
|
167,
|
|
97,
|
|
220,
|
|
115,
|
|
218,
|
|
133,
|
|
112,
|
|
243,
|
|
239,
|
|
238,
|
|
241,
|
|
214,
|
|
135,
|
|
169,
|
|
190,
|
|
173,
|
|
133,
|
|
171,
|
|
208,
|
|
32,
|
|
125,
|
|
44,
|
|
237,
|
|
86,
|
|
87,
|
|
178,
|
|
85,
|
|
86,
|
|
179,
|
|
84,
|
|
85,
|
|
180,
|
|
83,
|
|
84,
|
|
181,
|
|
201,
|
|
83,
|
|
182,
|
|
137,
|
|
93,
|
|
132,
|
|
76,
|
|
62,
|
|
183,
|
|
61,
|
|
76,
|
|
184,
|
|
57,
|
|
61,
|
|
185,
|
|
212,
|
|
57,
|
|
186,
|
|
214,
|
|
207,
|
|
187,
|
|
34,
|
|
143,
|
|
156,
|
|
79,
|
|
239,
|
|
237,
|
|
123,
|
|
137,
|
|
177,
|
|
44,
|
|
1,
|
|
4,
|
|
201,
|
|
194,
|
|
32,
|
|
64,
|
|
102,
|
|
129,
|
|
213,
|
|
215,
|
|
138,
|
|
59,
|
|
166,
|
|
219,
|
|
242,
|
|
99,
|
|
97,
|
|
2,
|
|
94,
|
|
141,
|
|
75,
|
|
59,
|
|
235,
|
|
24,
|
|
110,
|
|
228,
|
|
25,
|
|
130,
|
|
226,
|
|
23,
|
|
24,
|
|
229,
|
|
22,
|
|
23,
|
|
230,
|
|
26,
|
|
22,
|
|
231,
|
|
112,
|
|
26,
|
|
232,
|
|
189,
|
|
190,
|
|
243,
|
|
221,
|
|
56,
|
|
190,
|
|
28,
|
|
56,
|
|
221,
|
|
27,
|
|
28,
|
|
222,
|
|
29,
|
|
27,
|
|
223,
|
|
30,
|
|
29,
|
|
224,
|
|
247,
|
|
30,
|
|
225,
|
|
238,
|
|
79,
|
|
20,
|
|
166,
|
|
59,
|
|
75,
|
|
60,
|
|
75,
|
|
240,
|
|
147,
|
|
177,
|
|
215,
|
|
20,
|
|
79,
|
|
166,
|
|
187,
|
|
147,
|
|
213,
|
|
112,
|
|
233,
|
|
244,
|
|
233,
|
|
128,
|
|
245,
|
|
128,
|
|
114,
|
|
188,
|
|
114,
|
|
217,
|
|
174,
|
|
131,
|
|
115,
|
|
220,
|
|
217,
|
|
198,
|
|
236,
|
|
198,
|
|
131,
|
|
134,
|
|
177,
|
|
132,
|
|
58,
|
|
143,
|
|
35,
|
|
124,
|
|
110,
|
|
163,
|
|
7,
|
|
228,
|
|
110,
|
|
25,
|
|
356,
|
|
389,
|
|
368,
|
|
11,
|
|
302,
|
|
267,
|
|
452,
|
|
350,
|
|
349,
|
|
302,
|
|
303,
|
|
269,
|
|
357,
|
|
343,
|
|
277,
|
|
452,
|
|
453,
|
|
357,
|
|
333,
|
|
332,
|
|
297,
|
|
175,
|
|
152,
|
|
377,
|
|
384,
|
|
398,
|
|
382,
|
|
347,
|
|
348,
|
|
330,
|
|
303,
|
|
304,
|
|
270,
|
|
9,
|
|
336,
|
|
337,
|
|
278,
|
|
279,
|
|
360,
|
|
418,
|
|
262,
|
|
431,
|
|
304,
|
|
408,
|
|
409,
|
|
310,
|
|
415,
|
|
407,
|
|
270,
|
|
409,
|
|
410,
|
|
450,
|
|
348,
|
|
347,
|
|
422,
|
|
430,
|
|
434,
|
|
313,
|
|
314,
|
|
17,
|
|
306,
|
|
307,
|
|
375,
|
|
387,
|
|
388,
|
|
260,
|
|
286,
|
|
414,
|
|
398,
|
|
335,
|
|
406,
|
|
418,
|
|
364,
|
|
367,
|
|
416,
|
|
423,
|
|
358,
|
|
327,
|
|
251,
|
|
284,
|
|
298,
|
|
281,
|
|
5,
|
|
4,
|
|
373,
|
|
374,
|
|
253,
|
|
307,
|
|
320,
|
|
321,
|
|
425,
|
|
427,
|
|
411,
|
|
421,
|
|
313,
|
|
18,
|
|
321,
|
|
405,
|
|
406,
|
|
320,
|
|
404,
|
|
405,
|
|
315,
|
|
16,
|
|
17,
|
|
426,
|
|
425,
|
|
266,
|
|
377,
|
|
400,
|
|
369,
|
|
322,
|
|
391,
|
|
269,
|
|
417,
|
|
465,
|
|
464,
|
|
386,
|
|
257,
|
|
258,
|
|
466,
|
|
260,
|
|
388,
|
|
456,
|
|
399,
|
|
419,
|
|
284,
|
|
332,
|
|
333,
|
|
417,
|
|
285,
|
|
8,
|
|
346,
|
|
340,
|
|
261,
|
|
413,
|
|
441,
|
|
285,
|
|
327,
|
|
460,
|
|
328,
|
|
355,
|
|
371,
|
|
329,
|
|
392,
|
|
439,
|
|
438,
|
|
382,
|
|
341,
|
|
256,
|
|
429,
|
|
420,
|
|
360,
|
|
364,
|
|
394,
|
|
379,
|
|
277,
|
|
343,
|
|
437,
|
|
443,
|
|
444,
|
|
283,
|
|
275,
|
|
440,
|
|
363,
|
|
431,
|
|
262,
|
|
369,
|
|
297,
|
|
338,
|
|
337,
|
|
273,
|
|
375,
|
|
321,
|
|
450,
|
|
451,
|
|
349,
|
|
446,
|
|
342,
|
|
467,
|
|
293,
|
|
334,
|
|
282,
|
|
458,
|
|
461,
|
|
462,
|
|
276,
|
|
353,
|
|
383,
|
|
308,
|
|
324,
|
|
325,
|
|
276,
|
|
300,
|
|
293,
|
|
372,
|
|
345,
|
|
447,
|
|
382,
|
|
398,
|
|
362,
|
|
352,
|
|
345,
|
|
340,
|
|
274,
|
|
1,
|
|
19,
|
|
456,
|
|
248,
|
|
281,
|
|
436,
|
|
427,
|
|
425,
|
|
381,
|
|
256,
|
|
252,
|
|
269,
|
|
391,
|
|
393,
|
|
200,
|
|
199,
|
|
428,
|
|
266,
|
|
330,
|
|
329,
|
|
287,
|
|
273,
|
|
422,
|
|
250,
|
|
462,
|
|
328,
|
|
258,
|
|
286,
|
|
384,
|
|
265,
|
|
353,
|
|
342,
|
|
387,
|
|
259,
|
|
257,
|
|
424,
|
|
431,
|
|
430,
|
|
342,
|
|
353,
|
|
276,
|
|
273,
|
|
335,
|
|
424,
|
|
292,
|
|
325,
|
|
307,
|
|
366,
|
|
447,
|
|
345,
|
|
271,
|
|
303,
|
|
302,
|
|
423,
|
|
266,
|
|
371,
|
|
294,
|
|
455,
|
|
460,
|
|
279,
|
|
278,
|
|
294,
|
|
271,
|
|
272,
|
|
304,
|
|
432,
|
|
434,
|
|
427,
|
|
272,
|
|
407,
|
|
408,
|
|
394,
|
|
430,
|
|
431,
|
|
395,
|
|
369,
|
|
400,
|
|
334,
|
|
333,
|
|
299,
|
|
351,
|
|
417,
|
|
168,
|
|
352,
|
|
280,
|
|
411,
|
|
325,
|
|
319,
|
|
320,
|
|
295,
|
|
296,
|
|
336,
|
|
319,
|
|
403,
|
|
404,
|
|
330,
|
|
348,
|
|
349,
|
|
293,
|
|
298,
|
|
333,
|
|
323,
|
|
454,
|
|
447,
|
|
15,
|
|
16,
|
|
315,
|
|
358,
|
|
429,
|
|
279,
|
|
14,
|
|
15,
|
|
316,
|
|
285,
|
|
336,
|
|
9,
|
|
329,
|
|
349,
|
|
350,
|
|
374,
|
|
380,
|
|
252,
|
|
318,
|
|
402,
|
|
403,
|
|
6,
|
|
197,
|
|
419,
|
|
318,
|
|
319,
|
|
325,
|
|
367,
|
|
364,
|
|
365,
|
|
435,
|
|
367,
|
|
397,
|
|
344,
|
|
438,
|
|
439,
|
|
272,
|
|
271,
|
|
311,
|
|
195,
|
|
5,
|
|
281,
|
|
273,
|
|
287,
|
|
291,
|
|
396,
|
|
428,
|
|
199,
|
|
311,
|
|
271,
|
|
268,
|
|
283,
|
|
444,
|
|
445,
|
|
373,
|
|
254,
|
|
339,
|
|
263,
|
|
466,
|
|
249,
|
|
282,
|
|
334,
|
|
296,
|
|
449,
|
|
347,
|
|
346,
|
|
264,
|
|
447,
|
|
454,
|
|
336,
|
|
296,
|
|
299,
|
|
338,
|
|
10,
|
|
151,
|
|
278,
|
|
439,
|
|
455,
|
|
292,
|
|
407,
|
|
415,
|
|
358,
|
|
371,
|
|
355,
|
|
340,
|
|
345,
|
|
372,
|
|
390,
|
|
249,
|
|
466,
|
|
346,
|
|
347,
|
|
280,
|
|
442,
|
|
443,
|
|
282,
|
|
19,
|
|
94,
|
|
370,
|
|
441,
|
|
442,
|
|
295,
|
|
248,
|
|
419,
|
|
197,
|
|
263,
|
|
255,
|
|
359,
|
|
440,
|
|
275,
|
|
274,
|
|
300,
|
|
383,
|
|
368,
|
|
351,
|
|
412,
|
|
465,
|
|
263,
|
|
467,
|
|
466,
|
|
301,
|
|
368,
|
|
389,
|
|
380,
|
|
374,
|
|
386,
|
|
395,
|
|
378,
|
|
379,
|
|
412,
|
|
351,
|
|
419,
|
|
436,
|
|
426,
|
|
322,
|
|
373,
|
|
390,
|
|
388,
|
|
2,
|
|
164,
|
|
393,
|
|
370,
|
|
462,
|
|
461,
|
|
164,
|
|
0,
|
|
267,
|
|
302,
|
|
11,
|
|
12,
|
|
374,
|
|
373,
|
|
387,
|
|
268,
|
|
12,
|
|
13,
|
|
293,
|
|
300,
|
|
301,
|
|
446,
|
|
261,
|
|
340,
|
|
385,
|
|
384,
|
|
381,
|
|
330,
|
|
266,
|
|
425,
|
|
426,
|
|
423,
|
|
391,
|
|
429,
|
|
355,
|
|
437,
|
|
391,
|
|
327,
|
|
326,
|
|
440,
|
|
457,
|
|
438,
|
|
341,
|
|
382,
|
|
362,
|
|
459,
|
|
457,
|
|
461,
|
|
434,
|
|
430,
|
|
394,
|
|
414,
|
|
463,
|
|
362,
|
|
396,
|
|
369,
|
|
262,
|
|
354,
|
|
461,
|
|
457,
|
|
316,
|
|
403,
|
|
402,
|
|
315,
|
|
404,
|
|
403,
|
|
314,
|
|
405,
|
|
404,
|
|
313,
|
|
406,
|
|
405,
|
|
421,
|
|
418,
|
|
406,
|
|
366,
|
|
401,
|
|
361,
|
|
306,
|
|
408,
|
|
407,
|
|
291,
|
|
409,
|
|
408,
|
|
287,
|
|
410,
|
|
409,
|
|
432,
|
|
436,
|
|
410,
|
|
434,
|
|
416,
|
|
411,
|
|
264,
|
|
368,
|
|
383,
|
|
309,
|
|
438,
|
|
457,
|
|
352,
|
|
376,
|
|
401,
|
|
274,
|
|
275,
|
|
4,
|
|
421,
|
|
428,
|
|
262,
|
|
294,
|
|
327,
|
|
358,
|
|
433,
|
|
416,
|
|
367,
|
|
289,
|
|
455,
|
|
439,
|
|
462,
|
|
370,
|
|
326,
|
|
2,
|
|
326,
|
|
370,
|
|
305,
|
|
460,
|
|
455,
|
|
254,
|
|
449,
|
|
448,
|
|
255,
|
|
261,
|
|
446,
|
|
253,
|
|
450,
|
|
449,
|
|
252,
|
|
451,
|
|
450,
|
|
256,
|
|
452,
|
|
451,
|
|
341,
|
|
453,
|
|
452,
|
|
413,
|
|
464,
|
|
463,
|
|
441,
|
|
413,
|
|
414,
|
|
258,
|
|
442,
|
|
441,
|
|
257,
|
|
443,
|
|
442,
|
|
259,
|
|
444,
|
|
443,
|
|
260,
|
|
445,
|
|
444,
|
|
467,
|
|
342,
|
|
445,
|
|
459,
|
|
458,
|
|
250,
|
|
289,
|
|
392,
|
|
290,
|
|
290,
|
|
328,
|
|
460,
|
|
376,
|
|
433,
|
|
435,
|
|
250,
|
|
290,
|
|
392,
|
|
411,
|
|
416,
|
|
433,
|
|
341,
|
|
463,
|
|
464,
|
|
453,
|
|
464,
|
|
465,
|
|
357,
|
|
465,
|
|
412,
|
|
343,
|
|
412,
|
|
399,
|
|
360,
|
|
363,
|
|
440,
|
|
437,
|
|
399,
|
|
456,
|
|
420,
|
|
456,
|
|
363,
|
|
401,
|
|
435,
|
|
288,
|
|
372,
|
|
383,
|
|
353,
|
|
339,
|
|
255,
|
|
249,
|
|
448,
|
|
261,
|
|
255,
|
|
133,
|
|
243,
|
|
190,
|
|
133,
|
|
155,
|
|
112,
|
|
33,
|
|
246,
|
|
247,
|
|
33,
|
|
130,
|
|
25,
|
|
398,
|
|
384,
|
|
286,
|
|
362,
|
|
398,
|
|
414,
|
|
362,
|
|
463,
|
|
341,
|
|
263,
|
|
359,
|
|
467,
|
|
263,
|
|
249,
|
|
255,
|
|
466,
|
|
467,
|
|
260,
|
|
75,
|
|
60,
|
|
166,
|
|
238,
|
|
239,
|
|
79,
|
|
162,
|
|
127,
|
|
139,
|
|
72,
|
|
11,
|
|
37,
|
|
121,
|
|
232,
|
|
120,
|
|
73,
|
|
72,
|
|
39,
|
|
114,
|
|
128,
|
|
47,
|
|
233,
|
|
232,
|
|
128,
|
|
103,
|
|
104,
|
|
67,
|
|
152,
|
|
175,
|
|
148,
|
|
173,
|
|
157,
|
|
155,
|
|
119,
|
|
118,
|
|
101,
|
|
74,
|
|
73,
|
|
40,
|
|
107,
|
|
9,
|
|
108,
|
|
49,
|
|
48,
|
|
131,
|
|
32,
|
|
194,
|
|
211,
|
|
184,
|
|
74,
|
|
185,
|
|
191,
|
|
80,
|
|
183,
|
|
185,
|
|
40,
|
|
186,
|
|
119,
|
|
230,
|
|
118,
|
|
210,
|
|
202,
|
|
214,
|
|
84,
|
|
83,
|
|
17,
|
|
77,
|
|
76,
|
|
146,
|
|
161,
|
|
160,
|
|
30,
|
|
190,
|
|
56,
|
|
173,
|
|
182,
|
|
106,
|
|
194,
|
|
138,
|
|
135,
|
|
192,
|
|
129,
|
|
203,
|
|
98,
|
|
54,
|
|
21,
|
|
68,
|
|
5,
|
|
51,
|
|
4,
|
|
145,
|
|
144,
|
|
23,
|
|
90,
|
|
77,
|
|
91,
|
|
207,
|
|
205,
|
|
187,
|
|
83,
|
|
201,
|
|
18,
|
|
181,
|
|
91,
|
|
182,
|
|
180,
|
|
90,
|
|
181,
|
|
16,
|
|
85,
|
|
17,
|
|
205,
|
|
206,
|
|
36,
|
|
176,
|
|
148,
|
|
140,
|
|
165,
|
|
92,
|
|
39,
|
|
245,
|
|
193,
|
|
244,
|
|
27,
|
|
159,
|
|
28,
|
|
30,
|
|
247,
|
|
161,
|
|
174,
|
|
236,
|
|
196,
|
|
103,
|
|
54,
|
|
104,
|
|
55,
|
|
193,
|
|
8,
|
|
111,
|
|
117,
|
|
31,
|
|
221,
|
|
189,
|
|
55,
|
|
240,
|
|
98,
|
|
99,
|
|
142,
|
|
126,
|
|
100,
|
|
219,
|
|
166,
|
|
218,
|
|
112,
|
|
155,
|
|
26,
|
|
198,
|
|
209,
|
|
131,
|
|
169,
|
|
135,
|
|
150,
|
|
114,
|
|
47,
|
|
217,
|
|
224,
|
|
223,
|
|
53,
|
|
220,
|
|
45,
|
|
134,
|
|
32,
|
|
211,
|
|
140,
|
|
109,
|
|
67,
|
|
108,
|
|
146,
|
|
43,
|
|
91,
|
|
231,
|
|
230,
|
|
120,
|
|
113,
|
|
226,
|
|
247,
|
|
105,
|
|
63,
|
|
52,
|
|
241,
|
|
238,
|
|
242,
|
|
124,
|
|
46,
|
|
156,
|
|
95,
|
|
78,
|
|
96,
|
|
70,
|
|
46,
|
|
63,
|
|
116,
|
|
143,
|
|
227,
|
|
116,
|
|
123,
|
|
111,
|
|
1,
|
|
44,
|
|
19,
|
|
3,
|
|
236,
|
|
51,
|
|
207,
|
|
216,
|
|
205,
|
|
26,
|
|
154,
|
|
22,
|
|
165,
|
|
39,
|
|
167,
|
|
199,
|
|
200,
|
|
208,
|
|
101,
|
|
36,
|
|
100,
|
|
43,
|
|
57,
|
|
202,
|
|
242,
|
|
20,
|
|
99,
|
|
56,
|
|
28,
|
|
157,
|
|
124,
|
|
35,
|
|
113,
|
|
29,
|
|
160,
|
|
27,
|
|
211,
|
|
204,
|
|
210,
|
|
124,
|
|
113,
|
|
46,
|
|
106,
|
|
43,
|
|
204,
|
|
96,
|
|
62,
|
|
77,
|
|
227,
|
|
137,
|
|
116,
|
|
73,
|
|
41,
|
|
72,
|
|
36,
|
|
203,
|
|
142,
|
|
235,
|
|
64,
|
|
240,
|
|
48,
|
|
49,
|
|
64,
|
|
42,
|
|
41,
|
|
74,
|
|
214,
|
|
212,
|
|
207,
|
|
183,
|
|
42,
|
|
184,
|
|
210,
|
|
169,
|
|
211,
|
|
140,
|
|
170,
|
|
176,
|
|
104,
|
|
105,
|
|
69,
|
|
193,
|
|
122,
|
|
168,
|
|
50,
|
|
123,
|
|
187,
|
|
89,
|
|
96,
|
|
90,
|
|
66,
|
|
65,
|
|
107,
|
|
179,
|
|
89,
|
|
180,
|
|
119,
|
|
101,
|
|
120,
|
|
68,
|
|
63,
|
|
104,
|
|
234,
|
|
93,
|
|
227,
|
|
16,
|
|
15,
|
|
85,
|
|
209,
|
|
129,
|
|
49,
|
|
15,
|
|
14,
|
|
86,
|
|
107,
|
|
55,
|
|
9,
|
|
120,
|
|
100,
|
|
121,
|
|
153,
|
|
145,
|
|
22,
|
|
178,
|
|
88,
|
|
179,
|
|
197,
|
|
6,
|
|
196,
|
|
89,
|
|
88,
|
|
96,
|
|
135,
|
|
138,
|
|
136,
|
|
138,
|
|
215,
|
|
172,
|
|
218,
|
|
115,
|
|
219,
|
|
41,
|
|
42,
|
|
81,
|
|
5,
|
|
195,
|
|
51,
|
|
57,
|
|
43,
|
|
61,
|
|
208,
|
|
171,
|
|
199,
|
|
41,
|
|
81,
|
|
38,
|
|
224,
|
|
53,
|
|
225,
|
|
24,
|
|
144,
|
|
110,
|
|
105,
|
|
52,
|
|
66,
|
|
118,
|
|
229,
|
|
117,
|
|
227,
|
|
34,
|
|
234,
|
|
66,
|
|
107,
|
|
69,
|
|
10,
|
|
109,
|
|
151,
|
|
219,
|
|
48,
|
|
235,
|
|
183,
|
|
62,
|
|
191,
|
|
142,
|
|
129,
|
|
126,
|
|
116,
|
|
111,
|
|
143,
|
|
7,
|
|
163,
|
|
246,
|
|
118,
|
|
117,
|
|
50,
|
|
223,
|
|
222,
|
|
52,
|
|
94,
|
|
19,
|
|
141,
|
|
222,
|
|
221,
|
|
65,
|
|
196,
|
|
3,
|
|
197,
|
|
45,
|
|
220,
|
|
44,
|
|
156,
|
|
70,
|
|
139,
|
|
188,
|
|
122,
|
|
245,
|
|
139,
|
|
71,
|
|
162,
|
|
145,
|
|
153,
|
|
159,
|
|
149,
|
|
170,
|
|
150,
|
|
122,
|
|
188,
|
|
196,
|
|
206,
|
|
216,
|
|
92,
|
|
163,
|
|
144,
|
|
161,
|
|
164,
|
|
2,
|
|
167,
|
|
242,
|
|
141,
|
|
241,
|
|
0,
|
|
164,
|
|
37,
|
|
11,
|
|
72,
|
|
12,
|
|
144,
|
|
145,
|
|
160,
|
|
12,
|
|
38,
|
|
13,
|
|
70,
|
|
63,
|
|
71,
|
|
31,
|
|
226,
|
|
111,
|
|
157,
|
|
158,
|
|
154,
|
|
36,
|
|
101,
|
|
205,
|
|
203,
|
|
206,
|
|
165,
|
|
126,
|
|
209,
|
|
217,
|
|
98,
|
|
165,
|
|
97,
|
|
237,
|
|
220,
|
|
218,
|
|
237,
|
|
239,
|
|
241,
|
|
210,
|
|
214,
|
|
169,
|
|
140,
|
|
171,
|
|
32,
|
|
241,
|
|
125,
|
|
237,
|
|
179,
|
|
86,
|
|
178,
|
|
180,
|
|
85,
|
|
179,
|
|
181,
|
|
84,
|
|
180,
|
|
182,
|
|
83,
|
|
181,
|
|
194,
|
|
201,
|
|
182,
|
|
177,
|
|
137,
|
|
132,
|
|
184,
|
|
76,
|
|
183,
|
|
185,
|
|
61,
|
|
184,
|
|
186,
|
|
57,
|
|
185,
|
|
216,
|
|
212,
|
|
186,
|
|
192,
|
|
214,
|
|
187,
|
|
139,
|
|
34,
|
|
156,
|
|
218,
|
|
79,
|
|
237,
|
|
147,
|
|
123,
|
|
177,
|
|
45,
|
|
44,
|
|
4,
|
|
208,
|
|
201,
|
|
32,
|
|
98,
|
|
64,
|
|
129,
|
|
192,
|
|
213,
|
|
138,
|
|
235,
|
|
59,
|
|
219,
|
|
141,
|
|
242,
|
|
97,
|
|
97,
|
|
2,
|
|
141,
|
|
240,
|
|
75,
|
|
235,
|
|
229,
|
|
24,
|
|
228,
|
|
31,
|
|
25,
|
|
226,
|
|
230,
|
|
23,
|
|
229,
|
|
231,
|
|
22,
|
|
230,
|
|
232,
|
|
26,
|
|
231,
|
|
233,
|
|
112,
|
|
232,
|
|
244,
|
|
189,
|
|
243,
|
|
189,
|
|
221,
|
|
190,
|
|
222,
|
|
28,
|
|
221,
|
|
223,
|
|
27,
|
|
222,
|
|
224,
|
|
29,
|
|
223,
|
|
225,
|
|
30,
|
|
224,
|
|
113,
|
|
247,
|
|
225,
|
|
99,
|
|
60,
|
|
240,
|
|
213,
|
|
147,
|
|
215,
|
|
60,
|
|
20,
|
|
166,
|
|
192,
|
|
187,
|
|
213,
|
|
243,
|
|
112,
|
|
244,
|
|
244,
|
|
233,
|
|
245,
|
|
245,
|
|
128,
|
|
188,
|
|
188,
|
|
114,
|
|
174,
|
|
134,
|
|
131,
|
|
220,
|
|
174,
|
|
217,
|
|
236,
|
|
236,
|
|
198,
|
|
134,
|
|
215,
|
|
177,
|
|
58,
|
|
156,
|
|
143,
|
|
124,
|
|
25,
|
|
110,
|
|
7,
|
|
31,
|
|
228,
|
|
25,
|
|
264,
|
|
356,
|
|
368,
|
|
0,
|
|
11,
|
|
267,
|
|
451,
|
|
452,
|
|
349,
|
|
267,
|
|
302,
|
|
269,
|
|
350,
|
|
357,
|
|
277,
|
|
350,
|
|
452,
|
|
357,
|
|
299,
|
|
333,
|
|
297,
|
|
396,
|
|
175,
|
|
377,
|
|
381,
|
|
384,
|
|
382,
|
|
280,
|
|
347,
|
|
330,
|
|
269,
|
|
303,
|
|
270,
|
|
151,
|
|
9,
|
|
337,
|
|
344,
|
|
278,
|
|
360,
|
|
424,
|
|
418,
|
|
431,
|
|
270,
|
|
304,
|
|
409,
|
|
272,
|
|
310,
|
|
407,
|
|
322,
|
|
270,
|
|
410,
|
|
449,
|
|
450,
|
|
347,
|
|
432,
|
|
422,
|
|
434,
|
|
18,
|
|
313,
|
|
17,
|
|
291,
|
|
306,
|
|
375,
|
|
259,
|
|
387,
|
|
260,
|
|
424,
|
|
335,
|
|
418,
|
|
434,
|
|
364,
|
|
416,
|
|
391,
|
|
423,
|
|
327,
|
|
301,
|
|
251,
|
|
298,
|
|
275,
|
|
281,
|
|
4,
|
|
254,
|
|
373,
|
|
253,
|
|
375,
|
|
307,
|
|
321,
|
|
280,
|
|
425,
|
|
411,
|
|
200,
|
|
421,
|
|
18,
|
|
335,
|
|
321,
|
|
406,
|
|
321,
|
|
320,
|
|
405,
|
|
314,
|
|
315,
|
|
17,
|
|
423,
|
|
426,
|
|
266,
|
|
396,
|
|
377,
|
|
369,
|
|
270,
|
|
322,
|
|
269,
|
|
413,
|
|
417,
|
|
464,
|
|
385,
|
|
386,
|
|
258,
|
|
248,
|
|
456,
|
|
419,
|
|
298,
|
|
284,
|
|
333,
|
|
168,
|
|
417,
|
|
8,
|
|
448,
|
|
346,
|
|
261,
|
|
417,
|
|
413,
|
|
285,
|
|
326,
|
|
327,
|
|
328,
|
|
277,
|
|
355,
|
|
329,
|
|
309,
|
|
392,
|
|
438,
|
|
381,
|
|
382,
|
|
256,
|
|
279,
|
|
429,
|
|
360,
|
|
365,
|
|
364,
|
|
379,
|
|
355,
|
|
277,
|
|
437,
|
|
282,
|
|
443,
|
|
283,
|
|
281,
|
|
275,
|
|
363,
|
|
395,
|
|
431,
|
|
369,
|
|
299,
|
|
297,
|
|
337,
|
|
335,
|
|
273,
|
|
321,
|
|
348,
|
|
450,
|
|
349,
|
|
359,
|
|
446,
|
|
467,
|
|
283,
|
|
293,
|
|
282,
|
|
250,
|
|
458,
|
|
462,
|
|
300,
|
|
276,
|
|
383,
|
|
292,
|
|
308,
|
|
325,
|
|
283,
|
|
276,
|
|
293,
|
|
264,
|
|
372,
|
|
447,
|
|
346,
|
|
352,
|
|
340,
|
|
354,
|
|
274,
|
|
19,
|
|
363,
|
|
456,
|
|
281,
|
|
426,
|
|
436,
|
|
425,
|
|
380,
|
|
381,
|
|
252,
|
|
267,
|
|
269,
|
|
393,
|
|
421,
|
|
200,
|
|
428,
|
|
371,
|
|
266,
|
|
329,
|
|
432,
|
|
287,
|
|
422,
|
|
290,
|
|
250,
|
|
328,
|
|
385,
|
|
258,
|
|
384,
|
|
446,
|
|
265,
|
|
342,
|
|
386,
|
|
387,
|
|
257,
|
|
422,
|
|
424,
|
|
430,
|
|
445,
|
|
342,
|
|
276,
|
|
422,
|
|
273,
|
|
424,
|
|
306,
|
|
292,
|
|
307,
|
|
352,
|
|
366,
|
|
345,
|
|
268,
|
|
271,
|
|
302,
|
|
358,
|
|
423,
|
|
371,
|
|
327,
|
|
294,
|
|
460,
|
|
331,
|
|
279,
|
|
294,
|
|
303,
|
|
271,
|
|
304,
|
|
436,
|
|
432,
|
|
427,
|
|
304,
|
|
272,
|
|
408,
|
|
395,
|
|
394,
|
|
431,
|
|
378,
|
|
395,
|
|
400,
|
|
296,
|
|
334,
|
|
299,
|
|
6,
|
|
351,
|
|
168,
|
|
376,
|
|
352,
|
|
411,
|
|
307,
|
|
325,
|
|
320,
|
|
285,
|
|
295,
|
|
336,
|
|
320,
|
|
319,
|
|
404,
|
|
329,
|
|
330,
|
|
349,
|
|
334,
|
|
293,
|
|
333,
|
|
366,
|
|
323,
|
|
447,
|
|
316,
|
|
15,
|
|
315,
|
|
331,
|
|
358,
|
|
279,
|
|
317,
|
|
14,
|
|
316,
|
|
8,
|
|
285,
|
|
9,
|
|
277,
|
|
329,
|
|
350,
|
|
253,
|
|
374,
|
|
252,
|
|
319,
|
|
318,
|
|
403,
|
|
351,
|
|
6,
|
|
419,
|
|
324,
|
|
318,
|
|
325,
|
|
397,
|
|
367,
|
|
365,
|
|
288,
|
|
435,
|
|
397,
|
|
278,
|
|
344,
|
|
439,
|
|
310,
|
|
272,
|
|
311,
|
|
248,
|
|
195,
|
|
281,
|
|
375,
|
|
273,
|
|
291,
|
|
175,
|
|
396,
|
|
199,
|
|
312,
|
|
311,
|
|
268,
|
|
276,
|
|
283,
|
|
445,
|
|
390,
|
|
373,
|
|
339,
|
|
295,
|
|
282,
|
|
296,
|
|
448,
|
|
449,
|
|
346,
|
|
356,
|
|
264,
|
|
454,
|
|
337,
|
|
336,
|
|
299,
|
|
337,
|
|
338,
|
|
151,
|
|
294,
|
|
278,
|
|
455,
|
|
308,
|
|
292,
|
|
415,
|
|
429,
|
|
358,
|
|
355,
|
|
265,
|
|
340,
|
|
372,
|
|
388,
|
|
390,
|
|
466,
|
|
352,
|
|
346,
|
|
280,
|
|
295,
|
|
442,
|
|
282,
|
|
354,
|
|
19,
|
|
370,
|
|
285,
|
|
441,
|
|
295,
|
|
195,
|
|
248,
|
|
197,
|
|
457,
|
|
440,
|
|
274,
|
|
301,
|
|
300,
|
|
368,
|
|
417,
|
|
351,
|
|
465,
|
|
251,
|
|
301,
|
|
389,
|
|
385,
|
|
380,
|
|
386,
|
|
394,
|
|
395,
|
|
379,
|
|
399,
|
|
412,
|
|
419,
|
|
410,
|
|
436,
|
|
322,
|
|
387,
|
|
373,
|
|
388,
|
|
326,
|
|
2,
|
|
393,
|
|
354,
|
|
370,
|
|
461,
|
|
393,
|
|
164,
|
|
267,
|
|
268,
|
|
302,
|
|
12,
|
|
386,
|
|
374,
|
|
387,
|
|
312,
|
|
268,
|
|
13,
|
|
298,
|
|
293,
|
|
301,
|
|
265,
|
|
446,
|
|
340,
|
|
380,
|
|
385,
|
|
381,
|
|
280,
|
|
330,
|
|
425,
|
|
322,
|
|
426,
|
|
391,
|
|
420,
|
|
429,
|
|
437,
|
|
393,
|
|
391,
|
|
326,
|
|
344,
|
|
440,
|
|
438,
|
|
458,
|
|
459,
|
|
461,
|
|
364,
|
|
434,
|
|
394,
|
|
428,
|
|
396,
|
|
262,
|
|
274,
|
|
354,
|
|
457,
|
|
317,
|
|
316,
|
|
402,
|
|
316,
|
|
315,
|
|
403,
|
|
315,
|
|
314,
|
|
404,
|
|
314,
|
|
313,
|
|
405,
|
|
313,
|
|
421,
|
|
406,
|
|
323,
|
|
366,
|
|
361,
|
|
292,
|
|
306,
|
|
407,
|
|
306,
|
|
291,
|
|
408,
|
|
291,
|
|
287,
|
|
409,
|
|
287,
|
|
432,
|
|
410,
|
|
427,
|
|
434,
|
|
411,
|
|
372,
|
|
264,
|
|
383,
|
|
459,
|
|
309,
|
|
457,
|
|
366,
|
|
352,
|
|
401,
|
|
1,
|
|
274,
|
|
4,
|
|
418,
|
|
421,
|
|
262,
|
|
331,
|
|
294,
|
|
358,
|
|
435,
|
|
433,
|
|
367,
|
|
392,
|
|
289,
|
|
439,
|
|
328,
|
|
462,
|
|
326,
|
|
94,
|
|
2,
|
|
370,
|
|
289,
|
|
305,
|
|
455,
|
|
339,
|
|
254,
|
|
448,
|
|
359,
|
|
255,
|
|
446,
|
|
254,
|
|
253,
|
|
449,
|
|
253,
|
|
252,
|
|
450,
|
|
252,
|
|
256,
|
|
451,
|
|
256,
|
|
341,
|
|
452,
|
|
414,
|
|
413,
|
|
463,
|
|
286,
|
|
441,
|
|
414,
|
|
286,
|
|
258,
|
|
441,
|
|
258,
|
|
257,
|
|
442,
|
|
257,
|
|
259,
|
|
443,
|
|
259,
|
|
260,
|
|
444,
|
|
260,
|
|
467,
|
|
445,
|
|
309,
|
|
459,
|
|
250,
|
|
305,
|
|
289,
|
|
290,
|
|
305,
|
|
290,
|
|
460,
|
|
401,
|
|
376,
|
|
435,
|
|
309,
|
|
250,
|
|
392,
|
|
376,
|
|
411,
|
|
433,
|
|
453,
|
|
341,
|
|
464,
|
|
357,
|
|
453,
|
|
465,
|
|
343,
|
|
357,
|
|
412,
|
|
437,
|
|
343,
|
|
399,
|
|
344,
|
|
360,
|
|
440,
|
|
420,
|
|
437,
|
|
456,
|
|
360,
|
|
420,
|
|
363,
|
|
361,
|
|
401,
|
|
288,
|
|
265,
|
|
372,
|
|
353,
|
|
390,
|
|
339,
|
|
249,
|
|
339,
|
|
448,
|
|
255
|
|
];
|
|
});
|
|
|
|
// src/facemesh/index.js
|
|
var require_facemesh = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const blazeface = require_blazeface();
|
|
const keypoints = require_keypoints();
|
|
const pipe = require_pipeline();
|
|
const uv_coords = require_uvcoords();
|
|
const triangulation = require_triangulation().default;
|
|
exports.uv_coords = uv_coords;
|
|
exports.triangulation = triangulation;
|
|
async function loadDetectorModel(config) {
|
|
return blazeface.load(config);
|
|
}
|
|
async function loadMeshModel(modelUrl) {
|
|
return tf.loadGraphModel(modelUrl, {fromTFHub: modelUrl.includes("tfhub.dev")});
|
|
}
|
|
async function loadIrisModel(modelUrl) {
|
|
return tf.loadGraphModel(modelUrl, {fromTFHub: modelUrl.includes("tfhub.dev")});
|
|
}
|
|
async function load(config) {
|
|
const models = await Promise.all([
|
|
loadDetectorModel(config),
|
|
loadMeshModel(config.mesh.modelPath),
|
|
loadIrisModel(config.iris.modelPath)
|
|
]);
|
|
const faceMesh = new MediaPipeFaceMesh(models[0], models[1], models[2], config);
|
|
return faceMesh;
|
|
}
|
|
exports.load = load;
|
|
class MediaPipeFaceMesh {
|
|
constructor(blazeFace, blazeMeshModel, irisModel, config) {
|
|
this.pipeline = new pipe.Pipeline(blazeFace, blazeMeshModel, irisModel, config);
|
|
this.config = config;
|
|
}
|
|
async estimateFaces(input, config) {
|
|
if (config)
|
|
this.config = config;
|
|
const image = tf.tidy(() => {
|
|
if (!(input instanceof tf.Tensor)) {
|
|
input = tf.browser.fromPixels(input);
|
|
}
|
|
return input.toFloat().expandDims(0);
|
|
});
|
|
const results = [];
|
|
const predictions = await this.pipeline.predict(image, this.config.iris.enabled, this.config.mesh.enabled);
|
|
image.dispose();
|
|
if (!predictions)
|
|
return results;
|
|
for (const prediction of predictions) {
|
|
const confidence = prediction.confidence.arraySync();
|
|
if (confidence >= this.config.detector.minConfidence) {
|
|
const result = {
|
|
confidence: confidence || 0,
|
|
box: prediction.box ? [prediction.box.startPoint[0], prediction.box.startPoint[1], prediction.box.endPoint[0] - prediction.box.startPoint[0], prediction.box.endPoint[1] - prediction.box.startPoint[1]] : 0,
|
|
mesh: prediction.coords ? prediction.coords.arraySync() : null,
|
|
image: prediction.image ? tf.clone(prediction.image) : null
|
|
};
|
|
const annotations = {};
|
|
if (result.mesh && result.mesh.length > 0) {
|
|
for (const key in keypoints.MESH_ANNOTATIONS) {
|
|
if (this.config.iris.enabled || key.includes("Iris") === false) {
|
|
annotations[key] = keypoints.MESH_ANNOTATIONS[key].map((index) => result.mesh[index]);
|
|
}
|
|
}
|
|
}
|
|
result["annotations"] = annotations;
|
|
results.push(result);
|
|
}
|
|
tf.dispose(prediction.confidence);
|
|
tf.dispose(prediction.image);
|
|
tf.dispose(prediction.coords);
|
|
}
|
|
return results;
|
|
}
|
|
}
|
|
exports.MediaPipeFaceMesh = MediaPipeFaceMesh;
|
|
});
|
|
|
|
// src/ssrnet/index.js
|
|
var require_ssrnet = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const models = {};
|
|
let last = {age: 0, gender: ""};
|
|
let frame = 0;
|
|
async function getImage(image, size) {
|
|
const tensor = tf.tidy(() => {
|
|
const buffer = tf.browser.fromPixels(image);
|
|
const resize = tf.image.resizeBilinear(buffer, [size, size]);
|
|
const expand = tf.cast(tf.expandDims(resize, 0), "float32");
|
|
return expand;
|
|
});
|
|
return tensor;
|
|
}
|
|
async function predict(image, config) {
|
|
frame += 1;
|
|
if (frame >= config.face.age.skipFrames) {
|
|
frame = 0;
|
|
return last;
|
|
}
|
|
if (!models.age && config.face.age.enabled)
|
|
models.age = await tf.loadGraphModel(config.face.age.modelPath);
|
|
if (!models.gender && config.face.gender.enabled)
|
|
models.gender = await tf.loadGraphModel(config.face.gender.modelPath);
|
|
let enhance;
|
|
if (image instanceof tf.Tensor) {
|
|
const resize = tf.image.resizeBilinear(image, [config.face.age.inputSize, config.face.age.inputSize], false);
|
|
enhance = tf.mul(resize, [255]);
|
|
tf.dispose(resize);
|
|
} else {
|
|
enhance = await getImage(image, config.face.age.inputSize);
|
|
}
|
|
const obj = {};
|
|
if (config.face.age.enabled) {
|
|
const ageT = await models.age.predict(enhance);
|
|
obj.age = Math.trunc(10 * ageT.dataSync()[0]) / 10;
|
|
tf.dispose(ageT);
|
|
}
|
|
if (config.face.gender.enabled) {
|
|
const genderT = await models.gender.predict(enhance);
|
|
obj.gender = Math.trunc(100 * genderT.dataSync()[0]) < 50 ? "female" : "male";
|
|
tf.dispose(genderT);
|
|
}
|
|
tf.dispose(enhance);
|
|
last = obj;
|
|
return obj;
|
|
}
|
|
exports.predict = predict;
|
|
});
|
|
|
|
// src/posenet/modelBase.js
|
|
var require_modelBase = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
class BaseModel {
|
|
constructor(model, outputStride) {
|
|
this.model = model;
|
|
this.outputStride = outputStride;
|
|
const inputShape = this.model.inputs[0].shape;
|
|
tf.util.assert(inputShape[1] === -1 && inputShape[2] === -1, () => `Input shape [${inputShape[1]}, ${inputShape[2]}] must both be equal to or -1`);
|
|
}
|
|
predict(input) {
|
|
return tf.tidy(() => {
|
|
const asFloat = this.preprocessInput(input.toFloat());
|
|
const asBatch = asFloat.expandDims(0);
|
|
const results = this.model.predict(asBatch);
|
|
const results3d = results.map((y) => y.squeeze([0]));
|
|
const namedResults = this.nameOutputResults(results3d);
|
|
return {
|
|
heatmapScores: namedResults.heatmap.sigmoid(),
|
|
offsets: namedResults.offsets,
|
|
displacementFwd: namedResults.displacementFwd,
|
|
displacementBwd: namedResults.displacementBwd
|
|
};
|
|
});
|
|
}
|
|
dispose() {
|
|
this.model.dispose();
|
|
}
|
|
}
|
|
exports.BaseModel = BaseModel;
|
|
});
|
|
|
|
// src/posenet/modelMobileNet.js
|
|
var require_modelMobileNet = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const modelBase = require_modelBase();
|
|
class MobileNet extends modelBase.BaseModel {
|
|
preprocessInput(input) {
|
|
return tf.tidy(() => tf.div(input, 127.5).sub(1));
|
|
}
|
|
nameOutputResults(results) {
|
|
const [offsets, heatmap, displacementFwd, displacementBwd] = results;
|
|
return {offsets, heatmap, displacementFwd, displacementBwd};
|
|
}
|
|
}
|
|
exports.MobileNet = MobileNet;
|
|
});
|
|
|
|
// src/posenet/heapSort.js
|
|
var require_heapSort = __commonJS((exports) => {
|
|
function half(k) {
|
|
return Math.floor(k / 2);
|
|
}
|
|
class MaxHeap {
|
|
constructor(maxSize, getElementValue) {
|
|
this.priorityQueue = new Array(maxSize);
|
|
this.numberOfElements = -1;
|
|
this.getElementValue = getElementValue;
|
|
}
|
|
enqueue(x) {
|
|
this.priorityQueue[++this.numberOfElements] = x;
|
|
this.swim(this.numberOfElements);
|
|
}
|
|
dequeue() {
|
|
const max = this.priorityQueue[0];
|
|
this.exchange(0, this.numberOfElements--);
|
|
this.sink(0);
|
|
this.priorityQueue[this.numberOfElements + 1] = null;
|
|
return max;
|
|
}
|
|
empty() {
|
|
return this.numberOfElements === -1;
|
|
}
|
|
size() {
|
|
return this.numberOfElements + 1;
|
|
}
|
|
all() {
|
|
return this.priorityQueue.slice(0, this.numberOfElements + 1);
|
|
}
|
|
max() {
|
|
return this.priorityQueue[0];
|
|
}
|
|
swim(k) {
|
|
while (k > 0 && this.less(half(k), k)) {
|
|
this.exchange(k, half(k));
|
|
k = half(k);
|
|
}
|
|
}
|
|
sink(k) {
|
|
while (2 * k <= this.numberOfElements) {
|
|
let j = 2 * k;
|
|
if (j < this.numberOfElements && this.less(j, j + 1))
|
|
j++;
|
|
if (!this.less(k, j))
|
|
break;
|
|
this.exchange(k, j);
|
|
k = j;
|
|
}
|
|
}
|
|
getValueAt(i) {
|
|
return this.getElementValue(this.priorityQueue[i]);
|
|
}
|
|
less(i, j) {
|
|
return this.getValueAt(i) < this.getValueAt(j);
|
|
}
|
|
exchange(i, j) {
|
|
const t = this.priorityQueue[i];
|
|
this.priorityQueue[i] = this.priorityQueue[j];
|
|
this.priorityQueue[j] = t;
|
|
}
|
|
}
|
|
exports.MaxHeap = MaxHeap;
|
|
});
|
|
|
|
// src/posenet/buildParts.js
|
|
var require_buildParts = __commonJS((exports) => {
|
|
const heapSort = require_heapSort();
|
|
function scoreIsMaximumInLocalWindow(keypointId, score, heatmapY, heatmapX, localMaximumRadius, scores) {
|
|
const [height, width] = scores.shape;
|
|
let localMaximum = true;
|
|
const yStart = Math.max(heatmapY - localMaximumRadius, 0);
|
|
const yEnd = Math.min(heatmapY + localMaximumRadius + 1, height);
|
|
for (let yCurrent = yStart; yCurrent < yEnd; ++yCurrent) {
|
|
const xStart = Math.max(heatmapX - localMaximumRadius, 0);
|
|
const xEnd = Math.min(heatmapX + localMaximumRadius + 1, width);
|
|
for (let xCurrent = xStart; xCurrent < xEnd; ++xCurrent) {
|
|
if (scores.get(yCurrent, xCurrent, keypointId) > score) {
|
|
localMaximum = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!localMaximum) {
|
|
break;
|
|
}
|
|
}
|
|
return localMaximum;
|
|
}
|
|
function buildPartWithScoreQueue(scoreThreshold, localMaximumRadius, scores) {
|
|
const [height, width, numKeypoints] = scores.shape;
|
|
const queue = new heapSort.MaxHeap(height * width * numKeypoints, ({score}) => score);
|
|
for (let heatmapY = 0; heatmapY < height; ++heatmapY) {
|
|
for (let heatmapX = 0; heatmapX < width; ++heatmapX) {
|
|
for (let keypointId = 0; keypointId < numKeypoints; ++keypointId) {
|
|
const score = scores.get(heatmapY, heatmapX, keypointId);
|
|
if (score < scoreThreshold)
|
|
continue;
|
|
if (scoreIsMaximumInLocalWindow(keypointId, score, heatmapY, heatmapX, localMaximumRadius, scores)) {
|
|
queue.enqueue({score, part: {heatmapY, heatmapX, id: keypointId}});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return queue;
|
|
}
|
|
exports.buildPartWithScoreQueue = buildPartWithScoreQueue;
|
|
});
|
|
|
|
// src/posenet/keypoints.js
|
|
var require_keypoints2 = __commonJS((exports) => {
|
|
exports.partNames = [
|
|
"nose",
|
|
"leftEye",
|
|
"rightEye",
|
|
"leftEar",
|
|
"rightEar",
|
|
"leftShoulder",
|
|
"rightShoulder",
|
|
"leftElbow",
|
|
"rightElbow",
|
|
"leftWrist",
|
|
"rightWrist",
|
|
"leftHip",
|
|
"rightHip",
|
|
"leftKnee",
|
|
"rightKnee",
|
|
"leftAnkle",
|
|
"rightAnkle"
|
|
];
|
|
exports.NUM_KEYPOINTS = exports.partNames.length;
|
|
exports.partIds = exports.partNames.reduce((result, jointName, i) => {
|
|
result[jointName] = i;
|
|
return result;
|
|
}, {});
|
|
const connectedPartNames = [
|
|
["leftHip", "leftShoulder"],
|
|
["leftElbow", "leftShoulder"],
|
|
["leftElbow", "leftWrist"],
|
|
["leftHip", "leftKnee"],
|
|
["leftKnee", "leftAnkle"],
|
|
["rightHip", "rightShoulder"],
|
|
["rightElbow", "rightShoulder"],
|
|
["rightElbow", "rightWrist"],
|
|
["rightHip", "rightKnee"],
|
|
["rightKnee", "rightAnkle"],
|
|
["leftShoulder", "rightShoulder"],
|
|
["leftHip", "rightHip"]
|
|
];
|
|
exports.poseChain = [
|
|
["nose", "leftEye"],
|
|
["leftEye", "leftEar"],
|
|
["nose", "rightEye"],
|
|
["rightEye", "rightEar"],
|
|
["nose", "leftShoulder"],
|
|
["leftShoulder", "leftElbow"],
|
|
["leftElbow", "leftWrist"],
|
|
["leftShoulder", "leftHip"],
|
|
["leftHip", "leftKnee"],
|
|
["leftKnee", "leftAnkle"],
|
|
["nose", "rightShoulder"],
|
|
["rightShoulder", "rightElbow"],
|
|
["rightElbow", "rightWrist"],
|
|
["rightShoulder", "rightHip"],
|
|
["rightHip", "rightKnee"],
|
|
["rightKnee", "rightAnkle"]
|
|
];
|
|
exports.connectedPartIndices = connectedPartNames.map(([jointNameA, jointNameB]) => [exports.partIds[jointNameA], exports.partIds[jointNameB]]);
|
|
exports.partChannels = [
|
|
"left_face",
|
|
"right_face",
|
|
"right_upper_leg_front",
|
|
"right_lower_leg_back",
|
|
"right_upper_leg_back",
|
|
"left_lower_leg_front",
|
|
"left_upper_leg_front",
|
|
"left_upper_leg_back",
|
|
"left_lower_leg_back",
|
|
"right_feet",
|
|
"right_lower_leg_front",
|
|
"left_feet",
|
|
"torso_front",
|
|
"torso_back",
|
|
"right_upper_arm_front",
|
|
"right_upper_arm_back",
|
|
"right_lower_arm_back",
|
|
"left_lower_arm_front",
|
|
"left_upper_arm_front",
|
|
"left_upper_arm_back",
|
|
"left_lower_arm_back",
|
|
"right_hand",
|
|
"right_lower_arm_front",
|
|
"left_hand"
|
|
];
|
|
});
|
|
|
|
// src/posenet/vectors.js
|
|
var require_vectors = __commonJS((exports) => {
|
|
const kpt = require_keypoints2();
|
|
function getOffsetPoint(y, x, keypoint, offsets) {
|
|
return {
|
|
y: offsets.get(y, x, keypoint),
|
|
x: offsets.get(y, x, keypoint + kpt.NUM_KEYPOINTS)
|
|
};
|
|
}
|
|
exports.getOffsetPoint = getOffsetPoint;
|
|
function getImageCoords(part, outputStride, offsets) {
|
|
const {heatmapY, heatmapX, id: keypoint} = part;
|
|
const {y, x} = getOffsetPoint(heatmapY, heatmapX, keypoint, offsets);
|
|
return {
|
|
x: part.heatmapX * outputStride + x,
|
|
y: part.heatmapY * outputStride + y
|
|
};
|
|
}
|
|
exports.getImageCoords = getImageCoords;
|
|
function fillArray(element, size) {
|
|
const result = new Array(size);
|
|
for (let i = 0; i < size; i++) {
|
|
result[i] = element;
|
|
}
|
|
return result;
|
|
}
|
|
exports.fillArray = fillArray;
|
|
function clamp(a, min, max) {
|
|
if (a < min)
|
|
return min;
|
|
if (a > max)
|
|
return max;
|
|
return a;
|
|
}
|
|
exports.clamp = clamp;
|
|
function squaredDistance(y1, x1, y2, x2) {
|
|
const dy = y2 - y1;
|
|
const dx = x2 - x1;
|
|
return dy * dy + dx * dx;
|
|
}
|
|
exports.squaredDistance = squaredDistance;
|
|
function addVectors(a, b) {
|
|
return {x: a.x + b.x, y: a.y + b.y};
|
|
}
|
|
exports.addVectors = addVectors;
|
|
function clampVector(a, min, max) {
|
|
return {y: clamp(a.y, min, max), x: clamp(a.x, min, max)};
|
|
}
|
|
exports.clampVector = clampVector;
|
|
});
|
|
|
|
// src/posenet/decodePose.js
|
|
var require_decodePose = __commonJS((exports) => {
|
|
const keypoints = require_keypoints2();
|
|
const vectors = require_vectors();
|
|
const parentChildrenTuples = keypoints.poseChain.map(([parentJoinName, childJoinName]) => [keypoints.partIds[parentJoinName], keypoints.partIds[childJoinName]]);
|
|
const parentToChildEdges = parentChildrenTuples.map(([, childJointId]) => childJointId);
|
|
const childToParentEdges = parentChildrenTuples.map(([parentJointId]) => parentJointId);
|
|
function getDisplacement(edgeId, point, displacements) {
|
|
const numEdges = displacements.shape[2] / 2;
|
|
return {
|
|
y: displacements.get(point.y, point.x, edgeId),
|
|
x: displacements.get(point.y, point.x, numEdges + edgeId)
|
|
};
|
|
}
|
|
function getStridedIndexNearPoint(point, outputStride, height, width) {
|
|
return {
|
|
y: vectors.clamp(Math.round(point.y / outputStride), 0, height - 1),
|
|
x: vectors.clamp(Math.round(point.x / outputStride), 0, width - 1)
|
|
};
|
|
}
|
|
function traverseToTargetKeypoint(edgeId, sourceKeypoint, targetKeypointId, scoresBuffer, offsets, outputStride, displacements, offsetRefineStep = 2) {
|
|
const [height, width] = scoresBuffer.shape;
|
|
const sourceKeypointIndices = getStridedIndexNearPoint(sourceKeypoint.position, outputStride, height, width);
|
|
const displacement = getDisplacement(edgeId, sourceKeypointIndices, displacements);
|
|
const displacedPoint = vectors.addVectors(sourceKeypoint.position, displacement);
|
|
let targetKeypoint = displacedPoint;
|
|
for (let i = 0; i < offsetRefineStep; i++) {
|
|
const targetKeypointIndices = getStridedIndexNearPoint(targetKeypoint, outputStride, height, width);
|
|
const offsetPoint = vectors.getOffsetPoint(targetKeypointIndices.y, targetKeypointIndices.x, targetKeypointId, offsets);
|
|
targetKeypoint = vectors.addVectors({
|
|
x: targetKeypointIndices.x * outputStride,
|
|
y: targetKeypointIndices.y * outputStride
|
|
}, {x: offsetPoint.x, y: offsetPoint.y});
|
|
}
|
|
const targetKeyPointIndices = getStridedIndexNearPoint(targetKeypoint, outputStride, height, width);
|
|
const score = scoresBuffer.get(targetKeyPointIndices.y, targetKeyPointIndices.x, targetKeypointId);
|
|
return {position: targetKeypoint, part: keypoints.partNames[targetKeypointId], score};
|
|
}
|
|
function decodePose(root, scores, offsets, outputStride, displacementsFwd, displacementsBwd) {
|
|
const numParts = scores.shape[2];
|
|
const numEdges = parentToChildEdges.length;
|
|
const instanceKeypoints = new Array(numParts);
|
|
const {part: rootPart, score: rootScore} = root;
|
|
const rootPoint = vectors.getImageCoords(rootPart, outputStride, offsets);
|
|
instanceKeypoints[rootPart.id] = {
|
|
score: rootScore,
|
|
part: keypoints.partNames[rootPart.id],
|
|
position: rootPoint
|
|
};
|
|
for (let edge = numEdges - 1; edge >= 0; --edge) {
|
|
const sourceKeypointId = parentToChildEdges[edge];
|
|
const targetKeypointId = childToParentEdges[edge];
|
|
if (instanceKeypoints[sourceKeypointId] && !instanceKeypoints[targetKeypointId]) {
|
|
instanceKeypoints[targetKeypointId] = traverseToTargetKeypoint(edge, instanceKeypoints[sourceKeypointId], targetKeypointId, scores, offsets, outputStride, displacementsBwd);
|
|
}
|
|
}
|
|
for (let edge = 0; edge < numEdges; ++edge) {
|
|
const sourceKeypointId = childToParentEdges[edge];
|
|
const targetKeypointId = parentToChildEdges[edge];
|
|
if (instanceKeypoints[sourceKeypointId] && !instanceKeypoints[targetKeypointId]) {
|
|
instanceKeypoints[targetKeypointId] = traverseToTargetKeypoint(edge, instanceKeypoints[sourceKeypointId], targetKeypointId, scores, offsets, outputStride, displacementsFwd);
|
|
}
|
|
}
|
|
return instanceKeypoints;
|
|
}
|
|
exports.decodePose = decodePose;
|
|
});
|
|
|
|
// src/posenet/decodeMultiple.js
|
|
var require_decodeMultiple = __commonJS((exports) => {
|
|
const buildParts = require_buildParts();
|
|
const decodePose = require_decodePose();
|
|
const vectors = require_vectors();
|
|
function withinNmsRadiusOfCorrespondingPoint(poses, squaredNmsRadius, {x, y}, keypointId) {
|
|
return poses.some(({keypoints}) => {
|
|
const correspondingKeypoint = keypoints[keypointId].position;
|
|
return vectors.squaredDistance(y, x, correspondingKeypoint.y, correspondingKeypoint.x) <= squaredNmsRadius;
|
|
});
|
|
}
|
|
function getInstanceScore(existingPoses, squaredNmsRadius, instanceKeypoints) {
|
|
const notOverlappedKeypointScores = instanceKeypoints.reduce((result, {position, score}, keypointId) => {
|
|
if (!withinNmsRadiusOfCorrespondingPoint(existingPoses, squaredNmsRadius, position, keypointId)) {
|
|
result += score;
|
|
}
|
|
return result;
|
|
}, 0);
|
|
return notOverlappedKeypointScores / instanceKeypoints.length;
|
|
}
|
|
const kLocalMaximumRadius = 1;
|
|
function decodeMultiplePoses(scoresBuffer, offsetsBuffer, displacementsFwdBuffer, displacementsBwdBuffer, outputStride, maxPoseDetections, scoreThreshold = 0.5, nmsRadius = 20) {
|
|
const poses = [];
|
|
const queue = buildParts.buildPartWithScoreQueue(scoreThreshold, kLocalMaximumRadius, scoresBuffer);
|
|
const squaredNmsRadius = nmsRadius * nmsRadius;
|
|
while (poses.length < maxPoseDetections && !queue.empty()) {
|
|
const root = queue.dequeue();
|
|
const rootImageCoords = vectors.getImageCoords(root.part, outputStride, offsetsBuffer);
|
|
if (withinNmsRadiusOfCorrespondingPoint(poses, squaredNmsRadius, rootImageCoords, root.part.id))
|
|
continue;
|
|
const keypoints = decodePose.decodePose(root, scoresBuffer, offsetsBuffer, outputStride, displacementsFwdBuffer, displacementsBwdBuffer);
|
|
const score = getInstanceScore(poses, squaredNmsRadius, keypoints);
|
|
poses.push({keypoints, score});
|
|
}
|
|
return poses;
|
|
}
|
|
exports.decodeMultiplePoses = decodeMultiplePoses;
|
|
});
|
|
|
|
// src/posenet/decoders.js
|
|
var require_decoders = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const kpt = require_keypoints2();
|
|
function getPointsConfidence(heatmapScores, heatMapCoords) {
|
|
const numKeypoints = heatMapCoords.shape[0];
|
|
const result = new Float32Array(numKeypoints);
|
|
for (let keypoint = 0; keypoint < numKeypoints; keypoint++) {
|
|
const y = heatMapCoords.get(keypoint, 0);
|
|
const x = heatMapCoords.get(keypoint, 1);
|
|
result[keypoint] = heatmapScores.get(y, x, keypoint);
|
|
}
|
|
return result;
|
|
}
|
|
exports.getPointsConfidence = getPointsConfidence;
|
|
function getOffsetPoint(y, x, keypoint, offsetsBuffer) {
|
|
return {
|
|
y: offsetsBuffer.get(y, x, keypoint),
|
|
x: offsetsBuffer.get(y, x, keypoint + kpt.NUM_KEYPOINTS)
|
|
};
|
|
}
|
|
function getOffsetVectors(heatMapCoordsBuffer, offsetsBuffer) {
|
|
const result = [];
|
|
for (let keypoint = 0; keypoint < kpt.NUM_KEYPOINTS; keypoint++) {
|
|
const heatmapY = heatMapCoordsBuffer.get(keypoint, 0).valueOf();
|
|
const heatmapX = heatMapCoordsBuffer.get(keypoint, 1).valueOf();
|
|
const {x, y} = getOffsetPoint(heatmapY, heatmapX, keypoint, offsetsBuffer);
|
|
result.push(y);
|
|
result.push(x);
|
|
}
|
|
return tf.tensor2d(result, [kpt.NUM_KEYPOINTS, 2]);
|
|
}
|
|
exports.getOffsetVectors = getOffsetVectors;
|
|
function getOffsetPoints(heatMapCoordsBuffer, outputStride, offsetsBuffer) {
|
|
return tf.tidy(() => {
|
|
const offsetVectors = getOffsetVectors(heatMapCoordsBuffer, offsetsBuffer);
|
|
return heatMapCoordsBuffer.toTensor().mul(tf.scalar(outputStride, "int32")).toFloat().add(offsetVectors);
|
|
});
|
|
}
|
|
exports.getOffsetPoints = getOffsetPoints;
|
|
function mod(a, b) {
|
|
return tf.tidy(() => {
|
|
const floored = a.div(tf.scalar(b, "int32"));
|
|
return a.sub(floored.mul(tf.scalar(b, "int32")));
|
|
});
|
|
}
|
|
function argmax2d(inputs) {
|
|
const [height, width, depth] = inputs.shape;
|
|
return tf.tidy(() => {
|
|
const reshaped = inputs.reshape([height * width, depth]);
|
|
const coords = reshaped.argMax(0);
|
|
const yCoords = coords.div(tf.scalar(width, "int32")).expandDims(1);
|
|
const xCoords = mod(coords, width).expandDims(1);
|
|
return tf.concat([yCoords, xCoords], 1);
|
|
});
|
|
}
|
|
exports.argmax2d = argmax2d;
|
|
});
|
|
|
|
// src/posenet/decodeSingle.js
|
|
var require_decodeSingle = __commonJS((exports) => {
|
|
const kpt = require_keypoints2();
|
|
const decoders = require_decoders();
|
|
async function decodeSinglePose(heatmapScores, offsets, outputStride) {
|
|
let totalScore = 0;
|
|
const heatmapValues = decoders.argmax2d(heatmapScores);
|
|
const allTensorBuffers = await Promise.all([heatmapScores.buffer(), offsets.buffer(), heatmapValues.buffer()]);
|
|
const scoresBuffer = allTensorBuffers[0];
|
|
const offsetsBuffer = allTensorBuffers[1];
|
|
const heatmapValuesBuffer = allTensorBuffers[2];
|
|
const offsetPoints = decoders.getOffsetPoints(heatmapValuesBuffer, outputStride, offsetsBuffer);
|
|
const offsetPointsBuffer = await offsetPoints.buffer();
|
|
const keypointConfidence = Array.from(decoders.getPointsConfidence(scoresBuffer, heatmapValuesBuffer));
|
|
const keypoints = keypointConfidence.map((score, keypointId) => {
|
|
totalScore += score;
|
|
return {
|
|
position: {
|
|
y: offsetPointsBuffer.get(keypointId, 0),
|
|
x: offsetPointsBuffer.get(keypointId, 1)
|
|
},
|
|
part: kpt.partNames[keypointId],
|
|
score
|
|
};
|
|
});
|
|
heatmapValues.dispose();
|
|
offsetPoints.dispose();
|
|
return {keypoints, score: totalScore / keypoints.length};
|
|
}
|
|
exports.decodeSinglePose = decodeSinglePose;
|
|
});
|
|
|
|
// src/posenet/util.js
|
|
var require_util2 = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const kpt = require_keypoints2();
|
|
function eitherPointDoesntMeetConfidence(a, b, minConfidence) {
|
|
return a < minConfidence || b < minConfidence;
|
|
}
|
|
function getAdjacentKeyPoints(keypoints, minConfidence) {
|
|
return kpt.connectedPartIndices.reduce((result, [leftJoint, rightJoint]) => {
|
|
if (eitherPointDoesntMeetConfidence(keypoints[leftJoint].score, keypoints[rightJoint].score, minConfidence)) {
|
|
return result;
|
|
}
|
|
result.push([keypoints[leftJoint], keypoints[rightJoint]]);
|
|
return result;
|
|
}, []);
|
|
}
|
|
exports.getAdjacentKeyPoints = getAdjacentKeyPoints;
|
|
const {NEGATIVE_INFINITY, POSITIVE_INFINITY} = Number;
|
|
function getBoundingBox(keypoints) {
|
|
return keypoints.reduce(({maxX, maxY, minX, minY}, {position: {x, y}}) => ({
|
|
maxX: Math.max(maxX, x),
|
|
maxY: Math.max(maxY, y),
|
|
minX: Math.min(minX, x),
|
|
minY: Math.min(minY, y)
|
|
}), {
|
|
maxX: NEGATIVE_INFINITY,
|
|
maxY: NEGATIVE_INFINITY,
|
|
minX: POSITIVE_INFINITY,
|
|
minY: POSITIVE_INFINITY
|
|
});
|
|
}
|
|
exports.getBoundingBox = getBoundingBox;
|
|
function getBoundingBoxPoints(keypoints) {
|
|
const {minX, minY, maxX, maxY} = getBoundingBox(keypoints);
|
|
return [{x: minX, y: minY}, {x: maxX, y: minY}, {x: maxX, y: maxY}, {x: minX, y: maxY}];
|
|
}
|
|
exports.getBoundingBoxPoints = getBoundingBoxPoints;
|
|
async function toTensorBuffers3D(tensors) {
|
|
return Promise.all(tensors.map((tensor) => tensor.buffer()));
|
|
}
|
|
exports.toTensorBuffers3D = toTensorBuffers3D;
|
|
function scalePose(pose, scaleY, scaleX, offsetY = 0, offsetX = 0) {
|
|
return {
|
|
score: pose.score,
|
|
keypoints: pose.keypoints.map(({score, part, position}) => ({
|
|
score,
|
|
part,
|
|
position: {
|
|
x: position.x * scaleX + offsetX,
|
|
y: position.y * scaleY + offsetY
|
|
}
|
|
}))
|
|
};
|
|
}
|
|
exports.scalePose = scalePose;
|
|
function scalePoses(poses, scaleY, scaleX, offsetY = 0, offsetX = 0) {
|
|
if (scaleX === 1 && scaleY === 1 && offsetY === 0 && offsetX === 0) {
|
|
return poses;
|
|
}
|
|
return poses.map((pose) => scalePose(pose, scaleY, scaleX, offsetY, offsetX));
|
|
}
|
|
exports.scalePoses = scalePoses;
|
|
function getInputTensorDimensions(input) {
|
|
return input instanceof tf.Tensor ? [input.shape[0], input.shape[1]] : [input.height, input.width];
|
|
}
|
|
exports.getInputTensorDimensions = getInputTensorDimensions;
|
|
function toInputTensor(input) {
|
|
return input instanceof tf.Tensor ? input : tf.browser.fromPixels(input);
|
|
}
|
|
exports.toInputTensor = toInputTensor;
|
|
function toResizedInputTensor(input, resizeHeight, resizeWidth) {
|
|
return tf.tidy(() => {
|
|
const imageTensor = toInputTensor(input);
|
|
return imageTensor.resizeBilinear([resizeHeight, resizeWidth]);
|
|
});
|
|
}
|
|
exports.toResizedInputTensor = toResizedInputTensor;
|
|
function padAndResizeTo(input, [targetH, targetW]) {
|
|
const [height, width] = getInputTensorDimensions(input);
|
|
const targetAspect = targetW / targetH;
|
|
const aspect = width / height;
|
|
let [padT, padB, padL, padR] = [0, 0, 0, 0];
|
|
if (aspect < targetAspect) {
|
|
padT = 0;
|
|
padB = 0;
|
|
padL = Math.round(0.5 * (targetAspect * height - width));
|
|
padR = Math.round(0.5 * (targetAspect * height - width));
|
|
} else {
|
|
padT = Math.round(0.5 * (1 / targetAspect * width - height));
|
|
padB = Math.round(0.5 * (1 / targetAspect * width - height));
|
|
padL = 0;
|
|
padR = 0;
|
|
}
|
|
const resized = tf.tidy(() => {
|
|
let imageTensor = toInputTensor(input);
|
|
imageTensor = tf.pad3d(imageTensor, [[padT, padB], [padL, padR], [0, 0]]);
|
|
return imageTensor.resizeBilinear([targetH, targetW]);
|
|
});
|
|
return {resized, padding: {top: padT, left: padL, right: padR, bottom: padB}};
|
|
}
|
|
exports.padAndResizeTo = padAndResizeTo;
|
|
function scaleAndFlipPoses(poses, [height, width], [inputResolutionHeight, inputResolutionWidth], padding) {
|
|
const scaleY = (height + padding.top + padding.bottom) / inputResolutionHeight;
|
|
const scaleX = (width + padding.left + padding.right) / inputResolutionWidth;
|
|
const scaledPoses = scalePoses(poses, scaleY, scaleX, -padding.top, -padding.left);
|
|
return scaledPoses;
|
|
}
|
|
exports.scaleAndFlipPoses = scaleAndFlipPoses;
|
|
});
|
|
|
|
// src/posenet/modelPoseNet.js
|
|
var require_modelPoseNet = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const modelMobileNet = require_modelMobileNet();
|
|
const decodeMultiple = require_decodeMultiple();
|
|
const decodeSingle = require_decodeSingle();
|
|
const util = require_util2();
|
|
class PoseNet {
|
|
constructor(net, inputResolution) {
|
|
this.baseModel = net;
|
|
this.inputResolution = inputResolution;
|
|
}
|
|
async estimateMultiplePoses(input, config) {
|
|
const outputStride = this.baseModel.outputStride;
|
|
const inputResolution = this.inputResolution;
|
|
const [height, width] = util.getInputTensorDimensions(input);
|
|
const {resized, padding} = util.padAndResizeTo(input, [inputResolution, inputResolution]);
|
|
const {heatmapScores, offsets, displacementFwd, displacementBwd} = this.baseModel.predict(resized);
|
|
const allTensorBuffers = await util.toTensorBuffers3D([heatmapScores, offsets, displacementFwd, displacementBwd]);
|
|
const scoresBuffer = allTensorBuffers[0];
|
|
const offsetsBuffer = allTensorBuffers[1];
|
|
const displacementsFwdBuffer = allTensorBuffers[2];
|
|
const displacementsBwdBuffer = allTensorBuffers[3];
|
|
const poses = await decodeMultiple.decodeMultiplePoses(scoresBuffer, offsetsBuffer, displacementsFwdBuffer, displacementsBwdBuffer, outputStride, config.maxDetections, config.scoreThreshold, config.nmsRadius);
|
|
const resultPoses = util.scaleAndFlipPoses(poses, [height, width], [inputResolution, inputResolution], padding);
|
|
heatmapScores.dispose();
|
|
offsets.dispose();
|
|
displacementFwd.dispose();
|
|
displacementBwd.dispose();
|
|
resized.dispose();
|
|
return resultPoses;
|
|
}
|
|
async estimateSinglePose(input) {
|
|
const outputStride = this.baseModel.outputStride;
|
|
const inputResolution = this.inputResolution;
|
|
const [height, width] = util.getInputTensorDimensions(input);
|
|
const {resized, padding} = util.padAndResizeTo(input, inputResolution);
|
|
const {heatmapScores, offsets, displacementFwd, displacementBwd} = this.baseModel.predict(resized);
|
|
const pose = await decodeSingle.decodeSinglePose(heatmapScores, offsets, outputStride);
|
|
const poses = [pose];
|
|
const resultPoses = util.scaleAndFlipPoses(poses, [height, width], [inputResolution, inputResolution], padding);
|
|
heatmapScores.dispose();
|
|
offsets.dispose();
|
|
displacementFwd.dispose();
|
|
displacementBwd.dispose();
|
|
resized.dispose();
|
|
return resultPoses[0];
|
|
}
|
|
dispose() {
|
|
this.baseModel.dispose();
|
|
}
|
|
}
|
|
exports.PoseNet = PoseNet;
|
|
async function loadMobileNet(config) {
|
|
const outputStride = config.outputStride;
|
|
const graphModel = await tf.loadGraphModel(config.modelPath);
|
|
const mobilenet = new modelMobileNet.MobileNet(graphModel, outputStride);
|
|
return new PoseNet(mobilenet, config.inputResolution);
|
|
}
|
|
async function load(config) {
|
|
return loadMobileNet(config);
|
|
}
|
|
exports.load = load;
|
|
});
|
|
|
|
// src/posenet/index.js
|
|
var require_posenet = __commonJS((exports) => {
|
|
const modelMobileNet = require_modelMobileNet();
|
|
const modelPoseNet = require_modelPoseNet();
|
|
const decodeMultiple = require_decodeMultiple();
|
|
const decodeSingle = require_decodeSingle();
|
|
const keypoints = require_keypoints2();
|
|
const util = require_util2();
|
|
exports.load = modelPoseNet.load;
|
|
exports.PoseNet = modelPoseNet.PoseNet;
|
|
exports.MobileNet = modelMobileNet.MobileNet;
|
|
exports.decodeMultiplePoses = decodeMultiple.decodeMultiplePoses;
|
|
exports.decodeSinglePose = decodeSingle.decodeSinglePose;
|
|
exports.partChannels = keypoints.partChannels;
|
|
exports.partIds = keypoints.partIds;
|
|
exports.partNames = keypoints.partNames;
|
|
exports.poseChain = keypoints.poseChain;
|
|
exports.getAdjacentKeyPoints = util.getAdjacentKeyPoints;
|
|
exports.getBoundingBox = util.getBoundingBox;
|
|
exports.getBoundingBoxPoints = util.getBoundingBoxPoints;
|
|
exports.scaleAndFlipPoses = util.scaleAndFlipPoses;
|
|
exports.scalePose = util.scalePose;
|
|
});
|
|
|
|
// src/handpose/box.js
|
|
var require_box3 = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
function getBoxSize(box) {
|
|
return [
|
|
Math.abs(box.endPoint[0] - box.startPoint[0]),
|
|
Math.abs(box.endPoint[1] - box.startPoint[1])
|
|
];
|
|
}
|
|
exports.getBoxSize = getBoxSize;
|
|
function getBoxCenter(box) {
|
|
return [
|
|
box.startPoint[0] + (box.endPoint[0] - box.startPoint[0]) / 2,
|
|
box.startPoint[1] + (box.endPoint[1] - box.startPoint[1]) / 2
|
|
];
|
|
}
|
|
exports.getBoxCenter = getBoxCenter;
|
|
function cutBoxFromImageAndResize(box, image, cropSize) {
|
|
const h = image.shape[1];
|
|
const w = image.shape[2];
|
|
const boxes = [[
|
|
box.startPoint[1] / h,
|
|
box.startPoint[0] / w,
|
|
box.endPoint[1] / h,
|
|
box.endPoint[0] / w
|
|
]];
|
|
return tf.image.cropAndResize(image, boxes, [0], cropSize);
|
|
}
|
|
exports.cutBoxFromImageAndResize = cutBoxFromImageAndResize;
|
|
function scaleBoxCoordinates(box, factor) {
|
|
const startPoint = [box.startPoint[0] * factor[0], box.startPoint[1] * factor[1]];
|
|
const endPoint = [box.endPoint[0] * factor[0], box.endPoint[1] * factor[1]];
|
|
const palmLandmarks = box.palmLandmarks.map((coord) => {
|
|
const scaledCoord = [coord[0] * factor[0], coord[1] * factor[1]];
|
|
return scaledCoord;
|
|
});
|
|
return {startPoint, endPoint, palmLandmarks};
|
|
}
|
|
exports.scaleBoxCoordinates = scaleBoxCoordinates;
|
|
function enlargeBox(box, factor = 1.5) {
|
|
const center = getBoxCenter(box);
|
|
const size = getBoxSize(box);
|
|
const newHalfSize = [factor * size[0] / 2, factor * size[1] / 2];
|
|
const startPoint = [center[0] - newHalfSize[0], center[1] - newHalfSize[1]];
|
|
const endPoint = [center[0] + newHalfSize[0], center[1] + newHalfSize[1]];
|
|
return {startPoint, endPoint, palmLandmarks: box.palmLandmarks};
|
|
}
|
|
exports.enlargeBox = enlargeBox;
|
|
function squarifyBox(box) {
|
|
const centers = getBoxCenter(box);
|
|
const size = getBoxSize(box);
|
|
const maxEdge = Math.max(...size);
|
|
const halfSize = maxEdge / 2;
|
|
const startPoint = [centers[0] - halfSize, centers[1] - halfSize];
|
|
const endPoint = [centers[0] + halfSize, centers[1] + halfSize];
|
|
return {startPoint, endPoint, palmLandmarks: box.palmLandmarks};
|
|
}
|
|
exports.squarifyBox = squarifyBox;
|
|
function shiftBox(box, shiftFactor) {
|
|
const boxSize = [
|
|
box.endPoint[0] - box.startPoint[0],
|
|
box.endPoint[1] - box.startPoint[1]
|
|
];
|
|
const shiftVector = [boxSize[0] * shiftFactor[0], boxSize[1] * shiftFactor[1]];
|
|
const startPoint = [box.startPoint[0] + shiftVector[0], box.startPoint[1] + shiftVector[1]];
|
|
const endPoint = [box.endPoint[0] + shiftVector[0], box.endPoint[1] + shiftVector[1]];
|
|
return {startPoint, endPoint, palmLandmarks: box.palmLandmarks};
|
|
}
|
|
exports.shiftBox = shiftBox;
|
|
});
|
|
|
|
// src/handpose/hand.js
|
|
var require_hand = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const bounding = require_box3();
|
|
class HandDetector {
|
|
constructor(model, width, height, anchors, iouThreshold, scoreThreshold) {
|
|
this.model = model;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.iouThreshold = iouThreshold;
|
|
this.scoreThreshold = scoreThreshold;
|
|
this.anchors = anchors.map((anchor) => [anchor.x_center, anchor.y_center]);
|
|
this.anchorsTensor = tf.tensor2d(this.anchors);
|
|
this.inputSizeTensor = tf.tensor1d([width, height]);
|
|
this.doubleInputSizeTensor = tf.tensor1d([width * 2, height * 2]);
|
|
}
|
|
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(() => {
|
|
const landmarks = tf.add(tf.div(rawPalmLandmarks.reshape([-1, 7, 2]), this.inputSizeTensor), this.anchors[index]);
|
|
return tf.mul(landmarks, this.inputSizeTensor);
|
|
});
|
|
}
|
|
async getBoundingBoxes(input) {
|
|
const normalizedInput = tf.tidy(() => tf.mul(tf.sub(input, 0.5), 2));
|
|
let batchedPrediction;
|
|
if (tf.getBackend() === "webgl") {
|
|
const savedWebglPackDepthwiseConvFlag = tf.env().get("WEBGL_PACK_DEPTHWISECONV");
|
|
tf.env().set("WEBGL_PACK_DEPTHWISECONV", true);
|
|
batchedPrediction = this.model.predict(normalizedInput);
|
|
tf.env().set("WEBGL_PACK_DEPTHWISECONV", savedWebglPackDepthwiseConvFlag);
|
|
} else {
|
|
batchedPrediction = this.model.predict(normalizedInput);
|
|
}
|
|
const prediction = batchedPrediction.squeeze();
|
|
const scores = tf.tidy(() => tf.sigmoid(tf.slice(prediction, [0, 0], [-1, 1])).squeeze());
|
|
const rawBoxes = tf.slice(prediction, [0, 1], [-1, 4]);
|
|
const boxes = this.normalizeBoxes(rawBoxes);
|
|
const boxesWithHandsTensor = await tf.image.nonMaxSuppressionAsync(boxes, scores, 1, this.iouThreshold, this.scoreThreshold);
|
|
const boxesWithHands = await boxesWithHandsTensor.array();
|
|
const toDispose = [
|
|
normalizedInput,
|
|
batchedPrediction,
|
|
boxesWithHandsTensor,
|
|
prediction,
|
|
boxes,
|
|
rawBoxes,
|
|
scores
|
|
];
|
|
if (boxesWithHands.length === 0) {
|
|
toDispose.forEach((tensor) => tensor.dispose());
|
|
return null;
|
|
}
|
|
const boxIndex = boxesWithHands[0];
|
|
const matchingBox = tf.slice(boxes, [boxIndex, 0], [1, -1]);
|
|
const rawPalmLandmarks = tf.slice(prediction, [boxIndex, 5], [1, 14]);
|
|
const palmLandmarks = tf.tidy(() => this.normalizeLandmarks(rawPalmLandmarks, boxIndex).reshape([
|
|
-1,
|
|
2
|
|
]));
|
|
toDispose.push(rawPalmLandmarks);
|
|
toDispose.forEach((tensor) => tensor.dispose());
|
|
return {boxes: matchingBox, palmLandmarks};
|
|
}
|
|
async estimateHandBounds(input) {
|
|
const inputHeight = input.shape[1];
|
|
const inputWidth = input.shape[2];
|
|
const image = tf.tidy(() => input.resizeBilinear([this.width, this.height]).div(255));
|
|
const prediction = await this.getBoundingBoxes(image);
|
|
if (prediction === null) {
|
|
image.dispose();
|
|
return null;
|
|
}
|
|
const boundingBoxes = prediction.boxes.arraySync();
|
|
const startPoint = boundingBoxes[0].slice(0, 2);
|
|
const endPoint = boundingBoxes[0].slice(2, 4);
|
|
const palmLandmarks = prediction.palmLandmarks.arraySync();
|
|
image.dispose();
|
|
prediction.boxes.dispose();
|
|
prediction.palmLandmarks.dispose();
|
|
return bounding.scaleBoxCoordinates({startPoint, endPoint, palmLandmarks}, [inputWidth / this.width, inputHeight / this.height]);
|
|
}
|
|
}
|
|
exports.HandDetector = HandDetector;
|
|
});
|
|
|
|
// src/handpose/keypoints.js
|
|
var require_keypoints3 = __commonJS((exports) => {
|
|
exports.MESH_ANNOTATIONS = {
|
|
thumb: [1, 2, 3, 4],
|
|
indexFinger: [5, 6, 7, 8],
|
|
middleFinger: [9, 10, 11, 12],
|
|
ringFinger: [13, 14, 15, 16],
|
|
pinky: [17, 18, 19, 20],
|
|
palmBase: [0]
|
|
};
|
|
});
|
|
|
|
// src/handpose/util.js
|
|
var require_util3 = __commonJS((exports) => {
|
|
function normalizeRadians(angle) {
|
|
return angle - 2 * Math.PI * Math.floor((angle + Math.PI) / (2 * Math.PI));
|
|
}
|
|
exports.normalizeRadians = normalizeRadians;
|
|
function computeRotation(point1, point2) {
|
|
const radians = Math.PI / 2 - Math.atan2(-(point2[1] - point1[1]), point2[0] - point1[0]);
|
|
return normalizeRadians(radians);
|
|
}
|
|
exports.computeRotation = computeRotation;
|
|
const buildTranslationMatrix = (x, y) => [[1, 0, x], [0, 1, y], [0, 0, 1]];
|
|
function dot(v1, v2) {
|
|
let product = 0;
|
|
for (let i = 0; i < v1.length; i++) {
|
|
product += v1[i] * v2[i];
|
|
}
|
|
return product;
|
|
}
|
|
exports.dot = dot;
|
|
function getColumnFrom2DArr(arr, columnIndex) {
|
|
const column = [];
|
|
for (let i = 0; i < arr.length; i++) {
|
|
column.push(arr[i][columnIndex]);
|
|
}
|
|
return column;
|
|
}
|
|
exports.getColumnFrom2DArr = getColumnFrom2DArr;
|
|
function multiplyTransformMatrices(mat1, mat2) {
|
|
const product = [];
|
|
const size = mat1.length;
|
|
for (let row = 0; row < size; row++) {
|
|
product.push([]);
|
|
for (let col = 0; col < size; col++) {
|
|
product[row].push(dot(mat1[row], getColumnFrom2DArr(mat2, col)));
|
|
}
|
|
}
|
|
return product;
|
|
}
|
|
function buildRotationMatrix(rotation, center) {
|
|
const cosA = Math.cos(rotation);
|
|
const sinA = Math.sin(rotation);
|
|
const rotationMatrix = [[cosA, -sinA, 0], [sinA, cosA, 0], [0, 0, 1]];
|
|
const translationMatrix = buildTranslationMatrix(center[0], center[1]);
|
|
const translationTimesRotation = multiplyTransformMatrices(translationMatrix, rotationMatrix);
|
|
const negativeTranslationMatrix = buildTranslationMatrix(-center[0], -center[1]);
|
|
return multiplyTransformMatrices(translationTimesRotation, negativeTranslationMatrix);
|
|
}
|
|
exports.buildRotationMatrix = buildRotationMatrix;
|
|
function invertTransformMatrix(matrix) {
|
|
const rotationComponent = [[matrix[0][0], matrix[1][0]], [matrix[0][1], matrix[1][1]]];
|
|
const translationComponent = [matrix[0][2], matrix[1][2]];
|
|
const invertedTranslation = [
|
|
-dot(rotationComponent[0], translationComponent),
|
|
-dot(rotationComponent[1], translationComponent)
|
|
];
|
|
return [
|
|
rotationComponent[0].concat(invertedTranslation[0]),
|
|
rotationComponent[1].concat(invertedTranslation[1]),
|
|
[0, 0, 1]
|
|
];
|
|
}
|
|
exports.invertTransformMatrix = invertTransformMatrix;
|
|
function rotatePoint(homogeneousCoordinate, rotationMatrix) {
|
|
return [
|
|
dot(homogeneousCoordinate, rotationMatrix[0]),
|
|
dot(homogeneousCoordinate, rotationMatrix[1])
|
|
];
|
|
}
|
|
exports.rotatePoint = rotatePoint;
|
|
});
|
|
|
|
// src/handpose/pipeline.js
|
|
var require_pipeline2 = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const bounding = require_box3();
|
|
const util = require_util3();
|
|
const UPDATE_REGION_OF_INTEREST_IOU_THRESHOLD = 0.8;
|
|
const PALM_BOX_SHIFT_VECTOR = [0, -0.4];
|
|
const PALM_BOX_ENLARGE_FACTOR = 3;
|
|
const HAND_BOX_SHIFT_VECTOR = [0, -0.1];
|
|
const HAND_BOX_ENLARGE_FACTOR = 1.65;
|
|
const PALM_LANDMARK_IDS = [0, 5, 9, 13, 17, 1, 2];
|
|
const PALM_LANDMARKS_INDEX_OF_PALM_BASE = 0;
|
|
const PALM_LANDMARKS_INDEX_OF_MIDDLE_FINGER_BASE = 2;
|
|
class HandPipeline {
|
|
constructor(boundingBoxDetector, meshDetector, meshWidth, meshHeight, maxContinuousChecks, detectionConfidence) {
|
|
this.regionsOfInterest = [];
|
|
this.runsWithoutHandDetector = 0;
|
|
this.boundingBoxDetector = boundingBoxDetector;
|
|
this.meshDetector = meshDetector;
|
|
this.maxContinuousChecks = maxContinuousChecks;
|
|
this.detectionConfidence = detectionConfidence;
|
|
this.meshWidth = meshWidth;
|
|
this.meshHeight = meshHeight;
|
|
this.maxHandsNumber = 1;
|
|
}
|
|
getBoxForPalmLandmarks(palmLandmarks, rotationMatrix) {
|
|
const rotatedPalmLandmarks = palmLandmarks.map((coord) => {
|
|
const homogeneousCoordinate = [...coord, 1];
|
|
return util.rotatePoint(homogeneousCoordinate, rotationMatrix);
|
|
});
|
|
const boxAroundPalm = this.calculateLandmarksBoundingBox(rotatedPalmLandmarks);
|
|
return bounding.enlargeBox(bounding.squarifyBox(bounding.shiftBox(boxAroundPalm, PALM_BOX_SHIFT_VECTOR)), PALM_BOX_ENLARGE_FACTOR);
|
|
}
|
|
getBoxForHandLandmarks(landmarks) {
|
|
const boundingBox = this.calculateLandmarksBoundingBox(landmarks);
|
|
const boxAroundHand = bounding.enlargeBox(bounding.squarifyBox(bounding.shiftBox(boundingBox, HAND_BOX_SHIFT_VECTOR)), HAND_BOX_ENLARGE_FACTOR);
|
|
const palmLandmarks = [];
|
|
for (let i = 0; i < PALM_LANDMARK_IDS.length; i++) {
|
|
palmLandmarks.push(landmarks[PALM_LANDMARK_IDS[i]].slice(0, 2));
|
|
}
|
|
boxAroundHand.palmLandmarks = palmLandmarks;
|
|
return boxAroundHand;
|
|
}
|
|
transformRawCoords(rawCoords, box, angle, rotationMatrix) {
|
|
const boxSize = bounding.getBoxSize(box);
|
|
const scaleFactor = [boxSize[0] / this.meshWidth, boxSize[1] / this.meshHeight];
|
|
const coordsScaled = rawCoords.map((coord) => [
|
|
scaleFactor[0] * (coord[0] - this.meshWidth / 2),
|
|
scaleFactor[1] * (coord[1] - this.meshHeight / 2),
|
|
coord[2]
|
|
]);
|
|
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);
|
|
const boxCenter = [...bounding.getBoxCenter(box), 1];
|
|
const originalBoxCenter = [
|
|
util.dot(boxCenter, inverseRotationMatrix[0]),
|
|
util.dot(boxCenter, inverseRotationMatrix[1])
|
|
];
|
|
return coordsRotated.map((coord) => [
|
|
coord[0] + originalBoxCenter[0],
|
|
coord[1] + originalBoxCenter[1],
|
|
coord[2]
|
|
]);
|
|
}
|
|
async estimateHand(image, config) {
|
|
const useFreshBox = this.shouldUpdateRegionsOfInterest();
|
|
if (useFreshBox === true) {
|
|
const boundingBoxPrediction = await this.boundingBoxDetector.estimateHandBounds(image);
|
|
if (boundingBoxPrediction === null) {
|
|
image.dispose();
|
|
this.regionsOfInterest = [];
|
|
return null;
|
|
}
|
|
this.updateRegionsOfInterest(boundingBoxPrediction, true);
|
|
this.runsWithoutHandDetector = 0;
|
|
} else {
|
|
this.runsWithoutHandDetector++;
|
|
}
|
|
const currentBox = this.regionsOfInterest[0];
|
|
const angle = util.computeRotation(currentBox.palmLandmarks[PALM_LANDMARKS_INDEX_OF_PALM_BASE], currentBox.palmLandmarks[PALM_LANDMARKS_INDEX_OF_MIDDLE_FINGER_BASE]);
|
|
const palmCenter = bounding.getBoxCenter(currentBox);
|
|
const palmCenterNormalized = [palmCenter[0] / image.shape[2], palmCenter[1] / image.shape[1]];
|
|
const rotatedImage = tf.image.rotateWithOffset(image, angle, 0, palmCenterNormalized);
|
|
const rotationMatrix = util.buildRotationMatrix(-angle, palmCenter);
|
|
const box = useFreshBox ? this.getBoxForPalmLandmarks(currentBox.palmLandmarks, rotationMatrix) : currentBox;
|
|
const croppedInput = bounding.cutBoxFromImageAndResize(box, rotatedImage, [this.meshWidth, this.meshHeight]);
|
|
const handImage = croppedInput.div(255);
|
|
croppedInput.dispose();
|
|
rotatedImage.dispose();
|
|
let prediction;
|
|
if (tf.getBackend() === "webgl") {
|
|
const savedWebglPackDepthwiseConvFlag = tf.env().get("WEBGL_PACK_DEPTHWISECONV");
|
|
tf.env().set("WEBGL_PACK_DEPTHWISECONV", true);
|
|
prediction = this.meshDetector.predict(handImage);
|
|
tf.env().set("WEBGL_PACK_DEPTHWISECONV", savedWebglPackDepthwiseConvFlag);
|
|
} else {
|
|
prediction = this.meshDetector.predict(handImage);
|
|
}
|
|
const [flag, keypoints] = prediction;
|
|
handImage.dispose();
|
|
const flagValue = flag.dataSync()[0];
|
|
flag.dispose();
|
|
if (flagValue < config.minConfidence) {
|
|
keypoints.dispose();
|
|
this.regionsOfInterest = [];
|
|
return null;
|
|
}
|
|
const keypointsReshaped = tf.reshape(keypoints, [-1, 3]);
|
|
const rawCoords = keypointsReshaped.arraySync();
|
|
keypoints.dispose();
|
|
keypointsReshaped.dispose();
|
|
const coords = this.transformRawCoords(rawCoords, box, angle, rotationMatrix);
|
|
const nextBoundingBox = this.getBoxForHandLandmarks(coords);
|
|
this.updateRegionsOfInterest(nextBoundingBox, false);
|
|
const result = {
|
|
landmarks: coords,
|
|
confidence: flagValue,
|
|
box: {
|
|
topLeft: nextBoundingBox.startPoint,
|
|
bottomRight: nextBoundingBox.endPoint
|
|
}
|
|
};
|
|
return result;
|
|
}
|
|
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};
|
|
}
|
|
updateRegionsOfInterest(box, forceUpdate) {
|
|
if (forceUpdate) {
|
|
this.regionsOfInterest = [box];
|
|
} else {
|
|
const previousBox = this.regionsOfInterest[0];
|
|
let iou = 0;
|
|
if (previousBox != null && previousBox.startPoint != null) {
|
|
const [boxStartX, boxStartY] = box.startPoint;
|
|
const [boxEndX, boxEndY] = box.endPoint;
|
|
const [previousBoxStartX, previousBoxStartY] = previousBox.startPoint;
|
|
const [previousBoxEndX, previousBoxEndY] = previousBox.endPoint;
|
|
const xStartMax = Math.max(boxStartX, previousBoxStartX);
|
|
const yStartMax = Math.max(boxStartY, previousBoxStartY);
|
|
const xEndMin = Math.min(boxEndX, previousBoxEndX);
|
|
const yEndMin = Math.min(boxEndY, previousBoxEndY);
|
|
const intersection = (xEndMin - xStartMax) * (yEndMin - yStartMax);
|
|
const boxArea = (boxEndX - boxStartX) * (boxEndY - boxStartY);
|
|
const previousBoxArea = (previousBoxEndX - previousBoxStartX) * (previousBoxEndY - boxStartY);
|
|
iou = intersection / (boxArea + previousBoxArea - intersection);
|
|
}
|
|
this.regionsOfInterest[0] = iou > UPDATE_REGION_OF_INTEREST_IOU_THRESHOLD ? previousBox : box;
|
|
}
|
|
}
|
|
shouldUpdateRegionsOfInterest() {
|
|
const roisCount = this.regionsOfInterest.length;
|
|
return roisCount !== this.maxHandsNumber || this.runsWithoutHandDetector >= this.maxContinuousChecks;
|
|
}
|
|
}
|
|
exports.HandPipeline = HandPipeline;
|
|
});
|
|
|
|
// src/handpose/index.js
|
|
var require_handpose = __commonJS((exports) => {
|
|
const tf = require("@tensorflow/tfjs");
|
|
const hand = require_hand();
|
|
const keypoints = require_keypoints3();
|
|
const pipe = require_pipeline2();
|
|
async function loadHandDetectorModel(url) {
|
|
return tf.loadGraphModel(url, {fromTFHub: url.includes("tfhub.dev")});
|
|
}
|
|
async function loadHandPoseModel(url) {
|
|
return tf.loadGraphModel(url, {fromTFHub: url.includes("tfhub.dev")});
|
|
}
|
|
async function loadAnchors(url) {
|
|
return tf.util.fetch(url).then((d) => d.json());
|
|
}
|
|
async function load(config) {
|
|
const [ANCHORS, handDetectorModel, handPoseModel] = await Promise.all([
|
|
loadAnchors(config.detector.anchors),
|
|
loadHandDetectorModel(config.detector.modelPath),
|
|
loadHandPoseModel(config.skeleton.modelPath)
|
|
]);
|
|
const detector = new hand.HandDetector(handDetectorModel, config.inputSize, config.inputSize, ANCHORS, config.iouThreshold, config.scoreThreshold);
|
|
const pipeline = new pipe.HandPipeline(detector, handPoseModel, config.inputSize, config.inputSize, config.skipFrames, config.minConfidence);
|
|
const handpose = new HandPose(pipeline);
|
|
return handpose;
|
|
}
|
|
exports.load = load;
|
|
class HandPose {
|
|
constructor(pipeline) {
|
|
this.pipeline = pipeline;
|
|
}
|
|
static getAnnotations() {
|
|
return keypoints.MESH_ANNOTATIONS;
|
|
}
|
|
async estimateHands(input, config) {
|
|
const image = tf.tidy(() => {
|
|
if (!(input instanceof tf.Tensor)) {
|
|
input = tf.browser.fromPixels(input);
|
|
}
|
|
return input.toFloat().expandDims(0);
|
|
});
|
|
const prediction = await this.pipeline.estimateHand(image, config);
|
|
image.dispose();
|
|
if (!prediction)
|
|
return [];
|
|
const annotations = {};
|
|
for (const key of Object.keys(keypoints.MESH_ANNOTATIONS)) {
|
|
annotations[key] = keypoints.MESH_ANNOTATIONS[key].map((index) => prediction.landmarks[index]);
|
|
}
|
|
return [{
|
|
confidence: prediction.confidence || 0,
|
|
box: prediction.box ? [prediction.box.topLeft[0], prediction.box.topLeft[1], prediction.box.bottomRight[0] - prediction.box.topLeft[0], prediction.box.bottomRight[1] - prediction.box.topLeft[1]] : 0,
|
|
landmarks: prediction.landmarks,
|
|
annotations
|
|
}];
|
|
}
|
|
}
|
|
exports.HandPose = HandPose;
|
|
});
|
|
|
|
// src/config.js
|
|
var require_config = __commonJS((exports) => {
|
|
__export(exports, {
|
|
default: () => config_default
|
|
});
|
|
var config_default = {
|
|
face: {
|
|
enabled: true,
|
|
detector: {
|
|
modelPath: "/models/blazeface/model.json",
|
|
inputSize: 128,
|
|
maxFaces: 10,
|
|
skipFrames: 5,
|
|
minConfidence: 0.8,
|
|
iouThreshold: 0.3,
|
|
scoreThreshold: 0.75
|
|
},
|
|
mesh: {
|
|
enabled: true,
|
|
modelPath: "/models/facemesh/model.json",
|
|
inputSize: 192
|
|
},
|
|
iris: {
|
|
enabled: true,
|
|
modelPath: "/models/iris/model.json",
|
|
inputSize: 192
|
|
},
|
|
age: {
|
|
enabled: true,
|
|
modelPath: "/models/ssrnet-age/imdb/model.json",
|
|
inputSize: 64,
|
|
skipFrames: 5
|
|
},
|
|
gender: {
|
|
enabled: true,
|
|
modelPath: "/models/ssrnet-gender/imdb/model.json"
|
|
}
|
|
},
|
|
body: {
|
|
enabled: true,
|
|
modelPath: "/models/posenet/model.json",
|
|
inputResolution: 257,
|
|
outputStride: 16,
|
|
maxDetections: 5,
|
|
scoreThreshold: 0.75,
|
|
nmsRadius: 20
|
|
},
|
|
hand: {
|
|
enabled: true,
|
|
inputSize: 256,
|
|
skipFrames: 5,
|
|
minConfidence: 0.8,
|
|
iouThreshold: 0.3,
|
|
scoreThreshold: 0.75,
|
|
detector: {
|
|
anchors: "/models/handdetect/anchors.json",
|
|
modelPath: "/models/handdetect/model.json"
|
|
},
|
|
skeleton: {
|
|
modelPath: "/models/handskeleton/model.json"
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
// src/index.js
|
|
var require_src = __commonJS((exports) => {
|
|
const facemesh = require_facemesh();
|
|
const ssrnet = require_ssrnet();
|
|
const posenet = require_posenet();
|
|
const handpose = require_handpose();
|
|
const defaults = require_config().default;
|
|
const models = {
|
|
facemesh: null,
|
|
blazeface: null,
|
|
ssrnet: null,
|
|
iris: null
|
|
};
|
|
function mergeDeep(...objects) {
|
|
const isObject = (obj) => obj && typeof obj === "object";
|
|
return objects.reduce((prev, obj) => {
|
|
Object.keys(obj || {}).forEach((key) => {
|
|
const pVal = prev[key];
|
|
const oVal = obj[key];
|
|
if (Array.isArray(pVal) && Array.isArray(oVal)) {
|
|
prev[key] = pVal.concat(...oVal);
|
|
} else if (isObject(pVal) && isObject(oVal)) {
|
|
prev[key] = mergeDeep(pVal, oVal);
|
|
} else {
|
|
prev[key] = oVal;
|
|
}
|
|
});
|
|
return prev;
|
|
}, {});
|
|
}
|
|
async function detect(input, userConfig) {
|
|
const config = mergeDeep(defaults, userConfig);
|
|
let poseRes = [];
|
|
if (config.body.enabled) {
|
|
if (!models.posenet)
|
|
models.posenet = await posenet.load(config.body);
|
|
poseRes = await models.posenet.estimateMultiplePoses(input, config.body);
|
|
}
|
|
let handRes = [];
|
|
if (config.hand.enabled) {
|
|
if (!models.handpose)
|
|
models.handpose = await handpose.load(config.hand);
|
|
handRes = await models.handpose.estimateHands(input, config.hand);
|
|
}
|
|
const faceRes = [];
|
|
if (config.face.enabled) {
|
|
if (!models.facemesh)
|
|
models.facemesh = await facemesh.load(config.face);
|
|
const faces = await models.facemesh.estimateFaces(input, config.face);
|
|
for (const face of faces) {
|
|
const ssrdata = config.face.age.enabled || config.face.gender.enabled ? await ssrnet.predict(face.image, config) : {};
|
|
const iris = face.annotations.leftEyeIris && face.annotations.rightEyeIris ? Math.max(face.annotations.leftEyeIris[3][0] - face.annotations.leftEyeIris[1][0], face.annotations.rightEyeIris[3][0] - face.annotations.rightEyeIris[1][0]) : 0;
|
|
faceRes.push({
|
|
confidence: face.confidence,
|
|
box: face.box,
|
|
mesh: face.mesh,
|
|
annotations: face.annotations,
|
|
age: ssrdata.age,
|
|
gender: ssrdata.gender,
|
|
iris: iris !== 0 ? Math.trunc(100 * 11.7 / iris) / 100 : 0
|
|
});
|
|
}
|
|
}
|
|
return {face: faceRes, body: poseRes, hand: handRes};
|
|
}
|
|
exports.detect = detect;
|
|
exports.defaults = defaults;
|
|
exports.models = models;
|
|
exports.facemesh = facemesh;
|
|
exports.ssrnet = ssrnet;
|
|
exports.posenet = posenet;
|
|
exports.handpose = handpose;
|
|
});
|
|
export default require_src();
|
|
//# sourceMappingURL=human.esm.js.map
|