What Do the 98 Facial Landmarks in PFLD Represent? A Complete Guide to WFLW Annotation

The 98 facial landmarks in PFLD represent the complete set of manually annotated key-points from the WFLW (Wider Facial Landmarks in-the-wild) dataset, covering the jawline, eyebrows, nose, eyes, mouth, and additional silhouette points for robust pose handling.

PFLD (Pose-Invariant Facial Landmark Detection) is an open-source facial landmark detection system hosted at guoqiangqi/pfld. Unlike older systems that use 68 or fewer points, PFLD operates on 98 landmarks to achieve higher precision across extreme poses and occlusions. This article breaks down exactly what each of those 98 points represents and how the codebase processes them.

Understanding the 98 Facial Landmarks in PFLD

PFLD is built directly on the WFLW dataset, which provides 98 manually annotated facial key-points for each face image. These points are arranged in a fixed order and cover the full geometry of a human face, including 30 additional silhouette points beyond the traditional 68-point markup.

Facial Landmark Regions and Indices

The 98 landmarks are divided into distinct facial regions. Each index is 0-based and follows the original WFLW annotation scheme that PFLD assumes directly:

  • Contour / Jawline (0-16): 17 points tracing from the left ear-side chin point, down through the chin, to the right ear-side chin point.

  • Left Eyebrow (17-21): 5 points outlining the left eyebrow arch.

  • Right Eyebrow (22-26): 5 points outlining the right eyebrow arch.

  • Nose Bridge (27-30): 4 points running from the nasion (between eyebrows) down toward the nose tip.

  • Nose Bottom (31-35): 5 points defining the lower nose ridge and nostril bases.

  • Left Eye (36-41): 6 points tracing the left eye contour, including the inner and outer corners.

  • Right Eye (42-47): 6 points tracing the right eye contour.

  • Outer Lip (48-59): 12 points around the outer mouth boundary.

  • Inner Lip (60-67): 8 points around the inner mouth cavity.

  • Facial Silhouette (68-97): 30 additional points that further refine the jawline, cheek, and other subtle facial structures. WFLW includes these to improve robustness on extreme poses and occlusions.

How PFLD Processes the 98 Landmarks

The PFLD codebase hard-codes the expectation of 98 landmarks throughout the data pipeline and model architecture.

Data Loading and Validation

When loading annotations, the repository strictly enforces the 98-point format. In data/SetPreparation.py, the code checks that each label has a shape of (98, 2):


# From data/SetPreparation.py lines 53-55

landmarks = np.asarray(list(map(float, line.split()[:196])), dtype=np.float32)
landmarks = landmarks.reshape(-1, 2)   # Reshapes to (98, 2)

assert landmarks.shape == (98, 2), "Expected 98 landmarks with x,y coordinates"

The script normalises these coordinates to the cropped-face region and stores the result as a (98, 2) tensor for training.

Model Output Dimensions

The final network head in model2.py outputs 196 values, representing the x and y coordinates for all 98 landmarks:


# From model2.py lines 390-403

# The final layer outputs 196 values (98 landmarks * 2 coordinates)

landmarks = tf.layers.dense(inputs=features, units=196, name='output_layer')
landmarks = tf.reshape(landmarks, [-1, 98, 2])  # Reshape to (batch, 98, 2)

Heatmap Generation

During training, the utility LandmarkImage_98 in utils.py builds a Gaussian heatmap for each of the 98 points:


# From utils.py lines 59-96

def LandmarkImage_98(landmarks, image_size):
    """
    Generates 98-channel heatmap volume.
    Each channel corresponds to one landmark.
    """
    heatmaps = np.zeros((image_size[0], image_size[1], 98), dtype=np.float32)
    for i in range(98):
        # Draw Gaussian at landmark i location

        heatmaps[:, :, i] = draw_gaussian(heatmaps[:, :, i], landmarks[i], sigma=1.5)
    return heatmaps

Data Augmentation

The repository handles horizontal flipping via data/Mirror98.txt, which lists the index mapping for reordering the 98 points when a face is mirrored:


# Mirror98.txt contains 98 integers mapping original indices to flipped positions

mirror_idx = np.loadtxt('data/Mirror98.txt', delimiter=',', dtype=int)

def flip_landmarks(lms):
    lms_flipped = lms.copy()
    lms_flipped[:, 0] = 1.0 - lms_flipped[:, 0]  # Invert x-coordinates

    lms_flipped = lms_flipped[mirror_idx]         # Reorder indices

    return lms_flipped

