This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 1.0.1 instead.
Reason given by maintainers: .k.
Colorist: Simple, Safe, and Overlooked — Reclaiming Sustainable Domain Generalization with Statistical Color Matching @ MICCAI DEMI 2026
[Preprint] [Publication] [PyPI] [Pretrained Weights] [Citation]
Overview
Hardware shifts, color variations, and changing patient characteristics between development and deployment routinely break trained medical image classifiers. Existing remedies fall short: standard color jittering provides insufficient diversity, while deep generative style transfer algorithms hallucinate features, destroy clinically relevant structures, and waste massive compute resources. To address this, we revisit classical statistical color matching and repurpose it as Colorist, a highly efficient data augmentation strategy that applies global mean-standard deviation matching directly in the RGB color space. We demonstrate that this training-free, fully interpretable approach safely generates structurally intact domain variations, outperforming deep generative models in structural fidelity and color alignment. Across out-of-distribution histopathology, peripheral blood, dermatology, and retinal datasets, it improves balanced accuracy by up to +9% over state-of-the-art domain generalization regularizers and by +13% over an unaugmented baseline.
Triplets of content, style, and stylized output images created by Colorist across
six distinct medical modalities: simple global RGB matching transfers photometric
shifts without corrupting clinical anatomy.
For each color channel independently, the content image's first two moments are matched to those of a randomly drawn style image:
x' = (x - mu_c) * sigma_s / (sigma_c + eps) + mu_s, eps = 1e-8
Because this is a per-pixel affine map, it preserves the spatial anatomical layout exactly: pixel rank order within a channel is unchanged, so no structure can be invented or rearranged. It is training-free, runs in O(N), needs no GPU, and drops into a standard dataloader.
On novelty. The transform itself is Reinhard et al. (2001). The contribution of this paper is empirical: that applying it in native RGB is competitive with decorrelated color spaces, and that using it as an online augmentation is effective across nineteen medical datasets.
Key Contributions
- Statistical color matching repurposed as an efficient, interpretable training-time augmentation for out-of-distribution generalization.
- A demonstration that it outperforms compute-heavy deep generative models on both color transfer quality and structural preservation, at a fraction of the energy cost.
- A systematic evaluation of thirteen color spaces and three matching methods, showing that global mean-standard deviation matching in native RGB performs competitively with decorrelated spaces while avoiding dataloader conversion bottlenecks.
- Empirical validation across 12 in-distribution and 7 covariate-shifted OOD datasets.
Installation
From PyPI
pip install colorist-aug
The distribution is named
colorist-augbecausecoloriston PyPI is an unrelated terminal-color library. The import name iscolorist. Installing both packages into one environment is not supported.
From source
git clone https://github.com/sdoerrich97/colorist.git
cd colorist
pip install -e . # library only
pip install -e ".[examples]" # + matplotlib, jupyter for examples/
pip install -e ".[weights]" # + timm, huggingface-hub for the Model Zoo
pip install -e ".[dev]" # + ruff, mypy, pytest
Requires Python >= 3.10.
Reproducing the paper's environment
requirements.lock.txt is fully pinned and hash-verified, and every version in it
equals the one in the image the published numbers were computed with. Install it in
two steps, in a clean Python 3.13 environment:
# 1. Build dependencies, with the pythran bound that actually binds.
pip install "numpy>=2.2.0,<2.3.0" "pythran<0.18.1" "Cython>=3.0.4" \
"meson-python>=0.15" ninja "setuptools>=67" wheel "packaging>=21"
# 2. The lock itself, without build isolation.
pip install --require-hashes --no-build-isolation -r requirements.lock.txt
pip install --no-deps .
Step 1 is not optional and a single pip install -r will not work. scikit-image is
held at 0.23.2 for medmnistc compatibility, that release predates Python 3.13 so it is
always compiled from source, and its pyproject declares pythran with no upper bound.
pythran >= 0.18.1 needs C++17 while scikit-image 0.23.2 compiles its pythran extensions
with a hardcoded -std=c++14, so every translation unit dies with 'is_integral_v' is not a member of 'std'. Build isolation would fetch the newest pythran regardless of what
you installed, which is why it has to be turned off. PIP_CONSTRAINT does not fix
this: pip ignores it when resolving build dependencies.
numpy is pinned in step 1 for a second reason. Without build isolation scikit-image compiles against whatever numpy is ambient, and running an extension against an older numpy than it was built for is not covered by numpy 2's forward-compatibility guarantee. Installing the runtime numpy first makes build-time and run-time numpy equal.
The Dockerfile does exactly this, and docker build --target production is the path
that needs no manual steps. Published results used Python 3.13, PyTorch 2.9.1, CUDA 12.8.
See requirements-image.txt for the environment of record and
the command that regenerates the lock from it.
Quick Start
import colorist
# The published method: global mean/std matching in RGB.
out = colorist.transfer_rgb(content, style)
# Any of the thirteen evaluated color spaces, and the three matching methods.
out = colorist.transfer_lab(content, style, method="histogram")
print(colorist.list_color_spaces())
# ['hed', 'hsd', 'hsi', 'hsv', 'lab', 'lch', 'luv', 'rgb',
# 'ycbcr', 'ydbdr', 'yiq', 'ypbpr', 'yuv']
Inputs may be PIL images, numpy arrays or torch tensors, single or batched; a single image broadcasts against a batch. The output matches the input format.
As a torchvision v2 augmentation, applied with probability 0.3 as in the paper:
from torchvision.transforms import v2
from colorist import RandomColorTransfer
transform = v2.Compose([
v2.Resize((224, 224)),
RandomColorTransfer(color_space="rgb", p=0.3),
])
See examples/ for runnable notebooks covering the API, the color spaces, dataloader integration, and loading a published classifier.
Model Zoo
Colorist itself is training-free and has no parameters, so there are no weights
for the transform. What we publish are the DenseNet121 classifiers trained with
it, one per dataset per seed, on
Hugging Face.
All were trained in the published configuration (rgb, mean_std, p = 0.3) with
the three canonical seeds 71397589, 133560673, 265017005.
from colorist import load_pretrained_classifier
model = load_pretrained_classifier("camelyon17wilds", seed=71397589)
Test balanced accuracy, mean ± sd over the three seeds. These are the runs behind Table 3 of the paper; the per-dataset means reproduce its columns.
| Domain | Dataset | Classes | Colorist | No augmentation | Identifier |
|---|---|---|---|---|---|
| Histopathology | Camelyon17-WILDS | 2 | 93.04 ± 3.90 | 62.63 | camelyon17wilds |
| Epithelium-Stroma | 2 | 86.78 ± 4.98 | 60.96 | epistr |
|
| PathMNIST | 9 | 92.69 ± 0.65 | 88.96 | pathmnist |
|
| Dermatology | Fitzpatrick17k | 3 | 39.81 ± 2.21 | 36.45 | fitzpatrick17k |
| DDI | 2 | 50.03 ± 0.36 | 48.27 | ddi |
|
| DermaMNIST | 7 | 51.80 ± 0.54 | 51.86 | dermamnist |
|
| Haematology | Bone Marrow | 13 | 45.94 ± 1.76 | 16.16 | bone_marrow_smears_and_peripheral_blood |
| Peripheral Blood | 13 | 44.22 ± 1.95 | 30.52 | peripheral_blood |
|
| BloodMNIST | 8 | 98.48 ± 0.23 | 97.93 | bloodmnist |
|
| TissueMNIST | 8 | 61.19 ± 1.54 | 61.60 | tissuemnist |
|
| Ophthalmology | Retina | 5 | 20.85 ± 4.24 | 21.73 | retina |
| RetinaMNIST | 5 | 37.71 ± 1.45 | 36.06 | retinamnist |
|
| OCTMNIST | 4 | 89.47 ± 2.80 | 88.63 | octmnist |
|
| Radiology | ChestMNIST | 14 | 94.76 ± 0.04 | 94.78 | chestmnist |
| PneumoniaMNIST | 2 | 85.37 ± 2.00 | 82.59 | pneumoniamnist |
|
| BreastMNIST | 2 | 80.01 ± 3.00 | 81.64 | breastmnist |
|
| OrganAMNIST | 11 | 93.35 ± 0.67 | 95.64 | organamnist |
|
| OrganCMNIST | 11 | 91.59 ± 1.42 | 90.83 | organcmnist |
|
| OrganSMNIST | 11 | 77.56 ± 0.37 | 73.93 | organsmnist |
Bold marks the datasets where the augmentation helps. It is not uniformly positive, and the paper does not claim it is: the gains concentrate in the covariate-shifted benchmarks, which is where photometric shift is the dominant failure mode.
Results
Deep generative style transfer corrupts clinical geometry; Colorist maintains
structural integrity while transferring the photometric style.
Repository layout
colorist/ installable library (the method)
transfer_<space>.py one module per color space
_matching.py mean_std (published), histogram, ehm
_factory.py color-space registry, pretrained-classifier loader
transforms.py torchvision v2 transforms
experiments/ reproduction code; NOT shipped in the wheel
data/ one module per dataset
metrics/ SSIM, LPIPS, FID, Wasserstein, ArtFID
reference_methods/ the 13 Table 2 baselines, one folder per method
train_orig.py Colorist classifier training
train.py the above plus transfer-strategy options
color_transfer.py color-space and fidelity evaluation
scripts/ docker drivers and table generators
examples/ runnable notebooks
assets/ figures used by this README
Reproducing the paper
All evaluations use three seeds: 71397589, 133560673, 265017005.
| Result | Run | Then build the table with |
|---|---|---|
| Table 1 — color spaces and matching methods | scripts/color_transfer/color_spaces/<space>_docker.sh (drives experiments/color_transfer.py) |
scripts/create_latex_tables/create_latex_tables_color_space.sh |
| Table 2 — structural fidelity vs. 13 generative baselines | experiments/reference_methods/pretrain.py per method, then scripts/color_transfer/<method>_docker.sh |
scripts/create_latex_tables/create_latex_tables_color_transfer.sh |
| Table 3 — downstream classification | experiments/train.py (or train_orig.py) per dataset, strategy and seed |
experiments/create_latex_table_classifier.py |
| Supplement Fig. D1 — augmentation-probability sweep | experiments/train.py across the 21 probabilities |
experiments/create_supplement_figure_probability.py |
| Supplement Table F1 — runtime, throughput, energy | scripts/inference_reporting/supplement_j_runtime.sh and reference_methods.sh |
scripts/create_latex_tables/create_latex_table_runtime.py --all |
Select the matching method with --color_transfer_method {mean_std,histogram,ehm}.
Table 2 needs each baseline's own pretrained checkpoint. We do not redistribute
those; obtain them from the upstream projects listed in NOTICE, or train
the training-required methods from a VGG-19 initialisation with
experiments/reference_methods/pretrain.py.
Two things about the supplement artifacts are easy to get wrong, so both generators enforce them rather than leaving them to the caller:
- The baselines do not share one input resolution. Each declares its own
native size and the harness honours it: 256 for eight methods, 224 for Contrimix,
SGViTs and StylizingViT, and 512 for Modflows and WCT2.
create_latex_table_runtime.pyprints it per row, because the two 512 methods are also the two largest energy ratios and a reader who assumes a common resolution reads that gap as method cost. - Both devices are reported, in two blocks. GPU is how the baselines are
deployed; CPU is the like-for-like comparison, since Colorist has no GPU
implementation and appears once, at the foot of the CPU block. The generator emits
both from one pass and prints numbers at a precision that scales with magnitude,
so a 21-second CPU measurement is not printed to 10 us and a 0.047 it/s throughput
is not rounded to
0.0. - The probability figure is drawn at final print size (
figsize=(4.8, 3.9), the LLNCS text block). Drawn at 11in and included at\textwidth, its 8pt legend rendered at ~3.5pt.
Running a full sweep
Two wrappers run a whole section, one per sweep, and are the intended entry point for a campaign rather than the per-space and per-method drivers:
screen -dmS colorist-runs bash scripts/color_transfer/sweep_color_spaces.sh
screen -dmS colorist-runs-E bash scripts/color_transfer/sweep_reference_methods.sh
Every path and image tag is overridable from the environment. Run them
concurrently in two screens: the 22 min per invocation in
scan_completion.py was measured under exactly that load, so it already includes
the contention, and a sequential run is not faster per invocation.
Resuming an interrupted sweep
The color-space and reference-method sweeps run for days, so they get cut short.
Both write into a merged metrics.json per dataset (one per
$COLORIST_RESULTS/<section>/<dataset>-<dataset>_pairwise/), which makes that tree
the record of what actually completed, and makes the drivers re-runnable: each one
skips any invocation already recorded and runs only the rest.
# What is left, per color space or per reference method
python3 scripts/color_transfer/scan_completion.py color_space
python3 scripts/color_transfer/scan_completion.py reference --missing
# Resume: re-run the same driver, it picks up where it stopped
export COLORIST_RESULTS=/path/to/results
bash scripts/color_transfer/color_spaces/hsv_docker.sh
COLORIST_RESULTS has no default on purpose: metrics.json merges, so a run that
lands in the wrong tree interleaves two eras in one file with nothing marking the
boundary. COLORIST_RESULTS_MERGE lets a sweep write into a fresh tree while
counting an existing tree as coverage, COLORIST_DATASETS narrows a driver to
named datasets, and COLORIST_FORCE=1 recomputes recorded invocations instead of
skipping them.
scrub_suspect_cells.py nulls recorded cells so the resume guard reports them as
missing and the driver recomputes them in place. Use it when a run produced values
you have reason to distrust: the guard treats a cell as done whenever its probe
metric is non-null, so a value that was written but is wrong gets skipped, not
corrected. It derives the suspect set from file mtimes narrowed to the keys that
were in flight, runs as a dry run by default, and --apply additionally demands
--expect N, the count the dry run printed, so a mistyped key or a wider window
aborts before anything is written.
Containers
The drivers execute inside colorist:production and the repo is not mounted:
experiments/ comes from the image, so a change to metric or evaluation code takes
effect only once the image is rebuilt. When the change is code-only, derive from the
working image rather than rebuilding the dependency set, which keeps results
comparable with what is already in the tree:
docker build -f docker/Dockerfile.code-refresh \
--build-arg BASE=colorist:production -t colorist:production-$(date +%Y%m%d) .
export COLORIST_IMAGE=colorist:production-$(date +%Y%m%d)
When the dependency set itself has to change, build the full image:
docker build --no-cache --target production -t colorist:rebuild-test .
--no-cache is not optional for a verification build. The builder cache holds
thousands of entries, and a cached build reuses the pip install -r requirements.txt layer, which is where this image's only real build hazard lives.
A cached build exits 0 without ever exercising it.
That layer installs its build dependencies explicitly and then runs pip with
--no-build-isolation. Both halves are load-bearing and are explained at the point
of use in the Dockerfile. In short: scikit-image is held at 0.23.2 for medmnistc,
that release compiles its pythran extensions as C++14, and pythran >= 0.18.1
requires C++17, so pythran has to be pinned below it. PIP_CONSTRAINT cannot do
that pinning, because pip ignores it when resolving build dependencies. numpy is
pinned in the same step so that the numpy scikit-image compiles against is the numpy
it runs against.
COLORIST_IMAGE is honoured by every driver that produces a number the paper
reports: the colour-space drivers, the Table 2 method drivers, and everything under
scripts/inference_reporting/ and scripts/create_latex_tables/ (the latter runs
the significance tests inside the image, so a stale tag there means stale
statistics).
Datasets
The twelve 2D tasks of MedMNIST+ plus seven covariate-shift benchmarks:
Camelyon17-WILDS, Epithelium-Stroma, Fitzpatrick17k, DDI, peripheral blood, bone
marrow, and a retina set. See the paper for splits and sources; the loaders live in
experiments/data/. Datasets are not redistributed here — obtain each from its
original provider, under its own terms.
Development
pip install -e ".[dev]"
ruff check colorist examples && mypy colorist && pytest -q
The suite is CPU-only and runs in a few seconds. Tests that need
torch.from_numpy skip automatically when the installed torch and numpy disagree
on ABI.
The library, the examples and the tests are lint- and type-clean. ruff check .
across the whole tree is not: the experiments/ harness carries about 2000
pre-existing findings, overwhelmingly cosmetic (trailing whitespace on blank
lines, List instead of list, long lines). They are not fixed here because
that reformatting would touch the code that produced the published numbers, for
no behavioural gain. The vendored baselines under
experiments/reference_methods/<method>/ are excluded from lint entirely, in
pyproject.toml: they are kept close to verbatim so they stay comparable with
upstream.
Licensing
Code authored here is MIT; see LICENSE.
The thirteen comparison baselines under experiments/reference_methods/ are third
party. NOTICE records, per method, the upstream project, its license, and
where a project publishes none. Seven of the thirteen ship a license and a verbatim
copy sits in that method's directory and governs it. Do not assume this repository's
MIT license extends to them.
Datasets carry their own terms (CC BY 4.0, CC BY-NC 4.0, CC BY-NC-SA 3.0, CC0, and custom research-use agreements) and are not redistributed here.
Citation
@article{doerrich2026colorist,
title = {Simple, Safe, and Overlooked: Reclaiming Sustainable Domain
Generalization with Statistical Color Matching},
author = {Doerrich, Sebastian and Di Salvo, Francesco and
Rai, Shyam Nandan and Lents, Marco and Ledig, Christian},
year = {2026},
eprint = {2608.18915},
archivePrefix = {arXiv},
primaryClass = {cs.CV},
url = {https://arxiv.org/abs/2608.18915}
}
Changelog
v1.0.0
Initial public release accompanying "Simple, Safe, and Overlooked: Reclaiming Sustainable Domain Generalization with Statistical Color Matching" (MICCAI DEMI 2026).
Known issue in the published results, disclosed here. compute_fid used to
return a silent fallback of 100.0 when the Inception model was unavailable or the
computation threw, instead of failing. 45 invocations in the tree behind the
manuscript carry it, so their FID, and the ArtFID derived from it, are placeholders
rather than measurements. Four rows of Table 1 are affected: HSI, YIQ, YPbPr and
YUV. This code release fixes it: compute_fid now raises MetricUnavailableError
(as does compute_gatys_style_loss, which had the same fallback at 1.0), the
caller records None, and the invocation is re-run rather than reported as
measured.
The manuscript is not revised. All of its numbers were produced with one
compute setup and are internally consistent, and the fix arrived after the
camera-ready was submitted in good faith. Recomputing the affected cells changes no
conclusion: on the authoritative n=12 MedMNIST file, 78 significance tests, 21
p-values move and no result changes significance. The quoted p = 4.88e-04, Z = -3.06, r = 0.88 and p = 0.042, Z = -2.04, r = 0.59 are unchanged to every
printed digit. The corrected MedMNIST-average ArtFIDs are HSI 49.49 → 44.67,
YIQ 37.29 → 30.64, YPbPr 37.79 → 30.61, YUV 33.00 → 30.61; HED, HSV and RGB move
only in the last printed digit, and CIELAB, LCH, LUV and YCbCr do not move.
scan_completion.py --fid-fallbacks lists any remaining stand-in, and
--merge-with refuses to count one as coverage.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file colorist_aug-1.0.0.tar.gz.
File metadata
- Download URL: colorist_aug-1.0.0.tar.gz
- Upload date:
- Size: 74.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
031e7562e960c15365b2eb4ce1de97a88dbc08270d64af9ed7ac87841deb2f61
|
|
| MD5 |
2711b246106d809090b687ddee309219
|
|
| BLAKE2b-256 |
e34518bf057df375a5a06bdabcc9cf7e44f0f6650659bdbe93ff7bae839de67e
|
File details
Details for the file colorist_aug-1.0.0-py3-none-any.whl.
File metadata
- Download URL: colorist_aug-1.0.0-py3-none-any.whl
- Upload date:
- Size: 45.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a06b809ec7c3fd98ef4b69e0d1425a41e1e0f87d89c786654cdd54bf5946c767
|
|
| MD5 |
c083db2d86bb803c7007f3b62495de90
|
|
| BLAKE2b-256 |
2b84a0a7b39f2e19efeabeac241104f18f02cc79cf59090c3088e69b8c1d9fb7
|