human/src/face/facemesh.ts

140 lines
7.1 KiB
TypeScript
Raw Normal View History

2021-09-28 18:01:48 +02:00
/**
* BlazeFace, FaceMesh & Iris model implementation
*
* Based on:
* - [**MediaPipe BlazeFace**](https://drive.google.com/file/d/1f39lSzU5Oq-j_OXgS67KfN5wNsoeAZ4V/view)
* - Facial Spacial Geometry: [**MediaPipe FaceMesh**](https://drive.google.com/file/d/1VFC_wIpw4O7xBOiTgUldl79d9LA-LsnA/view)
* - Eye Iris Details: [**MediaPipe Iris**](https://drive.google.com/file/d/1bsWbokp9AklH2ANjCfmjqEzzxO1CNbMu/view)
*/
2021-10-22 22:09:52 +02:00
import { log, join, now } from '../util/util';
2021-09-28 18:01:48 +02:00
import * as tf from '../../dist/tfjs.esm.js';
import * as blazeface from './blazeface';
import * as util from './facemeshutil';
import * as coords from './facemeshcoords';
import * as iris from './iris';
import type { GraphModel, Tensor } from '../tfjs/types';
import type { FaceResult, Point } from '../result';
import type { Config } from '../config';
import { env } from '../util/env';
2021-11-03 21:32:07 +01:00
type BoxCache = { startPoint: Point, endPoint: Point, landmarks: Array<Point>, confidence: number };
2021-09-28 18:01:48 +02:00
let boxCache: Array<BoxCache> = [];
let model: GraphModel | null = null;
let inputSize = 0;
let skipped = Number.MAX_SAFE_INTEGER;
2021-10-22 22:09:52 +02:00
let lastTime = 0;
2021-11-03 21:32:07 +01:00
const enlargeFact = 1.6;
2021-09-28 18:01:48 +02:00
export async function predict(input: Tensor, config: Config): Promise<FaceResult[]> {
2021-10-22 22:09:52 +02:00
// reset cached boxes
2021-10-23 15:38:52 +02:00
const skipTime = (config.face.detector?.skipTime || 0) > (now() - lastTime);
const skipFrame = skipped < (config.face.detector?.skipFrames || 0);
2021-11-03 21:32:07 +01:00
if (!config.skipAllowed || !skipTime || !skipFrame || boxCache.length === 0) {
const possibleBoxes = await blazeface.getBoxes(input, config); // get results from blazeface detector
2021-10-22 22:09:52 +02:00
lastTime = now();
2021-09-28 18:01:48 +02:00
boxCache = []; // empty cache
2021-11-03 21:32:07 +01:00
for (const possible of possibleBoxes.boxes) { // extract data from detector
const box: BoxCache = {
startPoint: await possible.box.startPoint.data() as unknown as Point,
endPoint: await possible.box.endPoint.data() as unknown as Point,
landmarks: await possible.landmarks.array() as Array<Point>,
confidence: possible.confidence,
};
boxCache.push(util.squarifyBox(util.enlargeBox(util.scaleBoxCoordinates(box, possibleBoxes.scaleFactor), Math.sqrt(enlargeFact))));
2021-09-28 18:01:48 +02:00
}
2021-11-03 21:32:07 +01:00
possibleBoxes.boxes.forEach((prediction) => tf.dispose([prediction.box.startPoint, prediction.box.endPoint, prediction.landmarks]));
2021-09-28 18:01:48 +02:00
skipped = 0;
} else {
skipped++;
}
const faces: Array<FaceResult> = [];
2021-11-03 21:32:07 +01:00
const newCache: Array<BoxCache> = [];
2021-09-28 18:01:48 +02:00
let id = 0;
2021-11-03 21:32:07 +01:00
for (let i = 0; i < boxCache.length; i++) {
let box = boxCache[i];
2021-09-28 18:01:48 +02:00
let angle = 0;
let rotationMatrix;
2021-11-03 21:32:07 +01:00
const face: FaceResult = { // init face result
2021-09-28 18:01:48 +02:00
id: id++,
mesh: [],
meshRaw: [],
box: [0, 0, 0, 0],
boxRaw: [0, 0, 0, 0],
score: 0,
boxScore: 0,
faceScore: 0,
annotations: {},
};
if (config.face.detector?.rotation && config.face.mesh?.enabled && env.kernels.includes('rotatewithoffset')) {
[angle, rotationMatrix, face.tensor] = util.correctFaceRotation(box, input, inputSize);
} else {
2021-11-03 21:32:07 +01:00
rotationMatrix = util.fixedRotationMatrix;
face.tensor = util.cutBoxFromImageAndResize(box, input, config.face.mesh?.enabled ? [inputSize, inputSize] : [blazeface.size(), blazeface.size()]);
2021-09-28 18:01:48 +02:00
}
face.boxScore = Math.round(100 * box.confidence) / 100;
if (!config.face.mesh?.enabled) { // mesh not enabled, return resuts from detector only
face.box = util.getClampedBox(box, input);
face.boxRaw = util.getRawBox(box, input);
2021-11-03 21:32:07 +01:00
face.boxScore = Math.round(100 * box.confidence || 0) / 100;
face.score = face.boxScore;
2021-09-28 18:01:48 +02:00
face.mesh = box.landmarks.map((pt) => [
((box.startPoint[0] + box.endPoint[0])) / 2 + ((box.endPoint[0] + box.startPoint[0]) * pt[0] / blazeface.size()),
((box.startPoint[1] + box.endPoint[1])) / 2 + ((box.endPoint[1] + box.startPoint[1]) * pt[1] / blazeface.size()),
]);
face.meshRaw = face.mesh.map((pt) => [pt[0] / (input.shape[2] || 0), pt[1] / (input.shape[1] || 0), (pt[2] || 0) / inputSize]);
for (const key of Object.keys(coords.blazeFaceLandmarks)) face.annotations[key] = [face.mesh[coords.blazeFaceLandmarks[key] as number]]; // add annotations
2021-09-28 18:01:48 +02:00
} else if (!model) { // mesh enabled, but not loaded
if (config.debug) log('face mesh detection requested, but model is not loaded');
} else { // mesh enabled
const [contours, confidence, contourCoords] = model.execute(face.tensor as Tensor) as Array<Tensor>; // first returned tensor represents facial contours which are already included in the coordinates.
2021-11-03 21:32:07 +01:00
const faceConfidence = await confidence.data();
face.faceScore = Math.round(100 * faceConfidence[0]) / 100;
2021-09-28 18:01:48 +02:00
const coordsReshaped = tf.reshape(contourCoords, [-1, 3]);
let rawCoords = await coordsReshaped.array();
2021-11-03 21:32:07 +01:00
tf.dispose([contourCoords, coordsReshaped, confidence, contours]);
if (face.faceScore < (config.face.detector?.minConfidence || 1)) { // low confidence in detected mesh
box.confidence = face.faceScore; // reset confidence of cached box
2021-09-28 18:01:48 +02:00
} else {
if (config.face.iris?.enabled) rawCoords = await iris.augmentIris(rawCoords, face.tensor, config, inputSize); // augment results with iris
face.mesh = util.transformRawCoords(rawCoords, box, angle, rotationMatrix, inputSize); // get processed mesh
face.meshRaw = face.mesh.map((pt) => [pt[0] / (input.shape[2] || 0), pt[1] / (input.shape[1] || 0), (pt[2] || 0) / inputSize]);
for (const key of Object.keys(coords.meshAnnotations)) face.annotations[key] = coords.meshAnnotations[key].map((index) => face.mesh[index]); // add annotations
2021-11-03 21:32:07 +01:00
box = util.squarifyBox(util.enlargeBox(util.calculateLandmarksBoundingBox(face.mesh), enlargeFact)); // redefine box with mesh calculated one
2021-09-28 18:01:48 +02:00
face.box = util.getClampedBox(box, input); // update detected box with box around the face mesh
face.boxRaw = util.getRawBox(box, input);
2021-11-03 21:32:07 +01:00
face.score = face.faceScore;
newCache.push(box);
// other modules prefer wider crop for a face so we dispose it and do it again
/*
tf.dispose(face.tensor);
face.tensor = config.face.detector?.rotation && config.face.mesh?.enabled && env.kernels.includes('rotatewithoffset')
? face.tensor = util.correctFaceRotation(util.enlargeBox(box, Math.sqrt(enlargeFact)), input, inputSize)[2]
: face.tensor = util.cutBoxFromImageAndResize(util.enlargeBox(box, Math.sqrt(enlargeFact)), input, [inputSize, inputSize]);
*/
2021-09-28 18:01:48 +02:00
}
}
faces.push(face);
}
2021-11-03 21:32:07 +01:00
boxCache = [...newCache]; // reset cache
2021-09-28 18:01:48 +02:00
return faces;
}
export async function load(config: Config): Promise<GraphModel> {
if (env.initial) model = null;
if (!model) {
model = await tf.loadGraphModel(join(config.modelBasePath, config.face.mesh?.modelPath || '')) as unknown as GraphModel;
2021-10-13 16:56:56 +02:00
if (!model || !model['modelUrl']) log('load model failed:', config.face.mesh?.modelPath);
2021-09-28 18:01:48 +02:00
else if (config.debug) log('load model:', model['modelUrl']);
} else if (config.debug) log('cached model:', model['modelUrl']);
inputSize = model.inputs[0].shape ? model.inputs[0].shape[2] : 0;
if (inputSize === -1) inputSize = 64;
return model;
}
export const triangulation = coords.TRI468;
export const uvmap = coords.UV468;