How PFLD Handles Challenging Facial Conditions Like Occlusion and Blur

PFLD addresses occlusion and blur by leveraging binary attribute flags from the WFLW dataset and applying inverse-frequency loss weighting, ensuring the model prioritizes learning from rare, difficult samples during training.

The Progressive Face Localization Detector (PFLD) is a lightweight facial landmark detection system implemented in the guoqiangqi/pfld repository. Unlike standard detectors that treat all training samples equally, PFLD explicitly models challenging facial conditions—specifically occlusion and blur—through a sophisticated attribute-aware training pipeline that reweights the loss function based on the rarity of these conditions.

Understanding the WFLW Dataset Attributes

PFLD's robustness begins at the data layer. The model is trained on the WFLW (Wider Facial Landmarks in-the-wild) dataset, which provides six binary attribute flags for each image: pose, expression, illumination, make-up, occlusion, and blur.

In data/SetPreparation.py, the parser extracts these flags from the annotation files and stores them for later use:


# Lines 36-52: Parsing annotation lines

# Each line contains: image_path landmarks [attributes]

# Attributes are stored in self.occlusion and self.blur

The save_data routine (lines 146-151) then serializes these attributes into the training list file as space-separated integers, ensuring every training sample carries explicit metadata about whether it contains occlusion (bit 4) or blur (bit 5).

Attribute-Aware Loss Weighting in PFLD

The core mechanism for handling challenging facial conditions resides in train_model.py, where PFLD implements an adaptive loss function that assigns higher importance to samples exhibiting occlusion or blur.

Parsing Binary Attribute Flags

During training, the input pipeline feeds attribute vectors into the TensorFlow graph via a dedicated placeholder:


# Lines 72-77 in train_model.py

attribute_batch = tf.placeholder(tf.int32, shape=(None, 6), name='attribute_batch')

This tensor carries the six binary flags for every image in the batch, allowing the model to know in real-time which samples represent difficult facial conditions.

Inverse Frequency Weighting for Occlusion and Blur

PFLD computes per-sample loss weights using inverse frequency weighting, ensuring rare conditions like occlusion and blur receive higher weights. The implementation (lines 89-95) processes the attribute batch as follows:


# Convert attributes to float and exclude pose (index 0)

attributes_w_n = tf.to_float(attribute_batch[:, 1:6])  # expression, illumination, make-up, occlusion, blur

# Calculate batch-wise occurrence rate for each attribute

mat_ratio = tf.reduce_mean(attributes_w_n, axis=0)

# Invert frequency: rare attributes (occlusion, blur) get higher weights

mat_ratio = tf.map_fn(
    lambda x: tf.cond(x > 0, lambda: 1 / x, lambda: float(args.batch_size)),
    mat_ratio)

# Apply weights to each sample

attributes_w_n = tf.convert_to_tensor(attributes_w_n * mat_ratio)
attributes_w_n = tf.reduce_sum(attributes_w_n, axis=1)  # Final per-sample scalar weight

Because occlusion and blur occur less frequently than expressions or illumination variations in the dataset, their mat_ratio values are smaller, resulting in larger inverse weights. This forces the optimizer to pay more attention to these challenging facial conditions during backpropagation.

Applying Weights to Landmark Regression

The final landmark loss (lines 99-101) multiplies the raw L2 error by the computed attribute weights:


# Weighted L2 loss for facial landmark regression

loss_sum = tf.reduce_sum(tf.square(landmark_batch - landmarks_pre), axis=1)
loss_sum = tf.reduce_mean(loss_sum * attributes_w_n)  # Apply per-sample weights

This ensures that prediction errors on occluded or blurry faces contribute more heavily to the total loss, compelling the network to learn features robust to these specific degradations.

Auxiliary Network for Enhanced Robustness

PFLD further improves handling of difficult facial conditions through an auxiliary network defined in model2.py (lines 18-38). This auxiliary head shares backbone features (features['auxiliary_input']) to predict Euler angles and implicitly regularize the representation learning.

By jointly optimizing for pose estimation and landmark detection, the auxiliary task provides additional supervision that helps the model disambiguate occluded or blurred facial structures based on geometric constraints rather than texture alone.

Practical Implementation Examples

Loading Training Samples with Occlusion and Blur Flags

When preparing data for training or evaluation, you can inspect the attribute flags as parsed by SetPreparation.py:

import numpy as np

# Example line from the list.txt generated by SetPreparation.save_data()

example_line = 'data/train_data/imgs/0_0.png 0.1 0.2 ... [196 landmarks] 0 0 0 0 1 0'

fields = example_line.split()
img_path = fields[0]
landmarks = np.fromstring(' '.join(fields[1:197]), sep=' ')
attributes = np.fromstring(fields[197], sep=' ', dtype=int)

# Attribute indices: 0=pose, 1=expression, 2=illumination, 3=make-up, 4=occlusion, 5=blur

print(f'Occlusion flag: {attributes[4]}')  # 1 indicates occlusion present

print(f'Blur flag: {attributes[5]}')       # 1 indicates blur present

Implementing the Weighted Loss Function

To replicate PFLD's attribute-aware weighting in a custom training loop:

import tensorflow as tf

