Skip to content

Repository files navigation

FFCC-Python: Fast Fourier Color Constancy

A faithful Python reimplementation of the FFCC algorithm (Barron & Tsai, CVPR 2017) for automatic white balance and illuminant estimation.

This implementation reproduces the results from the original Google FFCC MATLAB codebase, validated on the Gehler/Shi (Reprocessed) benchmark dataset.

Highlights

  • Pure Python/NumPy — no MATLAB required
  • Reproduces the MATLAB reference on Gehler/Shi to within 0.03 deg mean angular error
  • Ships the dataset, the tuned hyperparameters, and a pre-trained model, so the reported numbers come out of a single command
  • Preconditioned L-BFGS training with cross-entropy → Von Mises loss annealing
  • MATLAB-compatible featurization for exact reproduction

Quick Start

Installation

pip install -e .

Or install dependencies directly:

pip install numpy scipy        # core
pip install opencv-python      # only needed by the scripts, for image loading

Predict the illuminant of an image

import numpy as np
from ffcc import FFCCModel

# models/gehler_model.npz ships with the repo (trained on all 568 Gehler/Shi images)
model = FFCCModel()
model.load("models/gehler_model.npz")

# image: (H, W, 3) float64 linear RGB in [0, 1]
rgb_illuminant = model.predict(image)

# Apply white balance
white_balanced = image / rgb_illuminant[np.newaxis, np.newaxis, :]

Or from the command line:

python scripts/demo.py --image data/GehlerShiThumb/000001.png

Train on your own dataset

from ffcc import featurize_image, gt_rgb_to_uv, train_ffcc

# Prepare training data: list of (histogram_feature, gt_uv) tuples
train_data = []
for img, gt_rgb in your_dataset:
    X = featurize_image(img)        # (64, 64, 2) histogram
    train_data.append((X, gt_rgb_to_uv(gt_rgb)))

# Train.  The default hyperparameters are the ones tuned for Gehler/Shi;
# retune them for a different sensor (see "Hyperparameters" below).
model = train_ffcc(train_data)
model.save("my_model.npz")

Algorithm Overview

FFCC estimates a scene's global illuminant by:

  1. Log-chroma histograms: Convert the image to UV space (u = log(G/R), v = log(G/B)) and compute 2D histograms (64x64 bins) for two channels:

    • Channel 0: Original pixel chromaticities
    • Channel 1: Edge chromaticities (Masked Local Absolute Deviation)
  2. FFT convolution: Apply learned frequency-domain filters to the histograms, producing a log-probability map over possible illuminants.

  3. Von Mises fitting: Fit a bivariate Von Mises distribution (circular Gaussian on the torus) to the posterior, extracting the mean as the illuminant estimate.

  4. Training: L-BFGS in a preconditioned Fourier latent space, with an annealed loss:

    • Stage 1: Cross-entropy (convex warm-up), 16 L-BFGS iterations
    • Stage 2: Von Mises negative log-likelihood (non-convex, more accurate), 64 iterations
Image → UV Histograms → FFT Conv → Softmax → Von Mises → UV → RGB gains
         (64×64×2)      (learned)   (P map)    (fit)     (μ)

The preconditioning is not cosmetic: it whitens the Fourier-domain total-variation prior so L-BFGS sees a well-conditioned problem. Optimizing the raw FFT coefficients instead converges to a far worse model.

Benchmark Results

Gehler/Shi (Reprocessed) — 3-fold Cross-Validation

Metric MATLAB (reference) This repo Delta
Mean 1.979 2.005 +0.026
Median 1.050 1.139 +0.089
Trimean 1.312 1.344 +0.032
Best 25% 0.300 0.338 +0.038
Worst 25% 5.106 5.124 +0.018

568 images, 3 folds, 2 annealing stages. Angular error in degrees (lower is better). The MATLAB column is the rgb_err block reported in projects/GehlerShiThumb/GehlerShiThumbHyperparams.m of the reference codebase.

Reproduce the benchmark

The Gehler/Shi thumbnails (568 images, 1.4 MB) are included in data/GehlerShiThumb/, and the tuned hyperparameters are built into the package. Just run:

python scripts/benchmark_gehler.py --data-dir data/GehlerShiThumb

# Results saved to results/gehler_results.json

About a minute of training on a laptop (~20 s per fold). Featurizing the 568 thumbnails takes under a second and is cached under cache/ afterwards.

Hyperparameters

Training is sensitive to the regularizer scale across several orders of magnitude, so the tuned values ship explicitly as ffcc.GEHLER_HYPERPARAMS and are used by default:

Hyperparameter Value
CROSSENT_MULTIPLIER 2-8.25
VONMISES_MULTIPLIER 21.75
FILTER_MULTIPLIERS [2-26.25, 2-23.5]
FILTER_SHIFTS [2-60, 2-71.75]
BIAS_MULTIPLIER 2-23
BIAS_SHIFT 2-93
VON_MISES_DIAGONAL_EPS 1

These are the values tuned for Gehler/Shi in the MATLAB reference (GehlerShiThumbHyperparams.m). They are dataset- and sensor-specific — expect to retune them for your own camera. To benchmark against a different MATLAB project, point the script at that project's file:

