face-api/src/factories/WithFaceLandmarks.ts

74 lines
3.1 KiB
TypeScript
Raw Normal View History

2020-08-18 13:54:53 +02:00
import { FaceDetection } from '../classes/FaceDetection';
import { FaceLandmarks } from '../classes/FaceLandmarks';
import { FaceLandmarks68 } from '../classes/FaceLandmarks68';
import { isWithFaceDetection, WithFaceDetection } from './WithFaceDetection';
export type WithFaceLandmarks<
TSource extends WithFaceDetection<{}>,
2020-12-23 17:26:55 +01:00
TFaceLandmarks extends FaceLandmarks = FaceLandmarks68 > = TSource & {
landmarks: TFaceLandmarks
unshiftedLandmarks: TFaceLandmarks
alignedRect: FaceDetection
}
2020-08-18 13:54:53 +02:00
export function isWithFaceLandmarks(obj: any): obj is WithFaceLandmarks<WithFaceDetection<{}>, FaceLandmarks> {
return isWithFaceDetection(obj)
2020-12-23 17:26:55 +01:00
// eslint-disable-next-line dot-notation
2020-08-18 13:54:53 +02:00
&& obj['landmarks'] instanceof FaceLandmarks
2020-12-23 17:26:55 +01:00
// eslint-disable-next-line dot-notation
2020-08-18 13:54:53 +02:00
&& obj['unshiftedLandmarks'] instanceof FaceLandmarks
2020-12-23 17:26:55 +01:00
// eslint-disable-next-line dot-notation
&& obj['alignedRect'] instanceof FaceDetection;
2020-08-18 13:54:53 +02:00
}
2021-03-07 15:58:20 +01:00
function calculateFaceAngle(mesh) {
const radians = (a1, a2, b1, b2) => Math.atan2(b2 - a2, b1 - a1);
const angle = { roll: <number | undefined>undefined, pitch: <number | undefined>undefined, yaw: <number | undefined>undefined };
if (!mesh || !mesh._positions || mesh._positions.length !== 68) return angle;
const pt = mesh._positions;
// roll is face lean left/right
// comparing x,y of outside corners of leftEye and rightEye
angle.roll = radians(pt[36]._x, pt[36]._y, pt[45]._x, pt[45]._y);
// yaw is face turn left/right
// comparing x distance of bottom of nose to left and right edge of face
// and y distance of top of nose to left and right edge of face
// precision is lacking since coordinates are not precise enough
angle.pitch = radians(pt[30]._x - pt[0]._x, pt[27]._y - pt[0]._y, pt[16]._x - pt[30]._x, pt[27]._y - pt[16]._y);
// pitch is face move up/down
// comparing size of the box around the face with top and bottom of detected landmarks
// silly hack, but this gives us face compression on y-axis
// e.g., tilting head up hides the forehead that doesn't have any landmarks so ratio drops
// value is normalized to range, but is not in actual radians
const bottom = pt.reduce((prev, cur) => (prev < cur._y ? prev : cur._y), +Infinity);
const top = pt.reduce((prev, cur) => (prev > cur._y ? prev : cur._y), -Infinity);
angle.yaw = 10 * (mesh._imgDims._height / (top - bottom) / 1.45 - 1);
return angle;
}
2020-08-18 13:54:53 +02:00
export function extendWithFaceLandmarks<
TSource extends WithFaceDetection<{}>,
2020-12-23 17:26:55 +01:00
TFaceLandmarks extends FaceLandmarks = FaceLandmarks68 >(sourceObj: TSource, unshiftedLandmarks: TFaceLandmarks): WithFaceLandmarks<TSource, TFaceLandmarks> {
const { box: shift } = sourceObj.detection;
const landmarks = unshiftedLandmarks.shiftBy<TFaceLandmarks>(shift.x, shift.y);
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const rect = landmarks.align();
const { imageDims } = sourceObj.detection;
const alignedRect = new FaceDetection(sourceObj.detection.score, rect.rescale(imageDims.reverse()), imageDims);
2021-03-07 15:58:20 +01:00
const angle = calculateFaceAngle(unshiftedLandmarks);
2020-08-18 13:54:53 +02:00
const extension = {
landmarks,
unshiftedLandmarks,
2020-12-23 17:26:55 +01:00
alignedRect,
2021-03-07 15:58:20 +01:00
angle,
2020-12-23 17:26:55 +01:00
};
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
return { ...sourceObj, ...extension };
}