ATLAS (Adaptive Transfer of Low-rank Abundance Splatting) compresses hyperspectral images (HSI) with low-rank spectral Gaussian splatting.
It first decomposes a data cube with NMF into a small set of endmember spectra and low-rank
abundance maps, then represents those abundance maps as a compact set of optimizable 2D
Gaussians. Each Gaussian stores only k abundance coefficients (k is about 10-12) rather
than the full l spectral bands, which is what makes Gaussian splatting possible in HSI.
The trained representation is itself the compressed file for that cube.
ATLAS builds on LoR-SGS (IEEE TGRS 2025) and ports the GaussianImage++ (AAAI 2026) techniques that transfer to the HSI domain: distortion-driven densification, two-stage LSQ+ quantization, learnable endmembers, and gradient-guided initialization. It drops the ones that do not (content-aware filters, position and progressive quantization, spatial delta coding). On four benchmarks it beats published LoR-SGS on 6 of 8 rate-distortion targets, by +1.20 dB PSNR on average.
ATLAS is a per-image representation, like an INR. You train it on the cube you want to compress, and there is no pretrained model to download.
The pipeline figure from the paper. NEW marks the techniques ATLAS adds to LoR-SGS, and sections (a) to (c) match the three steps below.
- Decompose. NMF factors the cube
X ≈ A·Einto abundance mapsAand endmembersE(rankk ≪ l). - Splat. Fit
N2D Gaussians (position, Cholesky covariance,kabundances) to reconstructA. D3 growsNby placing Gaussians at high-error pixels. - Quantize and encode. Two-stage QAT applies LSQ+ (FP16 positions, signed Cholesky, unsigned abundances), degenerate Gaussians are pruned, and parameters are entropy-coded.
ATLAS needs an NVIDIA GPU and PyTorch 2.10 or newer built with CUDA 12 or 13.
# 1. PyTorch built for your CUDA version (here CUDA 13.0; use cu128 for CUDA 12.8)
pip install torch --index-url https://download.pytorch.org/whl/cu130
# 2. ATLAS
pip install atlas-hsi
# optional extras:
pip install "atlas-hsi[viz]" # matplotlib plotting helpers
pip install "atlas-hsi[demo]" # Streamlit demo appOn Linux (x86_64) and Windows, the package includes the CUDA kernel prebuilt for CUDA 12 and 13, so nothing compiles and you do not need a CUDA toolkit. The prebuilt kernel runs on GPUs from the RTX 20 series (compute capability 7.5) onward, and with CUDA 12 also on V100 (7.0). We tested it with PyTorch built for CUDA 12.8 and 13.0.
On other platforms, or when you install from source, the kernel compiles the first time you
train or decode (12 s on our desktop), and later runs reuse it. Compiling needs a CUDA
toolkit (nvcc) with the same major version as PyTorch's CUDA, and on Windows also the MSVC
compiler from Visual Studio Build Tools.
pip install "git+https://github.com/vclab/atlas-hsi" # latest development version
pip install -e ".[viz,dev]" # local development, from a cloneIf training cannot find CUDA, or the kernel will not load, run:
atlas doctorIt reports your PyTorch build and GPU, and whether a prebuilt kernel matches it. Without a
prebuilt kernel it also checks the CUDA toolkit, ninja, and on Windows MSVC, and prints
what is missing. It finds the toolkit through CUDA_HOME, nvcc on PATH, or the
default install folders (/usr/local/cuda* on Linux). Add --build to load the kernel
right away, compiling it first if needed, as a final check.
# 1. NMF initialization, once per dataset/rank. Writes data/init/*.npy
atlas init --input paviau --rank 12
# 2. Train (FP32). The checkpoint in atlas_runs/ is the compressed representation.
atlas train --input paviau --num_points 14500 --iterations 7000
# 3. Recommended pipeline: D3 densification, two-stage QAT, learnable endmembers, SAM
atlas train --input paviau --num_points 7250 --max_num_points 14500 \
--grow_stop_iter 2000 --iterations 10500 --qat_start 5250 \
--prune_interval 100 --endmember_lr 1e-4 --sam_weight 0.05 \
--cholesky_bits 8
# A quantized (QAT) run also writes a compressed bitstream at <logdir>/compressed.atlas
# 4. Decompress a bitstream back into a cube (.npy). --reference reports reconstruction PSNR
atlas decompress atlas_runs/HSI/.../compressed.atlas -o recon.npy --reference paviauYou can also pass a path to your own cube instead of a dataset name:
atlas init --input /path/to/cube.mat --rank 12
atlas train --input /path/to/cube.mat --num_points 8000 --iterations 7000import atlas
cube = atlas.load_cube("paviau") # (H, W, C), normalized
E = atlas.load_endmember("paviau", rank=12) # (rank, channels)
A0 = atlas.load_abundance("paviau", rank=12) # for gradient-guided init
gt = atlas.to_tensor(cube) # (1, C, H, W)
result = atlas.train_nd(gt, endmember=E, image_name="paviau",
iterations=7000, num_points=14500, A0=A0)
print(result["psnr"], result["bpppb"])The optional [viz] extra adds matplotlib helpers under atlas.viz. They operate on plain
numpy arrays and need no GPU, so you can inspect cubes, abundance maps, and endmembers on a
machine without CUDA:
from atlas import viz
cube = atlas.load_cube("jasperridge")
E = atlas.load_endmember("jasperridge", rank=10)
A0 = atlas.load_abundance("jasperridge", rank=10) # flattened (H*W, k)
H, W = cube.shape[:2]
viz.show_pseudo_rgb(cube, show=True) # quick RGB preview
viz.show_abundance_maps(A0, shape=(H, W), show=True) # grid of abundance maps
viz.plot_endmembers(E, show=True) # endmember spectra
viz.plot_spectra(cube, recon, n=3, show=True) # orig vs recon signatures
viz.show_error_map(cube, recon, metric="sam", show=True) # per-pixel error map
viz.plot_rd_curve({"ATLAS": [(0.10, 37.2), (0.06, 31.5)]}, show=True) # RD curveEvery helper returns (fig, ax) so you can compose or save them; pass show=True to display
immediately. show_abundance_maps accepts (H, W, k), (k, H, W), or flattened (H*W, k)
(the NMF init layout, with shape=(H, W)).
| Flag | Default | Description |
|---|---|---|
--input / --dataset |
required | Dataset name (urban/salinas/jasperridge/paviau) or .mat/.npy path |
--iterations |
8000 | Training iterations |
--num_points |
required | Number of Gaussians (start count if D3 enabled) |
--max_num_points |
=num_points | D3: grow to this many Gaussians |
--grow_stop_iter |
5000 | D3: stop densifying at this iter |
--qat_start |
0 | Iteration to begin two-stage QAT (0 = FP32, no quantization) |
--prune_interval |
100 | Prune degenerate Gaussians every N iters during QAT |
--endmember_lr |
0 | Learnable-endmember LR (0 = frozen; 1e-4 recommended) |
--sam_weight |
0.0 | SAM loss weight (0.05 recommended) |
--rank |
per-dataset | NMF rank (10 for JasperRidge, else 12) |
--output-dir |
atlas_runs |
Where checkpoints and logs are written |
--abundance_bits / --cholesky_bits |
6 / 10 | advanced: LSQ+ bit-widths (used under QAT) |
--lsq-grad-scale |
off | experimental: LSQ+ gradient scaling for the quantizer scale and offset (rate-distortion-neutral with the Adan optimizer) |
Techniques the paper found do not transfer to HSI are not exposed: content-aware filters, spatial delta coding, and progressive QAT.
A QAT run writes compressed.atlas next to its checkpoints. This is the compressed file for
the cube: a binary container holding positions and the endmember as FP16, and the cholesky
covariance and abundances as LSQ+ integer codes, ANS entropy-coded. atlas decompress reads
it back and reconstructs the cube with the same forward pass, so the decoded result matches
the trained model:
atlas decompress run/compressed.atlas -o recon.npy --reference paviauThe trainer's reported bpppb is the entropy payload of this file (the on-disk file adds a
small fixed container header). For programmatic access, use
atlas.codec.write_bitstream(model, path) and atlas.codec.decompress(path).
| Dataset | Shape | Rank | Typical N | Iterations |
|---|---|---|---|---|
| PaviaU | 340x340x103 | 12 | 10000-23000 | 7000 |
Pavia Centre (paviac) |
1096x715x102 | 12 | 36000-72000 | 12000 |
| Salinas | 180x180x204 | 12 | 5000 | 7000 |
| JasperRidge | 100x100x198 | 10 | 600-2750 | 8000 |
| Urban | 307x307x162 | 12 | 14500-29000 | 7000 |
WHU-Hi-HongHu (whu_honghu) |
940x475x270 | 12 | 28000-56000 | 12000 |
Chikusei (chikusei) |
2372x2196x128 | 16 | 80000-160000 | 10000 |
Place raw dataset files (.mat, .npy, or ENVI .bsq/.hdr) in data/raw/; atlas init
writes NMF outputs to data/init/. Override with --data-dir and --init-dir. Chikusei is
auto-cropped to its largest all-valid region; the whole cube trains in VRAM (no tiling), so it
needs a large-VRAM GPU (~24 GB) and the self-test skips it on smaller cards.
I had an LLM write all the test cases, the suite checks the codec, data loading, atlas doctor, the plots, and the CUDA
kernel. The self-test trains, compresses, and decodes every dataset you have downloaded, and
checks the PSNR and rate against recorded values. Run both from a clone of the repository:
pytest tests/ -m "not gpu" # CPU tests only
python tests/selftest.py --quick # every layer, with a short training scheduletests/README.md lists the test cases and flags, and explains how to add
a dataset.
Appendix B of the paper lists the training configuration behind each rate-distortion
target. The self-test (python tests/selftest.py) trains one operating point per dataset,
including the three larger scenes of the scaling study, and checks it against recorded
values in tests/golden.json. The downstream classification study (Appendix G) has its
own folder, reproduce/downstream/, with the scripts, the
full command list, and the per-sample records behind every number in its table.
@inproceedings{rempel2026atlas,
title = {{ATLAS}: Hyperspectral Image Compression via Adaptive Transfer of Low-rank Abundance Splatting},
author = {Rempel, Jacob and Qureshi, Faisal Z.},
booktitle = {NeurIPS 2026 Workshop on Advances in Representation Learning for Earth Observation (REO-2)},
year = {2026}
}Built on LoR-SGS (Li et al., IEEE TGRS 2025), which builds on GaussianImage (Zhang et al., ECCV 2024), and ports techniques from GaussianImage++ (Li et al., AAAI 2026).
MIT (see LICENSE), which also carries the copyright notices of the LoR-SGS and GaussianImage
code that ATLAS builds on. atlas/optimizer.py is the Adan optimizer (Garena Online), licensed
under Apache-2.0 (see LICENSE-APACHE). The CUDA kernel in atlas/_kernel/ is Apache-2.0, derived from
nerfstudio gsplat.