healpix_resample.mask (mask-like / categorical data)#
Issue #43 (“make specific resampler for
mask-like data”) asked for something better than NearestResampler’s blocky single-nearest-sample
assignment for mask-like data — e.g. a Sentinel-2 L1C cloud-mask — without pretending the underlying
values are a continuous physical field the way BilinearResampler/PSFResampler assume.
healpix_resample.mask provides two resamplers for two genuinely different kinds of “mask”, both built
on the same idea: turn each discrete thing being resampled into a 0/1 indicator array, resample every
indicator through an ordinary interpolating resampler, and turn the resulting continuous maps back into a
discrete decision.
Bits/classes |
Mutually exclusive? |
Decision |
|
|---|---|---|---|
|
Independent boolean flags packed into an integer (e.g. an 8-bit quality mask) |
No — several can co-occur |
Each bit resampled and thresholded independently |
|
One class label per sample (e.g. land-cover classification) |
Yes |
argmax over all classes’ resampled indicators |
Neither class does its own KNN/geometry work — both wrap an already fully-specified interpolating
resampler instance (kernel=, default BilinearResampler) and reuse its cell_ids/resample()
machinery, including whatever NaN filtering, out_cell_ids, or (for a PSFResampler kernel)
fill_missing_out_cells behaviour that instance already has.
BitmaskResampler — independent flags (“OR”)#
from healpix_resample import BitmaskResampler
op = BitmaskResampler(lon_deg=lon, lat_deg=lat, level=level, n_bits=8)
res = op.resample(quality_mask) # (N,) int -- e.g. an 8-bit cloud/quality mask
output_mask = res.cell_data # (K,) int -- reassembled bitmask per HEALPix cell
For each bit b in range(n_bits), builds the indicator ((mask >> b) & 1) and resamples all n_bits
indicators in a single batched call to kernel ((n_bits, N) -> (n_bits, K)). Each bit’s resampled
fraction is thresholded at bit_threshold (default 0.5) independently of every other bit, then the
surviving bits are recombined into an integer bitmask. There is no argmax here: any combination of bits
can end up set in the output, exactly as in the input — that’s the point for flags that co-occur (cloud
and cloud-shadow and saturated, all at once).
n_bits is required, not auto-detected from max(mask): a bit that never happens to be set in one
particular resample() call (or in one parent-cell subset, if combined with subset_for_parent_cell)
would otherwise silently vanish from the output instead of correctly coming back all-zero.
bit_threshold and the choice of kernel#
With the default BilinearResampler, a bit’s resampled value is a proper weighted fraction of “how many
nearby samples have this bit set”, bounded in [0, 1] — so bit_threshold=0.5 cleanly means “the
majority of nearby samples have this bit set”. BicubicResampler (signed kernel) and PSFResampler
(CG-solved) can both produce values outside [0, 1], which weakens — without breaking — that “majority”
interpretation: the threshold still picks a definite bit value, just not necessarily exactly “more than
half of nearby samples”.
CategoricalResampler — mutually-exclusive classes (“AND”/dominant class)#
from healpix_resample import CategoricalResampler
op = CategoricalResampler(lon_deg=lon, lat_deg=lat, level=level)
res = op.resample(land_cover_class) # (N,) int -- one class label per sample
output_class = res.cell_data # (K,) int -- winning class per HEALPix cell
Distinct classes are discovered from mask itself on every call (torch.unique, sorted ascending) — a
one-hot indicator is built per class, all of them resampled in one batched call to kernel
((n_classes, N) -> (n_classes, K)), and the winning class per cell is whichever indicator scored
highest: argmax_over_bilinear, the issue’s own working name, with the default kernel.
Ties are broken deterministically: the lowest-valued tied class wins. Exact ties are rare in practice (they require perfect geometric symmetry between two classes’ local support).
Optional softmax score (return_scores=True)#
res = op.resample(land_cover_class, return_scores=True, softmax_temperature=0.1)
res.cell_data # (K,) winning class, same as without return_scores
res.classes # (n_classes,) class labels, in res.scores's row order
res.scores # (n_classes, K) softmax-normalized score per class per cell
return_scores=True returns a CategoricalResampleResults (a ResampleResults subclass) with two extra
fields: classes (the distinct labels found) and scores (a softmax over the raw per-class indicator
scores). This serves two purposes: a graceful, continuous alternative to the hard argmax tie-break, and a
confidence-style diagnostic per cell (related to issue #4,
“confidence factor”).
softmax_temperature (default 0.1) controls how sharply the softmax favours the argmax winner: lower
values sharpen towards a one-hot at the winning class, higher values spread mass across close
runners-up. The default is tuned for BilinearResampler’s natural [0, 1]-ish score scale — a bare
softmax (temperature 1) over scores already confined to such a narrow range would barely sharpen
anything, giving near-uniform “probabilities” even for a clear winner. Retune if you change kernel or
your classes are unusually balanced/imbalanced.
Choice of kernel and what the scores mean#
With the default BilinearResampler, per-class scores are bounded in [0, 1] and — because every
sample belongs to exactly one class, so its one-hot indicator sums to 1 — sum to (very close to) 1
across classes for every retained cell, by construction of BilinearResampler.M’s per-cell
normalization. That makes the raw scores already a reasonable probability-like quantity even before the
softmax step. BicubicResampler and PSFResampler don’t carry this guarantee (scores can be negative or
exceed 1), so argmax — the decision that actually matters — remains meaningful with either, but
return_scores’s softmax output is a softer, less strictly calibrated confidence signal for those two
kernels than for BilinearResampler.
Both classes: limitations#
maskmust be 1-D(N,)— batched(B, N)categorical/bitmask input isn’t supported (different rows could have different classes/bits present, which complicates the class-discovery step in ways this first version doesn’t attempt to resolve).maskmust not contain NaN — a mask value has no well-defined “missing” decomposition into indicators the way a continuous NaN sample does for the interpolating resamplers elsewhere in this package. If you have a “no-data” indicator, encode it as its own class/bit instead.Any extra keyword arguments passed to
resample()(e.g.lam/tol/max_iterfor aPSFResamplerkernel) are forwarded to the wrapped kernel’s ownresample().