face-api/src/factories/WithFaceLandmarks.ts

77 lines
3.5 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 & {
2021-03-08 03:15:53 +01:00
landmarks: TFaceLandmarks,
unshiftedLandmarks: TFaceLandmarks,
alignedRect: FaceDetection,
angle: { roll: number | undefined, pitch: number | undefined, yaw: number | undefined },
2020-12-23 17:26:55 +01:00
}
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) {
2021-03-08 14:55:51 +01:00
// returns the angle in the plane (in radians) between the positive x-axis and the ray from (0,0) to the point (x,y)
const radians = (a1, a2, b1, b2) => (Math.atan2(b2 - a2, b1 - a1) % Math.PI);
// convert radians to degrees
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const degrees = (theta) => (theta * 180) / Math.PI;
2021-03-07 15:58:20 +01:00
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;
2021-03-08 14:55:51 +01:00
// values are in radians in range of -pi/2 to pi/2 which is -90 to +90 degrees
// value of 0 means center
// roll is face lean from left to right
2021-03-07 15:58:20 +01:00
// comparing x,y of outside corners of leftEye and rightEye
2021-03-08 14:55:51 +01:00
angle.roll = -radians(pt[36]._x, pt[36]._y, pt[45]._x, pt[45]._y);
2021-03-07 15:58:20 +01:00
2021-03-08 14:55:51 +01:00
// pitch is face turn from left right
// comparing x distance of top of nose to left and right edge of face
2021-03-07 15:58:20 +01:00
// precision is lacking since coordinates are not precise enough
2021-03-08 14:55:51 +01:00
angle.pitch = radians(0, Math.abs(pt[0]._x - pt[30]._x) / pt[30]._x, Math.PI, Math.abs(pt[16]._x - pt[30]._x) / pt[30]._x);
2021-03-07 15:58:20 +01:00
2021-03-08 14:55:51 +01:00
// yaw is face move from up to down
2021-03-07 15:58:20 +01:00
// 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
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);
2021-03-08 14:55:51 +01:00
angle.yaw = Math.PI * (mesh._imgDims._height / (top - bottom) / 1.40 - 1);
2021-03-07 15:58:20 +01:00
return angle;
}
2022-04-05 13:38:11 +02:00
export function extendWithFaceLandmarks<TSource extends WithFaceDetection<{}>, TFaceLandmarks extends FaceLandmarks = FaceLandmarks68 >(sourceObj: TSource, unshiftedLandmarks: TFaceLandmarks): WithFaceLandmarks<TSource, TFaceLandmarks> {
2020-12-23 17:26:55 +01:00
const { box: shift } = sourceObj.detection;
const landmarks = unshiftedLandmarks.shiftBy<TFaceLandmarks>(shift.x, shift.y);
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 };
}