# How to Export the PFLD Model for Mobile Deployment

> Learn how to export the PFLD model for mobile deployment. Convert TensorFlow checkpoints to TensorFlow Lite format for efficient on-device inference.

- Repository: [Guoqiang QI/pfld](https://github.com/guoqiangqi/pfld)
- Tags: how-to-guide
- Published: 2026-03-03

---

**To export the PFLD model for mobile deployment, restore a TensorFlow 1.x checkpoint saved by [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py), freeze the graph using `graph_util.convert_variables_to_constants`, and convert the frozen protobuf to TensorFlow Lite format using `tf.lite.TFLiteConverter`.**

The PFLD (Pose-aware Face Landmark Detection) repository by guoqiangqi provides a TensorFlow 1.x implementation for facial landmark detection. While the training pipeline saves standard checkpoints, mobile deployment requires a frozen graph converted to `.tflite` format that can be executed by the lightweight TensorFlow Lite interpreter on Android or iOS devices.

## Understanding the Checkpoint Structure

During training, [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py) persists model parameters using `tf.train.Saver`. According to the source code at lines 123-162, the saver stores trainable variables without writing meta graphs at every epoch:

```python

# train_model.py (excerpt)

save_params = tf.trainable_variables()
saver = tf.train.Saver(save_params, max_to_keep=None)
saver.save(sess, checkpoint_path, global_step=epoch, write_meta_graph=False)

```

A checkpoint directory contains three file types: `model.ckpt-<epoch>.data-00000-of-00001`, `model.ckpt-<epoch>.index`, and `model.ckpt-<epoch>.meta`. To export for mobile, you must reconstruct the inference graph defined in [`model2.py`](https://github.com/guoqiangqi/pfld/blob/main/model2.py), restore these variables, and freeze them into a standalone graph file.

## Step 1: Restore the Trained Checkpoint

First, rebuild the inference graph exactly as it was constructed during training. In [`model2.py`](https://github.com/guoqiangqi/pfld/blob/main/model2.py) (lines 11-92), the `create_model` function defines the MobilenetV2-based architecture. You must recreate the placeholder structure and call `create_model` with the same hyper-parameters used during training:

```python
import tensorflow as tf
import argparse
from model2 import create_model

def build_graph():
    # Input placeholders must match training shapes

    img_ph = tf.placeholder(tf.float32, shape=[None, 112, 112, 3], name='image_batch')
    lm_ph = tf.placeholder(tf.float32, shape=[None, 196], name='landmark_batch')
    phase_ph = tf.placeholder(tf.bool, name='phase_train')
    
    # Recreate the exact namespace used during training

    args = argparse.Namespace(
        weight_decay=5e-5,
        batch_norm_params={
            'decay': 0.995, 
            'epsilon': 0.001,
            'updates_collections': None,
            'variables_collections': [tf.GraphKeys.TRAINABLE_VARIABLES],
            'is_training': False
        })
    
    # Build inference graph (returns heatmap and landmarks)

    _, landmarks = create_model(img_ph, lm_ph, phase_ph, args)
    return landmarks

# Restore session

landmarks = build_graph()
saver = tf.train.Saver()

```

## Step 2: Freeze the Graph

After restoring the checkpoint in a session, convert all variables to constants using `tf.graph_util.convert_variables_to_constants`. This produces a single `model.pb` file containing the graph definition and embedded weights:

```python
from tensorflow.python.framework import graph_util

ckpt_path = 'models1/model_test/model.ckpt-1000'  # Adjust to your checkpoint

output_node = 'pfld_inference/fc/MatMul'         # Final landmark tensor name

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    saver.restore(sess, ckpt_path)
    
    # Freeze graph: convert variables to constants

    frozen_graph_def = graph_util.convert_variables_to_constants(
        sess,
        sess.graph_def,
        [output_node])
    
    # Write frozen graph

    with tf.io.gfile.GFile('pfld_frozen.pb', 'wb') as f:
        f.write(frozen_graph_def.SerializeToString())

```

**Identifying the output node:** The final landmark tensor is created in [`model2.py`](https://github.com/guoqiangqi/pfld/blob/main/model2.py) at line 90 within the `pfld_inference` scope. Depending on your graph inspection (TensorBoard or `print(landmarks.name)`), the output node may be named `pfld_inference/fc/MatMul` or the preceding reshape operation `pfld_inference/conv8/Flatten/Reshape`.

## Step 3: Convert to TensorFlow Lite

Feed the frozen `pfld_frozen.pb` file to `tf.lite.TFLiteConverter` to generate the mobile-ready `.tflite` file:

```python
import tensorflow as tf

converter = tf.lite.TFLiteConverter.from_frozen_graph(
    graph_path='pfld_frozen.pb',
    input_arrays=['image_batch'],          # Must match placeholder name

    output_arrays=['pfld_inference/fc/MatMul'])  # Must match output node

# Optional: Enable optimizations for smaller binary size

converter.optimizations = [tf.lite.Optimize.DEFAULT]

tflite_model = converter.convert()

with open('pfld_mobile.tflite', 'wb') as f:
    f.write(tflite_model)

```

**Post-training quantization:** For reduced latency on mobile CPUs, enable integer-only quantization by providing a representative dataset:

```python
import numpy as np

converter.representative_dataset = lambda: \
    iter([np.random.rand(1, 112, 112, 3).astype(np.float32)])
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8

```

## Complete Export Script

Combine all three steps into a single utility script that you can execute after training completes:

```python
#!/usr/bin/env python

# export_to_tflite.py

# --------------------------------------------------------------

# 1. Restore checkpoint written by train_model.py

# 2. Freeze the graph (variables → constants)

# 3. Convert to TensorFlow Lite

# --------------------------------------------------------------

import argparse
import tensorflow as tf
from tensorflow.python.framework import graph_util
from model2 import create_model
import numpy as np

def build_graph():
    img_ph = tf.placeholder(tf.float32, shape=[None, 112, 112, 3], name='image_batch')
    lm_ph = tf.placeholder(tf.float32, shape=[None, 196], name='landmark_batch')
    phase_ph = tf.placeholder(tf.bool, name='phase_train')
    
    args = argparse.Namespace(
        weight_decay=5e-5,
        batch_norm_params={
            'decay': 0.995,
            'epsilon': 0.001,
            'updates_collections': None,
            'variables_collections': [tf.GraphKeys.TRAINABLE_VARIABLES],
            'is_training': False
        })
    
    _, landmarks = create_model(img_ph, lm_ph, phase_ph, args)
    return img_ph, landmarks

def freeze_ckpt(ckpt_path, output_node):
    _, landmarks = build_graph()
    saver = tf.train.Saver()
    
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        saver.restore(sess, ckpt_path)
        
        frozen_def = graph_util.convert_variables_to_constants(
            sess, sess.graph_def, [output_node])
        
        with tf.io.gfile.GFile('pfld_frozen.pb', 'wb') as f:
            f.write(frozen_def.SerializeToString())
    print('-> Frozen graph written to pfld_frozen.pb')

def convert_tflite(output_node):
    converter = tf.lite.TFLiteConverter.from_frozen_graph(
        graph_path='pfld_frozen.pb',
        input_arrays=['image_batch'],
        output_arrays=[output_node])
    
    converter.optimizations = [tf.lite.Optimize.DEFAULT]
    tflite_model = converter.convert()
    
    with open('pfld_mobile.tflite', 'wb') as f:
        f.write(tflite_model)
    print('-> TFLite model written to pfld_mobile.tflite')

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--ckpt', 
        default='models1/model_test/model.ckpt-1000',
        help='Path to checkpoint to export')
    args = parser.parse_args()
    
    OUTPUT_NODE = 'pfld_inference/fc/MatMul'
    
    freeze_ckpt(args.ckpt, OUTPUT_NODE)
    convert_tflite(OUTPUT_NODE)

```

Run the export after training:

```bash
python export_to_tflite.py --ckpt models1/model_test/model.ckpt-1000

```

This produces `pfld_frozen.pb` for debugging and `pfld_mobile.tflite` ready for Android or iOS integration.

## Summary

- **Checkpoint restoration:** Use `tf.train.Saver` to load weights from [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py) checkpoints into the graph structure defined in [`model2.py`](https://github.com/guoqiangqi/pfld/blob/main/model2.py).
- **Graph freezing:** Convert variables to constants using `graph_util.convert_variables_to_constants` to create a standalone `model.pb` file.
- **TFLite conversion:** Use `tf.lite.TFLiteConverter.from_frozen_graph` with input array `image_batch` and output array matching the final fully connected layer in `pfld_inference`.
- **Mobile optimization:** Apply `tf.lite.Optimize.DEFAULT` or full integer quantization for faster inference on edge devices.

## Frequently Asked Questions

### What is the exact output node name for the PFLD model?

The output node depends on the final operation in [`model2.py`](https://github.com/guoqiangqi/pfld/blob/main/model2.py). According to the source at line 90, the landmarks tensor is produced by a fully connected layer scoped as `pfld_inference/fc`, resulting in a node name like `pfld_inference/fc/MatMul` or the preceding `pfld_inference/conv8/Flatten/Reshape`. Verify by printing `landmarks.name` after building the graph in Python.

### Can I deploy the PFLD model without freezing the graph?

No. TensorFlow Lite requires a frozen graph (constants only) because mobile interpreters cannot restore variable checkpoints. The freeze step (Step 2) is mandatory to embed trained weights directly into the graph definition before TFLite conversion.

### What input dimensions does the mobile model expect?

The PFLD model expects input tensors of shape `[None, 112, 112, 3]` representing batch size, height, width, and RGB channels respectively. This is defined in [`model2.py`](https://github.com/guoqiangqi/pfld/blob/main/model2.py) and must match when you specify `input_arrays=['image_batch']` during TFLite conversion.

### Is quantization supported for the PFLD TensorFlow Lite model?

Yes. The TensorFlow Lite converter supports post-training quantization for PFLD. Enable `converter.optimizations = [tf.lite.Optimize.DEFAULT]` for basic optimization, or provide a `representative_dataset` and set `inference_input_type` to `tf.uint8` for full integer quantization that reduces model size and improves CPU inference speed.