2020-10-12 01:22:43 +02:00
|
|
|
const tf = require('@tensorflow/tfjs');
|
|
|
|
|
|
|
|
const ANCHORS_CONFIG = {
|
|
|
|
strides: [8, 16],
|
|
|
|
anchors: [2, 6],
|
|
|
|
};
|
2020-10-13 04:01:35 +02:00
|
|
|
|
2020-10-12 01:22:43 +02:00
|
|
|
const NUM_LANDMARKS = 6;
|
2020-10-16 02:20:37 +02:00
|
|
|
function generateAnchors(anchorSize, outputSpec) {
|
2020-10-12 01:22:43 +02:00
|
|
|
const anchors = [];
|
|
|
|
for (let i = 0; i < outputSpec.strides.length; i++) {
|
|
|
|
const stride = outputSpec.strides[i];
|
2020-10-16 02:20:37 +02:00
|
|
|
const gridRows = Math.floor((anchorSize + stride - 1) / stride);
|
|
|
|
const gridCols = Math.floor((anchorSize + stride - 1) / stride);
|
2020-10-12 01:22:43 +02:00
|
|
|
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;
|
|
|
|
}
|
2020-10-13 04:01:35 +02:00
|
|
|
|
|
|
|
const disposeBox = (box) => {
|
|
|
|
box.startEndTensor.dispose();
|
|
|
|
box.startPoint.dispose();
|
|
|
|
box.endPoint.dispose();
|
|
|
|
};
|
|
|
|
|
|
|
|
const createBox = (startEndTensor) => ({
|
|
|
|
startEndTensor,
|
|
|
|
startPoint: tf.slice(startEndTensor, [0, 0], [-1, 2]),
|
|
|
|
endPoint: tf.slice(startEndTensor, [0, 2], [-1, 2]),
|
|
|
|
});
|
|
|
|
|
|
|
|
const 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 createBox(newCoordinates);
|
|
|
|
};
|
|
|
|
|
2020-10-12 01:22:43 +02:00
|
|
|
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);
|
|
|
|
}
|
2020-10-13 04:01:35 +02:00
|
|
|
|
2020-10-12 01:22:43 +02:00
|
|
|
function scaleBoxFromPrediction(face, scaleFactor) {
|
|
|
|
return tf.tidy(() => {
|
|
|
|
const box = face['box'] ? face['box'] : face;
|
2020-10-13 04:01:35 +02:00
|
|
|
return scaleBox(box, scaleFactor).startEndTensor.squeeze();
|
2020-10-12 01:22:43 +02:00
|
|
|
});
|
|
|
|
}
|
2020-10-13 04:01:35 +02:00
|
|
|
|
2020-10-12 01:22:43 +02:00
|
|
|
class BlazeFaceModel {
|
|
|
|
constructor(model, config) {
|
|
|
|
this.blazeFaceModel = model;
|
|
|
|
this.width = config.detector.inputSize;
|
|
|
|
this.height = config.detector.inputSize;
|
2020-10-16 02:20:37 +02:00
|
|
|
this.anchorSize = config.detector.anchorSize;
|
2020-10-12 01:22:43 +02:00
|
|
|
this.maxFaces = config.detector.maxFaces;
|
2020-10-16 02:20:37 +02:00
|
|
|
this.anchorsData = generateAnchors(config.detector.anchorSize, ANCHORS_CONFIG);
|
2020-10-12 01:22:43 +02:00
|
|
|
this.anchors = tf.tensor2d(this.anchorsData);
|
2020-10-16 02:20:37 +02:00
|
|
|
this.inputSize = tf.tensor1d([this.width, this.height]);
|
2020-10-12 01:22:43 +02:00
|
|
|
this.iouThreshold = config.detector.iouThreshold;
|
2020-10-16 00:16:05 +02:00
|
|
|
this.scaleFaces = 0.8;
|
2020-10-12 01:22:43 +02:00
|
|
|
this.scoreThreshold = config.detector.scoreThreshold;
|
|
|
|
}
|
|
|
|
|
2020-10-13 04:01:35 +02:00
|
|
|
async getBoundingBoxes(inputImage) {
|
2020-10-12 01:22:43 +02:00
|
|
|
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);
|
2020-10-16 02:20:37 +02:00
|
|
|
let prediction;
|
|
|
|
// are we using tfhub or pinto converted model?
|
|
|
|
if (Array.isArray(batchedPrediction)) {
|
|
|
|
const sorted = batchedPrediction.sort((a, b) => a.size - b.size);
|
|
|
|
const concat384 = tf.concat([sorted[0], sorted[2]], 2); // dim: 384, 1 + 16
|
|
|
|
const concat512 = tf.concat([sorted[1], sorted[3]], 2); // dim: 512, 1 + 16
|
|
|
|
const concat = tf.concat([concat512, concat384], 1);
|
|
|
|
prediction = concat.squeeze(0);
|
|
|
|
} else {
|
|
|
|
prediction = batchedPrediction.squeeze(); // when using tfhub model
|
|
|
|
}
|
2020-10-12 01:22:43 +02:00
|
|
|
const decodedBounds = decodeBounds(prediction, this.anchors, this.inputSize);
|
|
|
|
const logits = tf.slice(prediction, [0, 0], [-1, 1]);
|
|
|
|
const scoresOut = tf.sigmoid(logits).squeeze();
|
2020-10-16 02:20:37 +02:00
|
|
|
// console.log(prediction, decodedBounds, logits, scoresOut);
|
2020-10-12 01:22:43 +02:00
|
|
|
return [prediction, decodedBounds, scoresOut];
|
|
|
|
});
|
2020-10-13 04:01:35 +02:00
|
|
|
|
2020-10-12 01:22:43 +02:00
|
|
|
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]));
|
2020-10-13 04:01:35 +02:00
|
|
|
boundingBoxes = await Promise.all(boundingBoxes.map(async (boundingBox) => {
|
|
|
|
const vals = await boundingBox.array();
|
|
|
|
boundingBox.dispose();
|
|
|
|
return vals;
|
|
|
|
}));
|
2020-10-12 01:22:43 +02:00
|
|
|
const annotatedBoxes = [];
|
|
|
|
for (let i = 0; i < boundingBoxes.length; i++) {
|
|
|
|
const boundingBox = boundingBoxes[i];
|
|
|
|
const annotatedBox = tf.tidy(() => {
|
2020-10-13 04:01:35 +02:00
|
|
|
const box = createBox(boundingBox);
|
2020-10-12 01:22:43 +02:00
|
|
|
const boxIndex = boxIndices[i];
|
2020-10-13 04:01:35 +02:00
|
|
|
const anchor = this.anchorsData[boxIndex];
|
2020-10-16 00:16:05 +02:00
|
|
|
const landmarks = tf
|
|
|
|
.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1])
|
2020-10-12 01:22:43 +02:00
|
|
|
.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,
|
2020-10-16 02:20:37 +02:00
|
|
|
scaleFactor: [inputImage.shape[2] / this.width, inputImage.shape[1] / this.height],
|
2020-10-12 01:22:43 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-10-16 00:16:05 +02:00
|
|
|
async estimateFaces(input) {
|
2020-10-12 01:22:43 +02:00
|
|
|
const image = tf.tidy(() => {
|
|
|
|
if (!(input instanceof tf.Tensor)) {
|
|
|
|
input = tf.browser.fromPixels(input);
|
|
|
|
}
|
|
|
|
return input.toFloat().expandDims(0);
|
|
|
|
});
|
2020-10-13 04:01:35 +02:00
|
|
|
const { boxes, scaleFactor } = await this.getBoundingBoxes(image);
|
2020-10-12 01:22:43 +02:00
|
|
|
image.dispose();
|
|
|
|
return Promise.all(boxes.map(async (face) => {
|
|
|
|
const scaledBox = scaleBoxFromPrediction(face, scaleFactor);
|
2020-10-16 00:16:05 +02:00
|
|
|
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,
|
|
|
|
]));
|
|
|
|
const normalizedFace = {
|
|
|
|
topLeft: boxData.slice(0, 2),
|
|
|
|
bottomRight: boxData.slice(2),
|
|
|
|
landmarks: scaledLandmarks,
|
|
|
|
probability: probabilityData,
|
|
|
|
};
|
|
|
|
disposeBox(face.box);
|
|
|
|
face.landmarks.dispose();
|
|
|
|
face.probability.dispose();
|
2020-10-12 01:22:43 +02:00
|
|
|
scaledBox.dispose();
|
|
|
|
return normalizedFace;
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
2020-10-13 04:01:35 +02:00
|
|
|
|
|
|
|
async function load(config) {
|
|
|
|
const blazeface = await tf.loadGraphModel(config.detector.modelPath, { fromTFHub: config.detector.modelPath.includes('tfhub.dev') });
|
|
|
|
const model = new BlazeFaceModel(blazeface, config);
|
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.load = load;
|
2020-10-12 01:22:43 +02:00
|
|
|
exports.BlazeFaceModel = BlazeFaceModel;
|
2020-10-13 04:01:35 +02:00
|
|
|
exports.disposeBox = disposeBox;
|