python scripts/benchmark_gehler.py --data-dir <dir> --hyperparams-m <path/to/*Hyperparams.m>

Pre-trained model

models/gehler_model.npz is trained on all 568 images with the same recipe:

python scripts/train_full_model.py --data-dir data/GehlerShiThumb

It is there for the demo and for off-the-shelf use. It has seen the entire dataset (training-set error 1.267 deg mean), so it must not be used to report accuracy on Gehler/Shi — use the cross-validation script for that.

Project Structure

ffcc-python/
├── ffcc/                       # Core package
│   ├── __init__.py             # Public API
│   ├── core.py                 # Featurization, forward pass, Von Mises fit, metrics
│   ├── train.py                # Preconditioned L-BFGS training + tuned hyperparameters
│   └── matlab_port.py          # MATLAB-compatible featurization
├── scripts/
│   ├── benchmark_gehler.py     # GehlerShi 3-fold CV reproduction
│   ├── train_full_model.py     # Train the shipped pre-trained model
│   ├── demo.py                 # Single-image prediction demo
│   └── download_gehler.py      # Dataset download helper
├── tests/
│   ├── test_core.py            # Featurization / forward pass / metrics tests
│   └── test_train.py           # Training + hyperparameter regression tests
├── data/GehlerShiThumb/        # Gehler/Shi dataset (568 images, included)
├── models/gehler_model.npz     # Pre-trained model (full dataset)
├── pyproject.toml              # Package metadata
├── requirements.txt            # Runtime dependencies
└── README.md

API Reference

Core Functions

Function Description
featurize_image(image, mask=None) Extract 2-channel UV histogram (64×64×2)
ffcc_forward(X, F_fft, B) Forward pass: histogram → (illuminant UV, posterior)
uv_to_rgb_gains(mu_uv) Convert UV illuminant to unit-norm RGB
gt_rgb_to_uv(gt_rgb) Convert GT RGB illuminant to UV space
angular_error(pred, gt) Angular error between two RGB vectors (degrees)
train_ffcc(data, ...) Train an FFCC model (preconditioned L-BFGS)
compute_error_metrics(errors) Compute mean/median/trimean/best25/worst25

Two featurizers exist. ffcc.featurize_image is the readable float implementation; ffcc.matlab_port.featurize_image preserves the MATLAB integer arithmetic path and is what the benchmark and models/gehler_model.npz use. They are not bit-identical. Swapping them at test time costs little when training used the same one (1.818 vs 1.820 deg on fold 1), but mixing them across training and inference does cost something: the shipped model scores 1.267 deg over the 568 images with MATLAB features and 1.354 deg with the float ones. Use one featurizer consistently; if you train through ffcc.featurize_image, predict through it too (which is what FFCCModel.predict does).

FFCCModel Class

model = FFCCModel(n_channels=2, init_mode='zeros')
model.predict(image)          # End-to-end: image → RGB illuminant
model.forward(X)              # Histogram → (mu_uv, P)
model.save("model.npz")       # Save weights
model.load("model.npz")       # Load weights

train_ffcc

train_ffcc(train_data, val_data=None, hyperparams=None,
           n_anneal_stages=2, verbose=True) -> FFCCModel

n_anneal_stages is the number of loss-annealing stages, not an epoch count: each stage is one L-BFGS run whose iteration budget grows log-linearly from 16 to 64. Two stages reproduce the reported results; more stages cost proportionally more time for a marginal change. val_data does not influence training — there is no early stopping; the per-stage validation error is recorded on model.val_history.

Dependencies

  • Required: NumPy ≥ 1.21, SciPy ≥ 1.7
  • Scripts: OpenCV-Python ≥ 4.5 (for image loading)
  • Testing: pytest ≥ 7.0

Changelog

0.2.0

  • Fixed: the benchmark script's built-in hyperparameters did not match the tuned values that produced the reported results, so running the documented command gave 3.21 deg mean instead of 2.005. The tuned values now ship in the package (ffcc.GEHLER_HYPERPARAMS) and are the default.
  • Fixed: ffcc.train_ffcc was an unconditioned implementation that did not converge usefully (~8 deg mean on fold 1, worse than gray-world). It is replaced by the preconditioned trainer that produces the reported results (1.82 deg on the same fold), which previously lived only inside the benchmark script.
  • Added: models/gehler_model.npz and scripts/train_full_model.py — the quick-start example referenced a model file that was not in the repository.
  • Changed: --epochs / n_epochs renamed to --anneal-stages / n_anneal_stages, which is what the parameter actually controls. The old name was misleading: --epochs 20 and --epochs 29 gave bit-identical results.
  • Added: tests/test_train.py, including a regression test that pins the tuned hyperparameter values — the defect above would have been caught by it.

Author

Shuwei Yue / 岳书威shuwei_yue@szpu.edu.cn

WeChat Official Account: ColorWorld花花世界

Citation

If you use this code, please cite the original FFCC paper:

@inproceedings{barron2017fast,
  title={Fast Fourier Color Constancy},
  author={Barron, Jonathan T and Tsai, Yun-Ta},
  booktitle={IEEE Conference on Computer Vision and Pattern Recognition (CVPR)},
  year={2017}
}

If you find this Python reimplementation useful, a star or acknowledgment is appreciated:

@misc{yue2025ffccpython,
  title={FFCC-Python: A faithful Python reimplementation of Fast Fourier Color Constancy},
  author={Yue, Shuwei},
  year={2025},
  url={https://github.com/shuwei666/ffcc-python}
}

License

Apache License 2.0. See LICENSE for details.

Acknowledgments

About

A faithful Python reimplementation of Fast Fourier Color Constancy (FFCC)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages