Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scalable Rational Revived

Color space transform (CST) research: fitting camera-RGB → XYZ correction methods (polynomial, root-polynomial, scalable-rational) on colorchecker data and evaluating them on real scenes.

Setup

uv sync

Managed with uv; uv run ... picks up the project's virtualenv automatically, no manual activation needed.

Experiments

The main experiment scripts under experiments/ fit PolynomialCST/RootPolynomialCST/ScalableRationalCST (degree 1-5, see methods/) and report mean/median/p95 of a color-difference metric (--metric: ciede2000 (default), luv, or angular).

Camera-side RGB (X) is always normalized by a single scalar, spectral.render.rgb_scale(camera_sensitivity, illuminant) — not per-channel (that would be von Kries) — before it reaches any method's fit/predict. render() itself is a raw, unnormalized spectral integral (its scale tracks illuminant power × wavelength sample count, e.g. ~[18, 9800] for a typical camera under D65); left that large, high-degree polynomial features (X**5) push the least-squares feature matrix's condition number past float64's representable precision and np.linalg.lstsq silently returns a numerically meaningless fit (this is why PolynomialCST at degree ≥ 4 used to look like it was "overfitting" — it wasn't; it was a numerics bug). datasets/chart.py's make_chart_dataset and experiments/hsi_cst.py's render_test_pixels both apply the same scalar for a given (camera, illuminant), so a model's training and test data always share the same scale. XYZ-side y is left unscaled.

Output conventions

Every experiment script writes both a granular per-combo (per-camera, often per-source too) file and a top-level summary.csv/combined_*.csv for each -o invocation - the granular files are as much a real result as the summary, not scratch: they're what a summary was computed from, and they're the only place a per-camera breakdown survives (every top-level summary averages across camera, among other axes).

When identifying which camera a file or row belongs to, always read it from the camera column already present in every granular CSV - never parse it out of the filename. Camera names aren't underscore-safe for that (e.g. Sony_DXC-930 itself contains an underscore), so positional filename parsing silently misparses it.

results/ in this repo is a pre-computed, trimmed set of outputs kept as tracked source (not .gitignored) — every result cited in the paper is traceable to a file under it. Detailed, per-pixel/per-scene granular CSVs that aren't cited anywhere (scene galleries, per-color/per-scene breakdowns, raw Monte-Carlo traces) were pruned to keep the tree small; everything that remains is regeneratable by rerunning the commands below. Table below maps each paper result to the file(s) it comes from:

Paper result File(s) under results/
Table 2 (LOO accuracy on DC/SG/Munsell), Table 7 (solver choice) training_sweep_{dc,sg,munsell}/
Section 3.2 paired Wilcoxon significance paired_significance/
Table 3 (real hyperspectral scenes) hsi_cst/*_summary.csv (+ per camera×illuminant granular CSVs)
Table 4 (FLOPs, throughput), Fig. 3 benchmark_inference/inference_performance.csv
Table 5 (sensor-noise robustness) noise_sweep/chart-loo_*.csv
Table 6 (analytic vs. Monte-Carlo noise-gain) noise_deformation/combined_mc_summary.csv, *_mc_summary.csv
Fig. 2, Fig. 3 (regenerated) experiments/paper_figures.py, fed by training_sweep_* and benchmark_inference
Table 1 (candidate/retained feature counts) methods/*.py + data/derived_bases/ (not under results/)

hsi_training_sweep/ (solver/precision sweep on HSI data) isn't directly cited in the paper but backs the "single fixed solver costs nothing" claim of Section 4.5, evaluated there on chart data via training_sweep_*/Table 7.

chart_cst_training_sweep.py — colorchecker-only accuracy, leave-one-out

Trains and tests on the same colorchecker (DC, SG, and/or Munsell), using leave-one-out cross-validation, across every (method, degree, precision, solver) combination in experiments/common.py's PRECISION_CONFIGS × SOLVERS — this is the canonical source for chart-only accuracy numbers (there used to be a separate chart_cst.py with a narrower, in-sample-only evaluation; it was removed as a duplicate — this script's solver=lstsq_numpy, train_dtype=float64 rows are exactly that comparison, done properly, plus a measured cond_number per (method, degree) and every other solver/precision choice alongside it).

uv run python experiments/chart_cst_training_sweep.py -o results/training_sweep
uv run python experiments/chart_cst_training_sweep.py -o results/training_sweep --chart dc --metric luv
uv run python experiments/chart_cst_training_sweep.py -o results/training_sweep --solvers lstsq_numpy,ridge_1e-3,qr

Writes one CSV per camera×chart plus summary.csv (mean over cameras and charts) into -o. See experiments/common.py's SOLVERS for the available solver/regularization choices.

hsi_cst.py — train on a colorchecker, test on real hyperspectral scenes

For a given hyperspectral dataset (CAVE, KAUST, or Doomer) and colorchecker: for every camera × illuminant ("source") pair, trains on the colorchecker rendered under that camera+source, then tests against real scenes from the dataset rendered under the same camera+source and normalized the same way (see the scalar-normalization note above — this is not per-channel von Kries). Results are averaged over sources, then over cameras.

