Official code release for our IEEE TPAMI 2026 paper. π Paper: Bridging Information Asymmetry: A Hierarchical Framework for Blind Face Restoration with Reduced Uncertainty (arXiv:2601.19506)
This repository contains the official training and inference code for Pref-Restore, a hierarchical framework for blind face restoration that bridges the information asymmetry between a degraded input and its high-quality target via reinforcement-learningβbased preference optimization.
Blind face restoration aims to reconstruct a detailed, high-quality face from a severely degraded input. The fundamental difficulty is information asymmetry: the sparse low-quality (LQ) input carries far less information than the dense high-quality (HQ) target, turning restoration into an ill-posed one-to-many problem that yields uncertainty and artifacts.
Pref-Restore is a hierarchical framework that integrates discrete semantic logic with continuous texture generation, and attacks the asymmetry from two complementary directions:
The result achieves state-of-the-art performance across both synthetic and real-world benchmarks.
This codebase requires two separate Python environments, because the SFT and RL stages depend on incompatible versions of torch / accelerate / deepspeed.
| Env | Used by | Python | Torch | Key packages |
|---|---|---|---|---|
art-fr | SFT training (blip3o/) + base inference | 3.11 | 2.4 + cu124 | accelerate==0.28.0, deepspeed==0.14.4, transformers==4.51.3, diffusers==0.34.0 |
DiffusionNFT | preference-RL training (DiffusionNFT/) + RL / LoRA inference | 3.10 | 2.6 + cu126 | accelerate==1.4.0, deepspeed==0.16.4, transformers==4.40.0, diffusers==0.33.1, flash-attn==2.7.4.post1, peft==0.10.0 |
Which env do I need? Look at the top of
artfr-run.shβ every command block is preceded by the rightconda activateline.
art-fr (SFT + base inference)conda create -n art-fr python=3.11 -y
conda activate art-fr
# PyTorch 2.4 + CUDA 12.4 (match your driver)
pip install torch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 \
--index-url https://download.pytorch.org/whl/cu124
# Project deps
pip install -r requirements.txt
# Install the BLIP-3o-NEXT package (this repo) and our modified BasicSR
pip install -e .
pip install -e BasicSR
DiffusionNFT (preference-RL training + RL / LoRA inference)conda create -n DiffusionNFT python=3.10 -y
conda activate DiffusionNFT
# PyTorch 2.6 + CUDA 12.6 (match your driver)
pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 \
--index-url https://download.pytorch.org/whl/cu126
# Install DiffusionNFT (pulls in flash-attn, deepspeed, peft, etc.)
pip install -e DiffusionNFT
# Also install this repo so the inference scripts can `import blip3o`
pip install -e .
Everything below is gitignored and must be downloaded locally. Grab only what your target step needs:
| To run⦠| You need |
|---|---|
| Inference | a trained checkpoint (your Stage-A SFT / Stage-B RL output) + your own LQ images |
| SFT training (Stage A) | β Backbone components + β’ Datasets |
| Preference-RL training (Stage B) | your SFT checkpoint + β‘ Reward models + β’ Datasets |
The model is built from three separately-downloaded pieces. The BLIP-3o-NEXT backbone is not self-contained β its code loads the TA-Tok tokenizer and the SANA decoder from external paths (the SFT scripts wire all three):
| Component | Download from | Wired via (in scripts/sft_step*.sh) | Role |
|---|---|---|---|
| BLIP3o-NEXT-SFT-3B (multimodal LLM backbone) | HF: BLIP3o/BLIP3o-NEXT-SFT-3B (code) | --model_name_or_path ( PRETRAINED_MODEL= ) | auto-regressive backbone |
ta_tok.pth (TA-Tok image tokenizer) | the TA-Tok / BLIP-3o-NEXT release | --vision_tower ( VISION_MODEL= ) β must be passed externally | image tokenizer |
| SANA 1.5 diffusion decoder (a diffusers folder) | Efficient-Large-Model / SANA1.5 | --diffusion_name_or_path ( DIFFUSION= ) | diffusion decoder |
You only need these three to train Stage A from scratch. For inference and RL (Stage B) you pass your trained Stage-A SFT checkpoint as
--model_path/model_name_or_pathβ it loads the fully-assembled model, so you do not re-supply the raw backbone, TA-Tok, or SANA files.
DiffusionNFT/reward_ckpts/All reward loaders resolve paths through DiffusionNFT/flow_grpo/reward_ckpt_path.py, which defaults to <repo>/DiffusionNFT/reward_ckpts/. Download each model into the exact local subpath shown below and you're done β no code edits needed.
DiffusionNFT/reward_ckpts/
βββ laion/CLIP-ViT-H-14-laion2B-s32B-b79K/ β PickScore backbone (used by PickScoreScorer)
βββ yuvalkirstain/PickScore_v1/ β PickScore preference head
βββ openai/clip-vit-large-patch14/ β used by ClipScorer
βββ HPS_v2.1_compressed.pt β HPSv2.1 weight
βββ open_clip_pytorch_model.bin β HPSv2 OpenCLIP backbone
βββ sac+logos+ava1-l14-linearMSE.pth β aesthetic scorer (optional)
βββ VQFR_metric_paper/ β only for the GT-aware reward (ArcFace)
βββ arcface/ β Python module (clone of ronghuaiyang/arcface-pytorch)
β βββ models/resnet.py
βββ resnet18_110.pth β ArcFace ResNet-18 identity weight
| File / folder | Download from | Used by |
|---|---|---|
laion/CLIP-ViT-H-14-laion2B-s32B-b79K/ | laion/CLIP-ViT-H-14-laion2B-s32B-b79K | PickScore (as the image/text encoder) |
yuvalkirstain/PickScore_v1/ | yuvalkirstain/PickScore_v1 | PickScore reward |
openai/clip-vit-large-patch14/ | openai/clip-vit-large-patch14 | CLIP-score reward |
HPS_v2.1_compressed.pt | tgxs002/HPSv2 release | HPSv2 reward |
open_clip_pytorch_model.bin | bundled with the HPSv2 release | HPSv2 backbone |
sac+logos+ava1-l14-linearMSE.pth | LAION-AI/aesthetic-predictor | aesthetic scorer (optional) |
VQFR_metric_paper/arcface/ (python module) | ronghuaiyang/arcface-pytorch | ArcFace identity reward (GT-aware config only) |
VQFR_metric_paper/resnet18_110.pth | TencentARC/VQFR β metric_weights | ArcFace identity reward (GT-aware config only) |
Default config
pref_restore_multi_rewarduses PickScore + HPSv2 + CLIPScore β you can ignore the ArcFace + VQFR rows. The GT-aware configpref_restore_gt_reward(paper default) additionally needs the twoVQFR_metric_paper/...items, plus an LMD weight that the GT config points to β seeDiffusionNFT/config/pref_restore_gt.py.Keep your weights elsewhere? Export
PREF_RESTORE_REWARD_CKPT_DIR=/your/abs/pathbefore launching training, and the loaders will read from that directory instead. For ArcFace you can additionally pointPREF_RESTORE_ARCFACE_ROOT/PREF_RESTORE_ARCFACE_WEIGHTat non-default locations.
| Data | Download from | Used by |
|---|---|---|
| FFHQ-256 / FFHQ-512 | FFHQ | SFT + RL β HQ targets |
| CelebA-HQ | CelebA-HQ | SFT + RL β train / val |
| FFHQ-512 + captions (the exact split we used) | HF: Ryan-sjtu/ffhq512-caption | drop-in (HQ image + caption) for Stage A |
| FFHQ + LLaVA short captions (the exact split we used) | HF: irodkin/ffhq_with_llava_shorter_captions | drop-in (HQ image + caption) for Stage A |
Our PhaseA caption manifest (long_captions.json) | HF: zjyao-PKU/Pref-Restore-Data β PhaseA/long_captions.json | the (HQ-image-basename, caption) pairs we use in Stage A |
Our PhaseB RL metadata ({train,test}_metadata.jsonl) | HF: zjyao-PKU/Pref-Restore-Data β PhaseB/restore_face_codeformer/ | RL prompts + (LQ, GT) image basenames for Stage B |
| Real-world FR test sets (LFW / WebPhoto / WIDER / CelebChild) or your own photos | standard blind-FR benchmarks | inference inputs |
You only need high-quality (HQ) face images + one caption per image to train. The degraded low-quality (LQ) inputs are synthesized on the fly during training (blur Β· down-sampling Β· noise Β· JPEG), so you do not pre-build LQ/HQ pairs.
The SFT scripts take --data_path = a plain-text manifest (train_data*.txt). Each line is a directory path; every such directory is scanned recursively for .parquet (or .tar / WebDataset) shards:
# train_data.txt β one dataset directory per line
/your/data/FFHQ/parquet
/your/data/CelebA-HQ/parquet
Each shard must provide two columns:
| Column | Content |
|---|---|
image | the HQ face image (decoded by π€ datasets as a PIL image) |
txt | a caption describing the image (a text column is auto-renamed to txt; leave empty for caption-free data) |
At training time each HQ image is degraded on the fly and the model learns LQ β HQ; a fraction of samples keep the original image as a pure reconstruction task. The caption is woven into the instruction ~90% of the time. See blip3o/data/dataset.py (LazySupervisedRestoreDataset) for the exact logic and the degradation parameters.
Inference takes --json_path = a JSON list of objects, one per LQ image:
[
{"image": "/path/to/lq_face_001.png", "caption": "a photo of a young woman, smiling"},
{"image": "/path/to/lq_face_002.png", "caption": ""}
]
image is the LQ input path; caption is optional (use "" if you have none). To synthesize LQ test images from HQ photos, use process_image_degradation.py. Restored images are written to --output_dir.
The full pipeline is two stages. See artfr-run.sh for the exact command sequence.
[env: art-fr]Two steps (toggle caption / reconstruction options in blip3o/data/dataset.py):
conda activate art-fr
bash scripts/sft_step1.sh # step 1: SFT from the BLIP3o-NEXT-SFT-3B backbone
bash scripts/sft_step2.sh # step 2: VAE encoder + diffusion head
| Step | Script | Trainer | Starts from |
|---|---|---|---|
| 1 | scripts/sft_step1.sh | blip3o/train/train_step1.py | BLIP3o-NEXT-SFT-3B (backbone + TA-Tok + SANA) |
| 2 | scripts/sft_step2.sh | blip3o/train/train_step2.py | the step-1 checkpoint |
DeepSpeed configs are under scripts/zero1.json / scripts/zero2.json.
π‘ Skip PhaseA β start straight from PhaseB. This stage is the most compute-intensive step of the whole pipeline, and we observed that restoration quality keeps improving as PhaseA training continues, with diminishing marginal returns β most of the easy gains land early; later iterations cost a lot of GPU-hours for a small numerical bump. So that the community can dive straight into PhaseB preference-RL training without re-running our SFT, we publish a PhaseA checkpoint at π€ zjyao-PKU/Pref-Restore-PhaseA-Fidelity. It is tuned to lean toward restoration fidelity and image realism, at the cost of slightly weaker aesthetic quality β exactly the trade-off you want as a base model that PhaseB's preference-RL will then push toward perceptual preference. Set
config.pretrained.model = "<local snapshot of the HF repo>"inDiffusionNFT/config/pref_restore_gt.pyand skip directly to Stage B below.
[env: DiffusionNFT]The RL trainer reads its prompt/image list from DiffusionNFT/dataset/<dataset_name>/{train,test}_metadata.jsonl (the path is constructed in DiffusionNFT/config/pref_restore_gt.py as os.path.join(cwd, f"dataset/{dataset}")). For the default GT-aware config (pref_restore_gt_reward), dataset_name = restore_face_codeformer.
1. Download the metadata from our HF dataset and put it in place:
# Inside the repo root
mkdir -p DiffusionNFT/dataset/restore_face_codeformer
# from https://huggingface.co/datasets/zjyao-PKU/Pref-Restore-Data
# PhaseB/restore_face_codeformer/train_metadata.jsonl
# PhaseB/restore_face_codeformer/test_metadata.jsonl
# -> place both files under DiffusionNFT/dataset/restore_face_codeformer/
2. JSONL format (one JSON object per line):
{"prompt": "A photograph of a person ...",
"image": "validation_104.png", // LQ input (CodeFormer-degraded face)
"gt_image": "validation_104.png", // HQ ground-truth (only in train)
"requirement": "Restore"}
image and gt_image are stored as basenames only. Place the actual image files alongside the JSONL in two sibling directories, e.g.:
DiffusionNFT/dataset/restore_face_codeformer/
βββ train_metadata.jsonl
βββ test_metadata.jsonl
βββ lq/ β put all LQ images here (matching `image`)
βββ gt/ β put all HQ images here (matching `gt_image`)
Wire lq/ and gt/ into the dataloader (or symlink them) so that <dataset_dir>/lq/<basename> and <dataset_dir>/gt/<basename> resolve to the actual files. The LQ images we used are CodeFormer-degraded CelebA-HQ faces; the GT images are the corresponding HQ originals. You can substitute your own degradation pipeline as long as the JSONL fields match.
conda activate DiffusionNFT
cd DiffusionNFT
export WANDB_PROJECT=DiffusionNFT_PrefRestore
# Default: GT-aware reward (PickScore + HPSv2 + CLIPScore + LMD + ArcFace + LPIPS)
torchrun --nproc_per_node=8 --master_port=11234 \
scripts/train_nft_prefRestore_gt.py \
--config config/pref_restore_gt.py:pref_restore_gt_reward
# Multi-reward variant (without GT-aware rewards)
torchrun --nproc_per_node=8 --master_port=11234 \
scripts/train_nft_prefRestore.py \
--config config/pref_restore.py:pref_restore_multi_reward
Before launching, open the chosen config file (e.g.
DiffusionNFT/config/pref_restore_gt.py) and edit three things:
- Base model β
config.pretrained.model = "<path to your Stage-A SFT checkpoint>"(or use ours:snapshot_download("zjyao-PKU/Pref-Restore-PhaseA-Fidelity")and pass the returned local path)- Reward weights β the
reward_fn = {...}dict (e.g.{"pickscore": 0.5, "hpsv2": 0.5, "clipscore": 1.0, "lmd": 1.0, "arcface": 1.0, "lpips": 0.5})- Dataset β the
dataset=kwarg passed to_get_config(...)(default:"restore_face_codeformer"); the trainer will readDiffusionNFT/dataset/<dataset>/{train,test}_metadata.jsonl
| Script | Config | Dataset (under DiffusionNFT/dataset/) |
|---|---|---|
scripts/train_nft_prefRestore_gt.py | config/pref_restore_gt.py:pref_restore_gt_reward | restore_face_codeformer/ (paper default) |
scripts/train_nft_prefRestore.py | config/pref_restore.py:pref_restore_multi_reward | restore_face/ |
RL checkpoints (including LoRA adapters) are written to DiffusionNFT/logs/ (gitignored).
Two entry points, matching the two stages. Both read a JSON list of LQ images and write restored images to --output_dir.
[env: art-fr]python inference_batch_noPrompt_fixLQ_vae.py \
--model_path /path/to/SFT_checkpoint \
--json_path /path/to/captions_lq.json \
--output_dir /path/to/results/base
[env: DiffusionNFT]python inference_batch_noPrompt_fixLQ_vae_lora.py \
--model_path /path/to/SFT_checkpoint \
--json_path /path/to/captions_lq.json \
--output_dir /path/to/results/rl \
--lora_path /path/to/DiffusionNFT/logs/.../checkpoints/checkpoint-XXX \
--use_lora
| Argument | Meaning |
|---|---|
--model_path | the SFT backbone checkpoint (Stage A output) |
--json_path | JSON list of low-quality input images |
--output_dir | where restored images are saved |
--lora_path | RL LoRA adapter (LoRA script only) |
--use_lora | enable the LoRA adapter (LoRA script only) |
This repository is released under the Apache License 2.0 (see LICENSE).
The inlined third-party code retains its original license:
BasicSR/ β Apache-2.0 (XPixelGroup)DiffusionNFT/ β original LICENSE preserved at DiffusionNFT/LICENSEblip3o/ β see the upstream BLIP-3o repositoryIf you find this work useful, please cite our paper:
@article{yao2026prefrestore,
title = {Bridging Information Asymmetry: A Hierarchical Framework for Deterministic Blind Face Restoration},
author = {Yao, Zhengjian and Hu, Jiakui and Li, Kaiwen and He, Hangzhou and
Zhang, Xinliang and Zeng, Shuang and Zhu, Lei and Lu, Yanye},
journal = {IEEE Transactions on Pattern Analysis and Machine Intelligence (TPAMI)},
year = {2026}
}
Preprint: arXiv:2601.19506
1 commits
Python
96.1%
Cuda
2.0%
C++
1.3%
Official code release for our IEEE TPAMI 2026 paper. π Paper: Bridging Information Asymmetry: A Hierarchical Framework for Blind Face Restoration with Reduced Uncertainty (arXiv:2601.19506)
This repository contains the official training and inference code for Pref-Restore, a hierarchical framework for blind face restoration that bridges the information asymmetry between a degraded input and its high-quality target via reinforcement-learningβbased preference optimization.
Blind face restoration aims to reconstruct a detailed, high-quality face from a severely degraded input. The fundamental difficulty is information asymmetry: the sparse low-quality (LQ) input carries far less information than the dense high-quality (HQ) target, turning restoration into an ill-posed one-to-many problem that yields uncertainty and artifacts.
Pref-Restore is a hierarchical framework that integrates discrete semantic logic with continuous texture generation, and attacks the asymmetry from two complementary directions:
The result achieves state-of-the-art performance across both synthetic and real-world benchmarks.
This codebase requires two separate Python environments, because the SFT and RL stages depend on incompatible versions of torch / accelerate / deepspeed.
| Env | Used by | Python | Torch | Key packages |
|---|---|---|---|---|
art-fr | SFT training (blip3o/) + base inference | 3.11 | 2.4 + cu124 | accelerate==0.28.0, deepspeed==0.14.4, transformers==4.51.3, diffusers==0.34.0 |
DiffusionNFT | preference-RL training (DiffusionNFT/) + RL / LoRA inference | 3.10 | 2.6 + cu126 | accelerate==1.4.0, deepspeed==0.16.4, transformers==4.40.0, diffusers==0.33.1, flash-attn==2.7.4.post1, peft==0.10.0 |
Which env do I need? Look at the top of
artfr-run.shβ every command block is preceded by the rightconda activateline.
art-fr (SFT + base inference)conda create -n art-fr python=3.11 -y
conda activate art-fr
# PyTorch 2.4 + CUDA 12.4 (match your driver)
pip install torch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 \
--index-url https://download.pytorch.org/whl/cu124
# Project deps
pip install -r requirements.txt
# Install the BLIP-3o-NEXT package (this repo) and our modified BasicSR
pip install -e .
pip install -e BasicSR
DiffusionNFT (preference-RL training + RL / LoRA inference)conda create -n DiffusionNFT python=3.10 -y
conda activate DiffusionNFT
# PyTorch 2.6 + CUDA 12.6 (match your driver)
pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 \
--index-url https://download.pytorch.org/whl/cu126
# Install DiffusionNFT (pulls in flash-attn, deepspeed, peft, etc.)
pip install -e DiffusionNFT
# Also install this repo so the inference scripts can `import blip3o`
pip install -e .
Everything below is gitignored and must be downloaded locally. Grab only what your target step needs:
| To run⦠| You need |
|---|---|
| Inference | a trained checkpoint (your Stage-A SFT / Stage-B RL output) + your own LQ images |
| SFT training (Stage A) | β Backbone components + β’ Datasets |
| Preference-RL training (Stage B) | your SFT checkpoint + β‘ Reward models + β’ Datasets |
The model is built from three separately-downloaded pieces. The BLIP-3o-NEXT backbone is not self-contained β its code loads the TA-Tok tokenizer and the SANA decoder from external paths (the SFT scripts wire all three):
| Component | Download from | Wired via (in scripts/sft_step*.sh) | Role |
|---|---|---|---|
| BLIP3o-NEXT-SFT-3B (multimodal LLM backbone) | HF: BLIP3o/BLIP3o-NEXT-SFT-3B (code) | --model_name_or_path ( PRETRAINED_MODEL= ) | auto-regressive backbone |
ta_tok.pth (TA-Tok image tokenizer) | the TA-Tok / BLIP-3o-NEXT release | --vision_tower ( VISION_MODEL= ) β must be passed externally | image tokenizer |
| SANA 1.5 diffusion decoder (a diffusers folder) | Efficient-Large-Model / SANA1.5 | --diffusion_name_or_path ( DIFFUSION= ) | diffusion decoder |
You only need these three to train Stage A from scratch. For inference and RL (Stage B) you pass your trained Stage-A SFT checkpoint as
--model_path/model_name_or_pathβ it loads the fully-assembled model, so you do not re-supply the raw backbone, TA-Tok, or SANA files.
DiffusionNFT/reward_ckpts/All reward loaders resolve paths through DiffusionNFT/flow_grpo/reward_ckpt_path.py, which defaults to <repo>/DiffusionNFT/reward_ckpts/. Download each model into the exact local subpath shown below and you're done β no code edits needed.
DiffusionNFT/reward_ckpts/
βββ laion/CLIP-ViT-H-14-laion2B-s32B-b79K/ β PickScore backbone (used by PickScoreScorer)
βββ yuvalkirstain/PickScore_v1/ β PickScore preference head
βββ openai/clip-vit-large-patch14/ β used by ClipScorer
βββ HPS_v2.1_compressed.pt β HPSv2.1 weight
βββ open_clip_pytorch_model.bin β HPSv2 OpenCLIP backbone
βββ sac+logos+ava1-l14-linearMSE.pth β aesthetic scorer (optional)
βββ VQFR_metric_paper/ β only for the GT-aware reward (ArcFace)
βββ arcface/ β Python module (clone of ronghuaiyang/arcface-pytorch)
β βββ models/resnet.py
βββ resnet18_110.pth β ArcFace ResNet-18 identity weight
| File / folder | Download from | Used by |
|---|---|---|
laion/CLIP-ViT-H-14-laion2B-s32B-b79K/ | laion/CLIP-ViT-H-14-laion2B-s32B-b79K | PickScore (as the image/text encoder) |
yuvalkirstain/PickScore_v1/ | yuvalkirstain/PickScore_v1 | PickScore reward |
openai/clip-vit-large-patch14/ | openai/clip-vit-large-patch14 | CLIP-score reward |
HPS_v2.1_compressed.pt | tgxs002/HPSv2 release | HPSv2 reward |
open_clip_pytorch_model.bin | bundled with the HPSv2 release | HPSv2 backbone |
sac+logos+ava1-l14-linearMSE.pth | LAION-AI/aesthetic-predictor | aesthetic scorer (optional) |
VQFR_metric_paper/arcface/ (python module) | ronghuaiyang/arcface-pytorch | ArcFace identity reward (GT-aware config only) |
VQFR_metric_paper/resnet18_110.pth | TencentARC/VQFR β metric_weights | ArcFace identity reward (GT-aware config only) |
Default config
pref_restore_multi_rewarduses PickScore + HPSv2 + CLIPScore β you can ignore the ArcFace + VQFR rows. The GT-aware configpref_restore_gt_reward(paper default) additionally needs the twoVQFR_metric_paper/...items, plus an LMD weight that the GT config points to β seeDiffusionNFT/config/pref_restore_gt.py.Keep your weights elsewhere? Export
PREF_RESTORE_REWARD_CKPT_DIR=/your/abs/pathbefore launching training, and the loaders will read from that directory instead. For ArcFace you can additionally pointPREF_RESTORE_ARCFACE_ROOT/PREF_RESTORE_ARCFACE_WEIGHTat non-default locations.
| Data | Download from | Used by |
|---|---|---|
| FFHQ-256 / FFHQ-512 | FFHQ | SFT + RL β HQ targets |
| CelebA-HQ | CelebA-HQ | SFT + RL β train / val |
| FFHQ-512 + captions (the exact split we used) | HF: Ryan-sjtu/ffhq512-caption | drop-in (HQ image + caption) for Stage A |
| FFHQ + LLaVA short captions (the exact split we used) | HF: irodkin/ffhq_with_llava_shorter_captions | drop-in (HQ image + caption) for Stage A |
Our PhaseA caption manifest (long_captions.json) | HF: zjyao-PKU/Pref-Restore-Data β PhaseA/long_captions.json | the (HQ-image-basename, caption) pairs we use in Stage A |
Our PhaseB RL metadata ({train,test}_metadata.jsonl) | HF: zjyao-PKU/Pref-Restore-Data β PhaseB/restore_face_codeformer/ | RL prompts + (LQ, GT) image basenames for Stage B |
| Real-world FR test sets (LFW / WebPhoto / WIDER / CelebChild) or your own photos | standard blind-FR benchmarks | inference inputs |
You only need high-quality (HQ) face images + one caption per image to train. The degraded low-quality (LQ) inputs are synthesized on the fly during training (blur Β· down-sampling Β· noise Β· JPEG), so you do not pre-build LQ/HQ pairs.
The SFT scripts take --data_path = a plain-text manifest (train_data*.txt). Each line is a directory path; every such directory is scanned recursively for .parquet (or .tar / WebDataset) shards:
# train_data.txt β one dataset directory per line
/your/data/FFHQ/parquet
/your/data/CelebA-HQ/parquet
Each shard must provide two columns:
| Column | Content |
|---|---|
image | the HQ face image (decoded by π€ datasets as a PIL image) |
txt | a caption describing the image (a text column is auto-renamed to txt; leave empty for caption-free data) |
At training time each HQ image is degraded on the fly and the model learns LQ β HQ; a fraction of samples keep the original image as a pure reconstruction task. The caption is woven into the instruction ~90% of the time. See blip3o/data/dataset.py (LazySupervisedRestoreDataset) for the exact logic and the degradation parameters.
Inference takes --json_path = a JSON list of objects, one per LQ image:
[
{"image": "/path/to/lq_face_001.png", "caption": "a photo of a young woman, smiling"},
{"image": "/path/to/lq_face_002.png", "caption": ""}
]
image is the LQ input path; caption is optional (use "" if you have none). To synthesize LQ test images from HQ photos, use process_image_degradation.py. Restored images are written to --output_dir.
The full pipeline is two stages. See artfr-run.sh for the exact command sequence.
[env: art-fr]Two steps (toggle caption / reconstruction options in blip3o/data/dataset.py):
conda activate art-fr
bash scripts/sft_step1.sh # step 1: SFT from the BLIP3o-NEXT-SFT-3B backbone
bash scripts/sft_step2.sh # step 2: VAE encoder + diffusion head
| Step | Script | Trainer | Starts from |
|---|---|---|---|
| 1 | scripts/sft_step1.sh | blip3o/train/train_step1.py | BLIP3o-NEXT-SFT-3B (backbone + TA-Tok + SANA) |
| 2 | scripts/sft_step2.sh | blip3o/train/train_step2.py | the step-1 checkpoint |
DeepSpeed configs are under scripts/zero1.json / scripts/zero2.json.
π‘ Skip PhaseA β start straight from PhaseB. This stage is the most compute-intensive step of the whole pipeline, and we observed that restoration quality keeps improving as PhaseA training continues, with diminishing marginal returns β most of the easy gains land early; later iterations cost a lot of GPU-hours for a small numerical bump. So that the community can dive straight into PhaseB preference-RL training without re-running our SFT, we publish a PhaseA checkpoint at π€ zjyao-PKU/Pref-Restore-PhaseA-Fidelity. It is tuned to lean toward restoration fidelity and image realism, at the cost of slightly weaker aesthetic quality β exactly the trade-off you want as a base model that PhaseB's preference-RL will then push toward perceptual preference. Set
config.pretrained.model = "<local snapshot of the HF repo>"inDiffusionNFT/config/pref_restore_gt.pyand skip directly to Stage B below.
[env: DiffusionNFT]The RL trainer reads its prompt/image list from DiffusionNFT/dataset/<dataset_name>/{train,test}_metadata.jsonl (the path is constructed in DiffusionNFT/config/pref_restore_gt.py as os.path.join(cwd, f"dataset/{dataset}")). For the default GT-aware config (pref_restore_gt_reward), dataset_name = restore_face_codeformer.
1. Download the metadata from our HF dataset and put it in place:
# Inside the repo root
mkdir -p DiffusionNFT/dataset/restore_face_codeformer
# from https://huggingface.co/datasets/zjyao-PKU/Pref-Restore-Data
# PhaseB/restore_face_codeformer/train_metadata.jsonl
# PhaseB/restore_face_codeformer/test_metadata.jsonl
# -> place both files under DiffusionNFT/dataset/restore_face_codeformer/
2. JSONL format (one JSON object per line):
{"prompt": "A photograph of a person ...",
"image": "validation_104.png", // LQ input (CodeFormer-degraded face)
"gt_image": "validation_104.png", // HQ ground-truth (only in train)
"requirement": "Restore"}
image and gt_image are stored as basenames only. Place the actual image files alongside the JSONL in two sibling directories, e.g.:
DiffusionNFT/dataset/restore_face_codeformer/
βββ train_metadata.jsonl
βββ test_metadata.jsonl
βββ lq/ β put all LQ images here (matching `image`)
βββ gt/ β put all HQ images here (matching `gt_image`)
Wire lq/ and gt/ into the dataloader (or symlink them) so that <dataset_dir>/lq/<basename> and <dataset_dir>/gt/<basename> resolve to the actual files. The LQ images we used are CodeFormer-degraded CelebA-HQ faces; the GT images are the corresponding HQ originals. You can substitute your own degradation pipeline as long as the JSONL fields match.
conda activate DiffusionNFT
cd DiffusionNFT
export WANDB_PROJECT=DiffusionNFT_PrefRestore
# Default: GT-aware reward (PickScore + HPSv2 + CLIPScore + LMD + ArcFace + LPIPS)
torchrun --nproc_per_node=8 --master_port=11234 \
scripts/train_nft_prefRestore_gt.py \
--config config/pref_restore_gt.py:pref_restore_gt_reward
# Multi-reward variant (without GT-aware rewards)
torchrun --nproc_per_node=8 --master_port=11234 \
scripts/train_nft_prefRestore.py \
--config config/pref_restore.py:pref_restore_multi_reward
Before launching, open the chosen config file (e.g.
DiffusionNFT/config/pref_restore_gt.py) and edit three things:
- Base model β
config.pretrained.model = "<path to your Stage-A SFT checkpoint>"(or use ours:snapshot_download("zjyao-PKU/Pref-Restore-PhaseA-Fidelity")and pass the returned local path)- Reward weights β the
reward_fn = {...}dict (e.g.{"pickscore": 0.5, "hpsv2": 0.5, "clipscore": 1.0, "lmd": 1.0, "arcface": 1.0, "lpips": 0.5})- Dataset β the
dataset=kwarg passed to_get_config(...)(default:"restore_face_codeformer"); the trainer will readDiffusionNFT/dataset/<dataset>/{train,test}_metadata.jsonl
| Script | Config | Dataset (under DiffusionNFT/dataset/) |
|---|---|---|
scripts/train_nft_prefRestore_gt.py | config/pref_restore_gt.py:pref_restore_gt_reward | restore_face_codeformer/ (paper default) |
scripts/train_nft_prefRestore.py | config/pref_restore.py:pref_restore_multi_reward | restore_face/ |
RL checkpoints (including LoRA adapters) are written to DiffusionNFT/logs/ (gitignored).
Two entry points, matching the two stages. Both read a JSON list of LQ images and write restored images to --output_dir.
[env: art-fr]python inference_batch_noPrompt_fixLQ_vae.py \
--model_path /path/to/SFT_checkpoint \
--json_path /path/to/captions_lq.json \
--output_dir /path/to/results/base
[env: DiffusionNFT]python inference_batch_noPrompt_fixLQ_vae_lora.py \
--model_path /path/to/SFT_checkpoint \
--json_path /path/to/captions_lq.json \
--output_dir /path/to/results/rl \
--lora_path /path/to/DiffusionNFT/logs/.../checkpoints/checkpoint-XXX \
--use_lora
| Argument | Meaning |
|---|---|
--model_path | the SFT backbone checkpoint (Stage A output) |
--json_path | JSON list of low-quality input images |
--output_dir | where restored images are saved |
--lora_path | RL LoRA adapter (LoRA script only) |
--use_lora | enable the LoRA adapter (LoRA script only) |
This repository is released under the Apache License 2.0 (see LICENSE).
The inlined third-party code retains its original license:
BasicSR/ β Apache-2.0 (XPixelGroup)DiffusionNFT/ β original LICENSE preserved at DiffusionNFT/LICENSEblip3o/ β see the upstream BLIP-3o repositoryIf you find this work useful, please cite our paper:
@article{yao2026prefrestore,
title = {Bridging Information Asymmetry: A Hierarchical Framework for Deterministic Blind Face Restoration},
author = {Yao, Zhengjian and Hu, Jiakui and Li, Kaiwen and He, Hangzhou and
Zhang, Xinliang and Zeng, Shuang and Zhu, Lei and Lu, Yanye},
journal = {IEEE Transactions on Pattern Analysis and Machine Intelligence (TPAMI)},
year = {2026}
}
Preprint: arXiv:2601.19506
1 commits
Python
96.1%
Cuda
2.0%
C++
1.3%