def compute_attribute_weights(attribute_batch, batch_size):
    """
    Computes inverse-frequency weights for occlusion and blur.
    attribute_batch: tensor of shape (None, 6) [pose, expr, illum, makeup, occlusion, blur]
    """
    # Exclude pose (index 0), keep expression through blur (indices 1-5)

    attributes_w_n = tf.cast(attribute_batch[:, 1:6], tf.float32)
    
    # Calculate mean occurrence (frequency) per attribute in batch

    mat_ratio = tf.reduce_mean(attributes_w_n, axis=0)
    
    # Inverse frequency: rare attributes (occlusion, blur) get higher weight

    mat_ratio = tf.map_fn(
        lambda x: tf.cond(x > 0, lambda: 1.0 / x, lambda: float(batch_size)),
        mat_ratio
    )
    
    # Apply weights and sum across attributes to get per-sample weight

    weighted_attrs = attributes_w_n * mat_ratio
    per_sample_weight = tf.reduce_sum(weighted_attrs, axis=1)
    
    return per_sample_weight

# Usage in loss computation

landmark_batch = tf.placeholder(tf.float32, shape=(None, 196))
landmarks_pre = tf.placeholder(tf.float32, shape=(None, 196))
attribute_batch = tf.placeholder(tf.int32, shape=(None, 6))

weights = compute_attribute_weights(attribute_batch, batch_size=32)
l2_loss = tf.reduce_sum(tf.square(landmark_batch - landmarks_pre), axis=1)
weighted_loss = tf.reduce_mean(l2_loss * weights)

Running Inference on Occluded or Blurry Images

When deploying PFLD on challenging images, the pre-trained model automatically handles occlusion and blur through its learned representations:

import tensorflow as tf
import cv2
import numpy as np
from model2 import create_model
from utils import LandmarkImage

# Initialize session and model

sess = tf.Session()
image_ph = tf.placeholder(tf.float32, shape=(None, 112, 112, 3))
landmark_ph = tf.placeholder(tf.float32, shape=(None, 196))

# Create model (phase_train=False for inference)

_, landmarks_pred, _ = create_model(image_ph, landmark_ph, 
                                    phase_train=False, 
                                    args={'weight_decay': 5e-5})

# Restore checkpoint

saver = tf.train.Saver()
saver.restore(sess, tf.train.latest_checkpoint('models1/model_test'))

# Process an occluded or blurry image

img = cv2.imread('challenging_face.jpg')

# Preprocess: resize to 112x112 and normalize

proc = LandmarkImage(img).preprocess()  # Returns normalized array

# Predict landmarks (model handles occlusion/blur via trained weights)

pred = sess.run(landmarks_pred, feed_dict={image_ph: proc[None, ...]})
landmarks = pred[0].reshape(-1, 2)  # 98 points x 2 coordinates

Summary

PFLD tackles challenging facial conditions like occlusion and blur through a multi-faceted approach:

  • Explicit Attribute Annotation: The WFLW dataset provides binary flags for occlusion (bit 4) and blur (bit 5), parsed in data/SetPreparation.py and fed into the training pipeline via attribute_batch placeholders.

  • Inverse-Frequency Loss Weighting: In train_model.py, PFLD computes per-sample weights where rare conditions (occlusion and blur) receive higher weights based on their inverse occurrence frequency in the batch, forcing the optimizer to prioritize difficult samples.

  • Weighted Landmark Regression: The final L2 loss is multiplied by these attribute weights, ensuring prediction errors on occluded or blurry faces contribute more heavily to the gradient updates.

  • Auxiliary Supervision: An auxiliary network in model2.py shares backbone features to predict pose and attributes, providing additional regularization that improves robustness when texture cues are degraded by blur or occlusion.

Frequently Asked Questions

How does PFLD detect faces that are partially covered by hands or objects?

PFLD handles occlusion by training on the WFLW dataset's explicit occlusion labels (bit 4 of the attribute vector). During training in train_model.py, samples marked as occluded receive higher loss weights due to inverse-frequency weighting, forcing the network to learn more robust features for partially covered faces. The auxiliary network also helps by enforcing geometric constraints that remain valid even when texture is occluded.

Why does PFLD use inverse frequency weighting for blur and occlusion?

Inverse frequency weighting ensures that rare but critical conditions influence the training loss proportionally to their difficulty. Since occlusion and blur occur less frequently than normal expressions or poses in the WFLW dataset, their occurrence rates (mat_ratio) are lower. By computing weights as 1 / mat_ratio, PFLD assigns higher scalar weights to these samples, preventing the model from ignoring challenging cases in favor of easier, more common examples.

Can PFLD handle real-time blur and occlusion during inference?

Yes, PFLD handles challenging facial conditions at inference time through its learned representations. While the inverse-frequency weighting is only applied during training, the resulting model has learned features robust to blur and occlusion. During inference (phase_train=False in model2.py), the auxiliary network and backbone operate without the attribute weighting, but the landmark predictions remain accurate because the network was explicitly optimized to minimize errors on occluded and blurry training samples.

What is the role of the auxiliary network in handling difficult conditions?

The auxiliary network in model2.py (lines 18-38) improves robustness to occlusion and blur by providing multi-task supervision. It shares backbone features to predict Euler angles (pose) and implicitly regularizes the representation learning. When faces are blurred or occluded, texture-based features become unreliable; the auxiliary task enforces geometric consistency through pose estimation, helping the backbone maintain discriminative features for landmark localization despite degraded input quality.

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 →