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 };
|
|
|
|
}
|