uv run python experiments/hsi_cst.py -o results/hsi_cst --dataset cave --chart dc
uv run python experiments/hsi_cst.py -o results/hsi_cst --dataset kaust --chart sg --take-every 8
uv run python experiments/hsi_cst.py -o results/hsi_cst --dataset doomer --chart dc

Required flags: --dataset {cave,kaust,doomer}, --chart {dc,sg}. dc and sg are run as separate invocations — results for different colorcheckers are not averaged together.

Doomer is different from CAVE/KAUST: its scenes are real captures, not pure reflectance — each HSI cube is calibrated radiance with a real, already baked-in illuminant (values well above 1, not bounded to [0, 1]). Most scenes also contain a gray calibration ball (mask); since its true reflectance is known (data/doomer/ball_reflectance.npy), DoomerDataset estimates that scene's illuminant from the ball region and divides it out, recovering a reflectance cube that plugs into the same rendering pipeline as CAVE/KAUST unchanged. Scenes without a ball annotation (~28% of them) are dropped since there is then nothing to estimate the illuminant from.

--take-every N (default 8) spatially subsamples each scene's pixels by stride N before rendering (image[::N, ::N]) — this keeps the KAUST run (409 scenes, ~28GB) tractable. Lower it for more test pixels at the cost of runtime/memory; N must be ≥ 1.

Dataset roots default to $HSI_DATA_ROOT/CAVE / $HSI_DATA_ROOT/KAUST / $HSI_DATA_ROOT/doomer/fullsize_hsi (HSI_DATA_ROOT env var, falling back to data/hyperspectral/) but can be overridden by constructing CaveDataset(root=...) / KaustDataset(root=...) / DoomerDataset(root=...) directly (datasets/hyperspectral.py) if scripting against them from elsewhere. Neither CAVE, KAUST, nor Doomer scene data is bundled in this repo — fetch them from their respective publishers and point HSI_DATA_ROOT (or the per-dataset root=) at wherever you put them. Likewise the Munsell reflectance set (datasets/chart.py's MUNSELL_PATH, override via the MUNSELL_PATH env var) isn't bundled.

Cameras used: canon600d, Sony_DXC-930 (bundled), canon60d (bundled). The single canonical list lives in experiments/common.py (CAMERAS); every experiment script imports it from there rather than declaring its own copy.

experiments/common.py also declares EXTRA_CAMERAS (45 more real sensitivities — phones, DSLRs, industrial cameras — used internally to derive data/derived_bases/*.npz, see below) and ALL_CAMERAS = CAMERAS + EXTRA_CAMERAS. Only the 3 cameras in CAMERAS are bundled in this public copy's data/sensitivities/; EXTRA_CAMERAS names are kept for provenance but their CSVs aren't included, so anything iterating ALL_CAMERAS (e.g. experiments/derive_orthonormal_basis.py, some tests) needs those files supplied separately to run. This was also a deliberate default even with the files present: every camera-iterating experiment would get ~16x more expensive at 48 cameras instead of 3 (the LOO sweep in chart_cst_training_sweep.py goes from minutes to hours; the HSI sweeps, whose one existing 4-camera/4-solver bake-off already took ~26h, would become infeasible).

Orthonormal feature basis (data/derived_bases/)

experiments/derive_orthonormal_basis.py pools RPCC/SRCC candidate features across all 48 ALL_CAMERAS sensitivities, all 9 illuminants, and all 3 chart targets to derive the well-conditioned projection basis used everywhere in Section 3 (the max-cond basis referenced throughout the paper). The resulting data/derived_bases/orthonormal_bases*.npz files are bundled pre-computed, so every other experiment/table in this repo reproduces without rerunning it. Rerunning the derivation itself needs the 45 EXTRA_CAMERAS sensitivity CSVs, which (see above) aren't bundled here.

Sources (illuminants) used: all of A, C, D50, D55, D65, D75, LED-B2, FL12, HP1 (datasets/chart.py's SOURCES).

Writes, per invocation: one CSV per camera×source, one ..._<camera>_summary.csv per camera (mean over sources), and one ..._summary.csv (mean over sources and cameras) — all prefixed {dataset}_{chart}_... in -o.

For a first look or a quick sanity check on a couple of scenes rather than the full dataset, slice the dataset before calling run() directly instead of adding a CLI flag for it, e.g.:

from experiments.hsi_cst import run, DATASETS
ds = DATASETS["kaust"]()
ds.filepaths = ds.filepaths[:2]
DATASETS["kaust"] = lambda: ds
run("kaust", "dc", "ciede2000", take_every=8, outdir=...)

(Note: experiments/ isn't an installed package, so from experiments.hsi_cst import ... only works if something already put the repo root on sys.path; running the module directly via uv run python experiments/hsi_cst.py is the normal path and needs no such workaround.)

Tests

uv run pytest

Dataset-dependent tests (CAVE/KAUST/Doomer, and cameras from EXTRA_CAMERAS not bundled in this copy) skip automatically if the relevant files aren't present.

About

Code and results for paper Scalable-Rational Color Correction

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages