face-api/src/classes/FaceLandmarks.ts

131 lines
4.1 KiB
TypeScript
Raw Normal View History

2020-12-19 17:46:41 +01:00
import { minBbox } from '../ops/index';
import { getCenterPoint } from '../utils/index';
2020-08-18 13:54:53 +02:00
import { IBoundingBox } from './BoundingBox';
import { Box } from './Box';
import { Dimensions, IDimensions } from './Dimensions';
import { FaceDetection } from './FaceDetection';
import { Point } from './Point';
import { IRect, Rect } from './Rect';
// face alignment constants
2020-12-23 17:26:55 +01:00
const relX = 0.5;
const relY = 0.43;
const relScale = 0.45;
2020-08-18 13:54:53 +02:00
export interface IFaceLandmarks {
positions: Point[]
shift: Point
}
export class FaceLandmarks implements IFaceLandmarks {
protected _shift: Point
2020-12-23 17:26:55 +01:00
2020-08-18 13:54:53 +02:00
protected _positions: Point[]
2020-12-23 17:26:55 +01:00
2020-08-18 13:54:53 +02:00
protected _imgDims: Dimensions
constructor(
relativeFaceLandmarkPositions: Point[],
imgDims: IDimensions,
2020-12-23 17:26:55 +01:00
shift: Point = new Point(0, 0),
2020-08-18 13:54:53 +02:00
) {
2020-12-23 17:26:55 +01:00
const { width, height } = imgDims;
this._imgDims = new Dimensions(width, height);
this._shift = shift;
2020-08-18 13:54:53 +02:00
this._positions = relativeFaceLandmarkPositions.map(
2020-12-23 17:26:55 +01:00
(pt) => pt.mul(new Point(width, height)).add(shift),
);
2020-08-18 13:54:53 +02:00
}
2020-12-23 17:26:55 +01:00
public get shift(): Point { return new Point(this._shift.x, this._shift.y); }
public get imageWidth(): number { return this._imgDims.width; }
public get imageHeight(): number { return this._imgDims.height; }
public get positions(): Point[] { return this._positions; }
2020-08-18 13:54:53 +02:00
public get relativePositions(): Point[] {
return this._positions.map(
2020-12-23 17:26:55 +01:00
(pt) => pt.sub(this._shift).div(new Point(this.imageWidth, this.imageHeight)),
);
2020-08-18 13:54:53 +02:00
}
public forSize<T extends FaceLandmarks>(width: number, height: number): T {
return new (this.constructor as any)(
this.relativePositions,
2020-12-23 17:26:55 +01:00
{ width, height },
);
2020-08-18 13:54:53 +02:00
}
public shiftBy<T extends FaceLandmarks>(x: number, y: number): T {
return new (this.constructor as any)(
this.relativePositions,
this._imgDims,
2020-12-23 17:26:55 +01:00
new Point(x, y),
);
2020-08-18 13:54:53 +02:00
}
public shiftByPoint<T extends FaceLandmarks>(pt: Point): T {
2020-12-23 17:26:55 +01:00
return this.shiftBy(pt.x, pt.y);
2020-08-18 13:54:53 +02:00
}
/**
* Aligns the face landmarks after face detection from the relative positions of the faces
* bounding box, or it's current shift. This function should be used to align the face images
* after face detection has been performed, before they are passed to the face recognition net.
* This will make the computed face descriptor more accurate.
*
* @param detection (optional) The bounding box of the face or the face detection result. If
* no argument was passed the position of the face landmarks are assumed to be relative to
* it's current shift.
* @returns The bounding box of the aligned face.
*/
public align(
detection?: FaceDetection | IRect | IBoundingBox | null,
2020-12-23 17:26:55 +01:00
options: { useDlibAlignment?: boolean, minBoxPadding?: number } = { },
2020-08-18 13:54:53 +02:00
): Box {
if (detection) {
const box = detection instanceof FaceDetection
? detection.box.floor()
2020-12-23 17:26:55 +01:00
: new Box(detection);
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
return this.shiftBy(box.x, box.y).align(null, options);
2020-08-18 13:54:53 +02:00
}
2020-12-23 17:26:55 +01:00
const { useDlibAlignment, minBoxPadding } = { useDlibAlignment: false, minBoxPadding: 0.2, ...options };
2020-08-18 13:54:53 +02:00
if (useDlibAlignment) {
2020-12-23 17:26:55 +01:00
return this.alignDlib();
2020-08-18 13:54:53 +02:00
}
2020-12-23 17:26:55 +01:00
return this.alignMinBbox(minBoxPadding);
2020-08-18 13:54:53 +02:00
}
private alignDlib(): Box {
2020-12-23 17:26:55 +01:00
const centers = this.getRefPointsForAlignment();
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const [leftEyeCenter, rightEyeCenter, mouthCenter] = centers;
const distToMouth = (pt: Point) => mouthCenter.sub(pt).magnitude();
const eyeToMouthDist = (distToMouth(leftEyeCenter) + distToMouth(rightEyeCenter)) / 2;
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const size = Math.floor(eyeToMouthDist / relScale);
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const refPoint = getCenterPoint(centers);
2020-08-18 13:54:53 +02:00
// TODO: pad in case rectangle is out of image bounds
2020-12-23 17:26:55 +01:00
const x = Math.floor(Math.max(0, refPoint.x - (relX * size)));
const y = Math.floor(Math.max(0, refPoint.y - (relY * size)));
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
return new Rect(x, y, Math.min(size, this.imageWidth + x), Math.min(size, this.imageHeight + y));
2020-08-18 13:54:53 +02:00
}
private alignMinBbox(padding: number): Box {
2020-12-23 17:26:55 +01:00
const box = minBbox(this.positions);
return box.pad(box.width * padding, box.height * padding);
2020-08-18 13:54:53 +02:00
}
protected getRefPointsForAlignment(): Point[] {
2020-12-23 17:26:55 +01:00
throw new Error('getRefPointsForAlignment not implemented by base class');
2020-08-18 13:54:53 +02:00
}
2020-12-23 17:26:55 +01:00
}