DEV Community

Robin Ma
Robin Ma

Posted on

Building a Facial Landmark Analyzer with Python

Facial analysis has evolved significantly from simple bounding box object detectors. With modern convolutional and transformer-based architectures, extracting sub-millimeter anatomical landmarks in real-time runs comfortably inside browser runtimes and lightweight Python microservices.

Aesthetic metric calculations, such as facial thirds, canthal tilts, and facial width-to-height ratios (fWHR), have gained significant interest across social platforms. To a computer vision engineer, these are simply deterministic geometric operations executed on normalized coordinate matrices.

In this walkthrough, we will build a working Python pipeline using MediaPipe Face Mesh and NumPy to extract, normalize, and calculate geometric feature vectors from a single 2D image, while examining the optical challenges inherent in 2D facial measurement.

Programming and computer vision code on terminal display

Setting Up the Pipeline

We will use Google's MediaPipe library, which infers a 468-point 3D facial surface geometry in a single forward pass without requiring dedicated GPU acceleration.

First, install the necessary dependencies:

pip install opencv-python mediapipe numpy
Enter fullscreen mode Exit fullscreen mode

Extracting Dense Landmark Coordinates

The core pipeline loads an RGB image, executes landmark regression, and converts normalized coordinates ([0.0, 1.0]) into pixel-space matrices:

import cv2
import mediapipe as mp
import numpy as np

class FacialGeometryExtractor:
    def __init__(self):
        self.mp_face_mesh = mp.solutions.face_mesh
        self.face_mesh = self.mp_face_mesh.FaceMesh(
            static_image_mode=True,
            max_num_faces=1,
            refine_landmarks=True,
            min_detection_confidence=0.5
        )

    def extract_landmarks(self, image_bgr):
        h, w, _ = image_bgr.shape
        image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
        results = self.face_mesh.process(image_rgb)

        if not results.multi_face_landmarks:
            return None

        # Extract 468 points as a (468, 3) NumPy array
        landmarks = np.array([
            [pt.x * w, pt.y * h, pt.z * w]
            for pt in results.multi_face_landmarks[0].landmark
        ])
        return landmarks
Enter fullscreen mode Exit fullscreen mode

Calculating Core Geometric Metrics

Once raw landmark coordinates are available, we calculate key structural vectors.

1. Canthal Tilt Angle

Canthal tilt measures the inclination angle between the inner corner (endocanthion) and the outer corner (exocanthion) of the eye:

def compute_canthal_tilt(inner_eye, outer_eye):
    """
    Computes angle in degrees relative to the horizontal axis.
    Positive indicates positive canthal tilt.
    """
    dx = outer_eye[0] - inner_eye[0]
    dy = outer_eye[1] - inner_eye[1]

    # Invert dy since image coordinate origin (0,0) is top-left
    angle_rad = np.arctan2(-dy, dx)
    return np.degrees(angle_rad)
Enter fullscreen mode Exit fullscreen mode

2. Facial Width-to-Height Ratio (fWHR)

fWHR is an extensively researched metric in evolutionary psychology, measuring bizygomatic width against midface height:

def compute_fwhr(left_zygoma, right_zygoma, brow_midpoint, upper_lip):
    bizygomatic_width = np.linalg.norm(right_zygoma[:2] - left_zygoma[:2])
    midface_height = np.linalg.norm(upper_lip[:2] - brow_midpoint[:2])

    if midface_height == 0:
        return 0.0
    return bizygomatic_width / midface_height
Enter fullscreen mode Exit fullscreen mode

3. Vertical Thirds Proportions

To evaluate vertical harmony, we calculate distances between the trichion (or upper forehead boundary), glabella (between eyebrows), subnasale (base of nose), and menton (chin tip):

def compute_vertical_thirds(forehead_top, glabella, subnasale, menton):
    upper_third = np.linalg.norm(glabella[:2] - forehead_top[:2])
    middle_third = np.linalg.norm(subnasale[:2] - glabella[:2])
    lower_third = np.linalg.norm(menton[:2] - subnasale[:2])

    total = upper_third + middle_third + lower_third
    if total == 0:
        return (0.33, 0.33, 0.33)

    return (
        upper_third / total,
        middle_third / total,
        lower_third / total
    )
Enter fullscreen mode Exit fullscreen mode

Pose Normalization and Affine Alignment

Unconstrained selfies rarely exhibit perfect frontal orientation. Roll, pitch, and yaw introduce substantial noise into Euclidean distance measurements.

Before calculating distances, production implementations of an online PSL scale calculator typically run affine normalization passes to rotate the face so the interpupillary line is strictly horizontal. By scaling the bounding box relative to the interpupillary distance (IPD), the feature vectors become scale- and rotation-invariant.

def align_face_roll(image, left_pupil, right_pupil):
    d_x = right_pupil[0] - left_pupil[0]
    d_y = right_pupil[1] - left_pupil[1]
    angle = np.degrees(np.arctan2(d_y, d_x))

    center = (
        int((left_pupil[0] + right_pupil[0]) / 2),
        int((left_pupil[1] + right_pupil[1]) / 2)
    )

    rot_matrix = cv2.getRotationMatrix2D(center, angle, scale=1.0)
    aligned = cv2.warpAffine(
        image, 
        rot_matrix, 
        (image.shape[1], image.shape[0]), 
        flags=cv2.INTER_CUBIC
    )
    return aligned
Enter fullscreen mode Exit fullscreen mode

The Mathematical Limits of 2D Perspective Projection

While the code runs reliably, machine learning engineers must account for the fundamental limitations of 2D landmark modeling:

  • Focal Length Distortion: Smartphone front cameras utilize wide-angle lenses (24mm to 28mm full-frame equivalent). At 35cm selfie distances, barrel perspective distortion widens the central midface by 15% to 30% compared to telephoto lenses (85mm).
  • Pitch Flattening: Tilting the head slightly downward reduces the perceived 2D height of the lower third, introducing significant measurement variance.
  • Illusion of Absolute Beauty: Beyond optical noise, clinical aesthetic literature demonstrates that rigid mathematical constants like the golden ratio fail to correlate with real-world human charm and social appeal. Real attraction is dynamic, driven by micro-expressions, posture, and voice rather than static 2D coordinates.

Architecture Considerations for Biometric Web Apps

If you deploy a facial geometry parser into production, architectural decisions are critical:

  • Ephemeral Processing: Avoid storing user uploads in databases or S3 buckets. Process tensors in memory and purge raw payloads immediately.
  • Client-Side WASM / TF.js: Executing MediaPipe directly in the user's browser via WebAssembly removes server compute costs while guaranteeing zero server-side biometric data retention.

Conclusion

With modern open-source models, building a functional facial landmark parser takes fewer than 100 lines of Python.

However, good engineering requires knowing the limits of the model. 2D landmarking delivers reproducible geometric data points, but optical physics and human perception ensure that a living, expressive face will always remain more complex than a matrix of coordinates.

Top comments (0)