Transforms¶
Header-aware preprocessing for FITS images and tables.
When to use¶
- High-dynamic-range visualization (arcsinh, zscale, log/sqrt stretches)
- Model input scaling that you want reusable across a Dataset
- FITS BSCALE / null hygiene (
FITSHeaderScale,TNullToNan, column scale)
When not to¶
- You need raw ADU / physical values as stored — use
read_tensor/table.read - One-off arithmetic on a single tensor — plain PyTorch is enough
- Catalog filtering — use
table.read(..., where=)(C++ pushdown), not transforms
Wire a pipeline into training with FitsTensorDataset(..., transform=pipeline)
or call it on a tensor from read_tensor. See Data module for
when to introduce a Dataset / make_loader.
RGB
Lupton asinh RGB ships via torchfits.transforms.lupton_rgb (same as
torchfits convert … --to png).
All transforms implement the FITSTransform callable protocol
(forward / inverse / __call__). They are not
torch.nn.Module subclasses — wrap with :func:as_module (or
:class:AsModule) for nn.Sequential. Inverse state is
instance-local (_last_* fields); construct one pipeline per worker
when num_workers > 0.
from torchfits.transforms import ArcsinhStretch, BackgroundSubtract, Compose, ZScaleNormalize
pipeline = Compose([BackgroundSubtract(), ArcsinhStretch(a=0.1), ZScaleNormalize()])
normalized = pipeline(image)
restored = pipeline.inverse(normalized)
Masks¶
Most transforms accept an optional boolean mask (True = valid).
Compose forwards the same mask to every child:
pipeline(image, mask=finite_mask)
pipeline.inverse(normalized, mask=finite_mask)
Invertibility¶
| Kind | inverse() |
|---|---|
| Stretches, normalizers, FITS scale | Yes (state cached on the instance) |
SigmaClip, AsymmetricSigmaClip, TNullToNan |
No — lossy / many-to-one |
Stateless stretches are the most likely to work under torch.compile;
data-dependent normalizers cache Python-side state and may graph-break.
There is no certified compile matrix yet.
The implementation lives under torchfits.transforms as a small package
(stretch, normalize, fits_meta, clip, rgb) re-exported from
torchfits.transforms.
Stretches¶
Stateless, always invertible. Apply non-linear flux scaling for visualization.
ArcsinhStretch(a=1.0)¶
Lupton+ (2004) arcsinh stretch — LSST/SDSS standard.
Inverse: \(x = \frac{\sinh(\text{output} \cdot \operatorname{arcsinh}(a))}{a}\)
| Param | Default | Description |
|---|---|---|
a |
1.0 |
Softening parameter — smaller values are more linear near zero |
When to use
The default choice for astronomical image display. Handles the huge dynamic range of sky images gracefully — stars and galaxies both remain visible.
LogStretch(a=1000.0, eps=1e-9)¶
Logarithmic stretch for heavy-tailed flux distributions.
Inverse: \(x = \frac{10^{\text{output} \cdot \log_{10}(1 + a)} - 1}{a}\)
| Param | Default | Description |
|---|---|---|
a |
1000.0 |
Scale factor before log — larger compresses low flux more gently |
eps |
1e-9 |
Floor to prevent log(0) |
When to use
When you need stronger compression than arcsinh, e.g., for very wide-field images with bright stars and faint diffuse emission.
SqrtStretch()¶
Square-root stretch — stabilizes Poisson variance.
Inverse: \(x = \text{output}^2\)
When to use
Quick and simple. Good default for photon-counting data where Poisson statistics apply.
Normalizers¶
Data-dependent, invertible. Compute statistics from the image and cache them for inverse transforms.
ZScaleNormalize(contrast=0.25, dim=(-2, -1))¶
IRAF zscale auto-contrast algorithm. Maps display range to [0, 1].
Both clamped to \([x_{\min}, x_{\max}]\).
Inverse: \(x = \text{output} \cdot (z_2 - z_1) + z_1\)
| Param | Default | Description |
|---|---|---|
contrast |
0.25 |
Controls how aggressively to trim outliers — smaller = tighter range |
dim |
(-2, -1) |
Dimensions for statistics |
When to use
The standard choice for astronomical image display. Automatically adapts to the dynamic range of the data. Use for quick visualization or when you want a [0, 1] normalized image that preserves relative contrast.
RobustNormalize(dim=(-2, -1))¶
Subtract median, divide by MAD-derived standard deviation.
where \(\text{MAD} = \text{median}(|x - \text{median}(x)|)\).
Inverse: \(x = \text{output} \cdot \text{std\_approx} + \text{median}\)
| Param | Default | Description |
|---|---|---|
dim |
(-2, -1) |
Dimensions for statistics |
When to use
Universal ML preprocessing. Produces zero-mean, unit-variance-like data robust to outliers. Use as the last step before feeding to a neural network.
BackgroundSubtract(dim=(-2, -1))¶
Subtract median background level.
Inverse: \(x = \text{output} + \text{median}\)
| Param | Default | Description |
|---|---|---|
dim |
(-2, -1) |
Dimensions for background estimation |
When to use
First step in most image pipelines. Removes the constant sky background before stretching or normalization.
PercentileClipNormalize(lower_pct=1, upper_pct=99, dim=(-2, -1))¶
Clip to percentile range, scale to [0, 1].
Inverse: \(x = \text{output} \cdot (\text{upper} - \text{lower}) + \text{lower}\)
| Param | Default | Description |
|---|---|---|
lower_pct |
1.0 |
Lower percentile |
upper_pct |
99.0 |
Upper percentile |
dim |
(-2, -1) |
Dimensions for quantile computation |
When to use
More aggressive than zscale — hard-clips outliers. Good for display when you know the percentile range of "interesting" data.
MinMaxNormalize(dim=(-2, -1))¶
Min-max normalization to [0, 1] with ULP-safe epsilon.
Inverse: \(x = \text{output} \cdot (v_{\max} - v_{\min}) + v_{\min}\)
| Param | Default | Description |
|---|---|---|
dim |
(-2, -1) |
Dimensions for min/max |
When to use
Simple normalization when you know the data has no outliers. Avoid for astronomical images — a single bright star dominates the range.
GlobalScalarNorm(stat="median", dim=None)¶
Divide by a single scalar statistic. Minimal linear prep.
where scalar is one of: median(x), max(x), mean(x), or
\(\sqrt{\text{mean}(x^2)}\) (RMS).
Inverse: \(x = \text{output} \times \text{scalar}\)
| Param | Default | Description |
|---|---|---|
stat |
"median" |
"median", "max", "mean", or "rms" |
dim |
None |
Dimensions (None = all) |
When to use
When you want the simplest possible normalization — just scale by the typical value. Good for quick per-image scaling before comparison.
Outlier Rejection¶
SigmaClip(n_sigma=3.0, max_iter=5, dim=(-2,-1), fill="mean")¶
Iterative sigma-clipping with mean or median fill.
- Compute mean \(\mu\) and std \(\sigma\) over dims.
- Mask pixels where \(|x - \mu| > n_\sigma \cdot \sigma\).
- Repeat until convergence or
max_iter. - Replace clipped values with surviving mean or median.
Inverse: None (lossy).
| Param | Default | Description |
|---|---|---|
n_sigma |
3.0 |
Clipping threshold |
max_iter |
5 |
Max iterations |
dim |
(-2, -1) |
Dimensions for statistics |
fill |
"mean" |
"mean" or "median" replacement |
When to use
Standard for cleaning cosmic rays and hot pixels from images. Use
fill="median" for more robust replacement in the presence of many
outliers.
AsymmetricSigmaClip(n_low=3.0, n_high=3.0, dim=(-2,-1))¶
One-pass asymmetric sigma-clip via estimate_background (median + MAD).
Outliers replaced with median.
Inverse: None (lossy).
| Param | Default | Description |
|---|---|---|
n_low |
3.0 |
Std below median to clip |
n_high |
3.0 |
Std above median to clip |
dim |
(-2, -1) |
Dimensions for statistics |
When to use
Faster than iterative sigma-clip. Use different n_low/n_high when
the outlier distribution is asymmetric (e.g., bright stars are more
common than dark holes).
FITS Metadata¶
FITSHeaderScale(bscale=1.0, bzero=0.0)¶
Apply/remove FITS BSCALE/BZERO linear scaling.
Inverse: \(x = \frac{\text{output} - \text{BZERO}}{\text{BSCALE}}\)
| Param | Default | Description |
|---|---|---|
bscale |
1.0 |
FITS BSCALE keyword |
bzero |
0.0 |
FITS BZERO keyword |
Factory: FITSHeaderScale.from_header(header) — extracts BSCALE/BZERO from
a FITS header dict.
FITSScaleColumns(scales)¶
Per-column BSCALE/BZERO for table tensors.
Inverse: \(x[c] = \frac{\text{output}[c] - \text{TZERO}_c}{\text{TSCAL}_c}\)
| Param | Default | Description |
|---|---|---|
scales |
(required) | dict[str, (TSCAL, TZERO)] |
Factory: FITSScaleColumns.from_header(header).
TNullToNan(nulls)¶
Map FITS TNULL sentinels to NaN in table columns.
Inverse: None (lossy).
| Param | Default | Description |
|---|---|---|
nulls |
(required) | dict[str, TNULL_value] |
Factory: TNullToNan.from_header(header).
FITSHeaderNormalize(header, scale_floats=False)¶
Auto-normalize from BITPIX/BSCALE/BZERO. Integer types mapped to [0, 1].
- Integer types (BITPIX 8/16/32/64): min-max normalize using the native range scaled by BSCALE/BZERO.
- Float types (BITPIX -32/-64): identity unless
scale_floats=True.
Inverse: Yes (for normalized types).
| Param | Default | Description |
|---|---|---|
header |
(required) | FITS header dict |
scale_floats |
False |
Also normalize float data |
Utility¶
Compose(transforms)¶
Chain transforms; inverse() unwinds in reverse order.
pipeline = Compose([
BackgroundSubtract(),
ArcsinhStretch(a=0.1),
ZScaleNormalize(),
])
normalized = pipeline(image)
original = pipeline.inverse(normalized)
FITSTransform¶
Base class for custom transforms. Override forward() and optionally
inverse(). __call__ delegates to forward(). Not an nn.Module.
as_module(transform) / AsModule¶
Wrap a FITSTransform as a thin nn.Module for nn.Sequential:
import torch.nn as nn
from torchfits.transforms import ArcsinhStretch, as_module
model = nn.Sequential(as_module(ArcsinhStretch(a=0.1)), nn.Linear(64, 10))
Only the forward pass is exposed; call transform.inverse on the wrapped
instance for undo.
lupton_rgb(r, g, b, *, Q=8.0, stretch=0.5)¶
Lupton asinh RGB from three single-band tensors (same shape). Returns a
[H, W, 3] float tensor in [0, 1]. Matches Astropy's
make_lupton_rgb / LuptonAsinhStretch mapping. See
examples/example_lupton_rgb_sdss.py.
Writing a custom transform¶
Subclass FITSTransform, implement forward() (and inverse() if the
operation is invertible):
import torch
from torchfits.transforms import FITSTransform
class ScaleOffset(FITSTransform):
"""Affine transform: forward(x) = x * scale + offset."""
def __init__(self, scale: float, offset: float) -> None:
self.scale = scale
self.offset = offset
def forward(
self, x: torch.Tensor, mask: torch.Tensor | None = None
) -> torch.Tensor:
return x * self.scale + self.offset
def inverse(
self, x: torch.Tensor, mask: torch.Tensor | None = None
) -> torch.Tensor:
return (x - self.offset) / self.scale
Compose it with built-in transforms — Compose calls each child with the
same tensor and mask= kwarg, and unwinds inverse() in reverse order:
from torchfits.transforms import BackgroundSubtract, Compose
xf = ScaleOffset(scale=2.0, offset=-10.0)
pipeline = Compose([BackgroundSubtract(), xf])
out = pipeline(image)
restored = pipeline.inverse(out)
Wire it into a Dataset so every sample gets the same preprocessing:
from torchfits.data import FitsImageDataset
dataset = FitsImageDataset("images/*.fits", hdu=0, transform=xf)
Dict payloads vs tensor-only Compose
Compose and the built-in transforms operate on a single tensor plus an
optional mask= kwarg. A Dataset payload is a plain Tensor only when
no ivar_hdu= / mask_hdu= was requested; otherwise it is a
{"flux", "ivar"?, "mask"?} dict, and the Dataset calls
transform(payload) with that dict as the single positional argument —
it does not unpack mask into a mask= kwarg for you (see
Dataset transform= signature). A
custom transform that must handle both cases branches on isinstance(x,
dict):
def forward(self, x, mask=None):
if isinstance(x, dict):
flux = x["flux"] * self.scale + self.offset
return {**x, "flux": flux}
return x * self.scale + self.offset
Full runnable version, including the FitsImageDataset wiring above:
examples/example_custom_transform.py.
Helpers¶
| Function | Role |
|---|---|
safe_arcsinh, safe_log |
Numerically stable stretch primitives |
estimate_background |
Shared background estimator for normalizers / clip |
zscale_limits |
IRAF-style zscale limit finder used by ZScaleNormalize |
Importing¶
Import transform classes from torchfits.transforms (namespace-only since
0.9.2 — they are not re-exported at the package root):
from torchfits.transforms import (
ArcsinhStretch, AsModule, BackgroundSubtract, Compose, ZScaleNormalize,
RobustNormalize, MinMaxNormalize, PercentileClipNormalize,
LogStretch, SqrtStretch, GlobalScalarNorm,
AsymmetricSigmaClip, SigmaClip,
FITSScaleColumns, TNullToNan, FITSHeaderNormalize,
as_module, lupton_rgb,
)
examples/example_transforms.py (image pipeline) and
examples/example_lupton_rgb_sdss.py (Lupton RGB) for runnable demos.