chenyc
2025-05-29 92f69c57b920cf62ecc9f15f9ed196fa26dbf2ac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { minBbox } from '../ops/index';
import { getCenterPoint } from '../utils/index';
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
const relX = 0.5;
const relY = 0.43;
const relScale = 0.45;
 
export interface IFaceLandmarks {
  positions: Point[]
  shift: Point
}
 
export class FaceLandmarks implements IFaceLandmarks {
  protected _shift: Point;
 
  protected _positions: Point[];
 
  protected _imgDims: Dimensions;
 
  constructor(
    relativeFaceLandmarkPositions: Point[],
    imgDims: IDimensions,
    shift: Point = new Point(0, 0),
  ) {
    const { width, height } = imgDims;
    this._imgDims = new Dimensions(width, height);
    this._shift = shift;
    this._positions = relativeFaceLandmarkPositions.map(
      (pt) => pt.mul(new Point(width, height)).add(shift),
    );
  }
 
  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; }
 
  public get relativePositions(): Point[] {
    return this._positions.map(
      (pt) => pt.sub(this._shift).div(new Point(this.imageWidth, this.imageHeight)),
    );
  }
 
  public forSize<T extends FaceLandmarks>(width: number, height: number): T {
    return new (this.constructor as any)(
      this.relativePositions,
      { width, height },
    );
  }
 
  public shiftBy<T extends FaceLandmarks>(x: number, y: number): T {
    return new (this.constructor as any)(
      this.relativePositions,
      this._imgDims,
      new Point(x, y),
    );
  }
 
  public shiftByPoint<T extends FaceLandmarks>(pt: Point): T {
    return this.shiftBy(pt.x, pt.y);
  }
 
  /**
   * 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,
    options: { useDlibAlignment?: boolean, minBoxPadding?: number } = { },
  ): Box {
    if (detection) {
      const box = detection instanceof FaceDetection
        ? detection.box.floor()
        : new Box(detection);
 
      return this.shiftBy(box.x, box.y).align(null, options);
    }
 
    const { useDlibAlignment, minBoxPadding } = { useDlibAlignment: false, minBoxPadding: 0.2, ...options };
 
    if (useDlibAlignment) {
      return this.alignDlib();
    }
 
    return this.alignMinBbox(minBoxPadding);
  }
 
  private alignDlib(): Box {
    const centers = this.getRefPointsForAlignment();
 
    const [leftEyeCenter, rightEyeCenter, mouthCenter] = centers;
    const distToMouth = (pt: Point) => mouthCenter.sub(pt).magnitude();
    const eyeToMouthDist = (distToMouth(leftEyeCenter) + distToMouth(rightEyeCenter)) / 2;
 
    const size = Math.floor(eyeToMouthDist / relScale);
 
    const refPoint = getCenterPoint(centers);
    // TODO: pad in case rectangle is out of image bounds
    const x = Math.floor(Math.max(0, refPoint.x - (relX * size)));
    const y = Math.floor(Math.max(0, refPoint.y - (relY * size)));
 
    return new Rect(x, y, Math.min(size, this.imageWidth + x), Math.min(size, this.imageHeight + y));
  }
 
  private alignMinBbox(padding: number): Box {
    const box = minBbox(this.positions);
    return box.pad(box.width * padding, box.height * padding);
  }
 
  protected getRefPointsForAlignment(): Point[] {
    throw new Error('getRefPointsForAlignment not implemented by base class');
  }
}