Working with 98 Facial Landmarks: Code Examples

Loading and Visualising Landmarks

When processing raw WFLW annotation files, extract the first 196 values (98 × 2) and reshape them:

import numpy as np
import cv2

# Example line from WFLW annotation (first 196 values are coordinates)

line = '0.123 0.456 ... 0.789 0.012 ...'  # 196 coordinate values

# Parse and reshape to (98, 2)

landmarks = np.asarray(list(map(float, line.split()[:196])), dtype=np.float32)
landmarks = landmarks.reshape(-1, 2)

# Visualise on image

img = cv2.imread('face.jpg')
h, w = img.shape[:2]
for (x, y) in landmarks:
    cx, cy = int(x * w), int(y * h)
    cv2.circle(img, (cx, cy), 2, (0, 255, 0), -1)
cv2.imwrite('output.jpg', img)

Generating Training Heatmaps

Use the provided utility to create 98-channel Gaussian heatmaps for training:

import tensorflow as tf
from utils import LandmarkImage_98
import numpy as np

# Placeholder for batch of landmarks (batch_size, 98, 2)

landmarks_ph = tf.placeholder(tf.float32, shape=(None, 98, 2))

# Generate heatmaps (batch, 112, 112, 98)

heatmaps = LandmarkImage_98(landmarks_ph, image_size=(112, 112))

with tf.Session() as sess:
    dummy = np.random.rand(1, 98, 2).astype(np.float32)
    output = sess.run(heatmaps, feed_dict={landmarks_ph: dummy})
    print(f"Output shape: {output.shape}")  # (1, 112, 112, 98)

Horizontal Flip Augmentation

Implement mirroring augmentation using the index mapping:

import numpy as np

# Load mirroring indices from repository

mirror_idx = np.loadtxt('data/Mirror98.txt', delimiter=',', dtype=int)

def augment_flip(landmarks, image):
    """Horizontally flip image and reorder landmarks."""
    # Flip image

    flipped_img = cv2.flip(image, 1)
    
    # Flip coordinates: x' = 1 - x

    flipped_lms = landmarks.copy()
    flipped_lms[:, 0] = 1.0 - flipped_lms[:, 0]
    
    # Reorder indices to maintain semantic consistency

    flipped_lms = flipped_lms[mirror_idx]
    
    return flipped_lms, flipped_img

Summary

  • The 98 facial landmarks in PFLD correspond to the complete annotation set from the WFLW dataset, providing dense coverage of facial geometry.
  • The landmarks are divided into regions: contour (0-16), eyebrows (17-26), nose (27-35), eyes (36-47), mouth (48-67), and facial silhouette (68-97).
  • The codebase enforces this structure strictly: data/SetPreparation.py validates the (98, 2) shape, model2.py outputs 196 values (98×2), and utils.py generates 98-channel heatmaps.
  • Horizontal flip augmentation uses data/Mirror98.txt to maintain semantic ordering of the 98 points during data augmentation.

Frequently Asked Questions

What dataset provides the 98 facial landmarks used in PFLD?

PFLD uses the WFLW (Wider Facial Landmarks in-the-wild) dataset, which provides 98 manually annotated facial key-points for each image. This dataset extends earlier formats like 300W by adding 30 extra silhouette points to improve robustness on extreme poses and occlusions.

How are the 98 facial landmarks ordered in PFLD?

The landmarks follow a fixed 0-based index scheme: 0-16 define the jawline contour, 17-26 cover both eyebrows, 27-35 map the nose structure, 36-47 trace both eyes, 48-67 outline the lips, and 68-97 provide additional facial silhouette points. This ordering is hard-coded in the repository's data loading and augmentation logic.

Why does PFLD use 98 landmarks instead of the traditional 68?

The additional 30 points (indices 68-97) provide denser coverage of the facial boundary and cheek regions, which helps the model maintain accuracy under extreme head poses, partial occlusion, and varying lighting conditions. The WFLW dataset introduced these extra points specifically to address failure cases common in sparser annotation schemes.

How does PFLD handle horizontal flipping for the 98 landmarks?

The repository uses data/Mirror98.txt, which contains a pre-computed index mapping that reassigns each landmark to its mirrored counterpart. During augmentation, the code inverts the x-coordinate (x' = 1 - x) and then reorders the array using the mirror indices to ensure that, for example, the left eye points become the right eye points after flipping.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →