Skip to content

QA

geotoolz.qa provides sensor-specific QA-bit decoders layered on top of the generic primitives in geotoolz.cloud. The two modules share one decoder implementation:

  • geotoolz.cloud._src.array.mask_from_qa_bits — single-bit-flag decoding (OR of bits).
  • geotoolz.cloud._src.array.mask_from_scl — categorical class membership.
  • geotoolz.qa._src.array.mask_from_bit_field — contiguous multi-bit field decoding (needed for MODIS).

Pick geotoolz.cloud.MaskFromQABits / MaskFromSCL when you have an explicit list of bits / classes. Pick geotoolz.qa.LandsatQA_PIXEL / S2QA60 / S2SCL / MODISStateQA when you want the published-spec defaults.

geotoolz.qa

QA bit decoding and sensor mask presets.

The operators in this module return boolean GeoTensor masks where True means "mask this pixel out". Use them with geotoolz.cloud.ApplyMask or downstream mask-combination operators.

CloudSEN12

Bases: Operator

Placeholder for the optional ML-based CloudSEN12 detector.

Raises ImportError on call.

Examples:

>>> from geotoolz.qa import CloudSEN12
>>> CloudSEN12(checkpoint="default").get_config()
{'checkpoint': 'default'}
Source code in src/geotoolz/qa/_src/operators.py
class CloudSEN12(Operator):
    """Placeholder for the optional ML-based CloudSEN12 detector.

    Raises ``ImportError`` on call.

    Examples:
        >>> from geotoolz.qa import CloudSEN12
        >>> CloudSEN12(checkpoint="default").get_config()
        {'checkpoint': 'default'}
    """

    forbid_in_yaml: ClassVar[bool] = True

    def __init__(self, *, checkpoint: str = "default") -> None:
        self.checkpoint = checkpoint

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        raise ImportError(
            "CloudSEN12 requires the optional ML mask extra, which is not "
            "packaged in this release."
        )

    def get_config(self) -> dict[str, Any]:
        return {"checkpoint": self.checkpoint}

DecodeBitmask

Bases: Operator

Unpack a QA bitmask into named boolean mask layers.

A more general sibling of geotoolz.cloud.MaskFromQABits: instead of returning a single OR-ed mask, this operator returns a stacked multi-band boolean carrier with one layer per named entry in bits.

Parameters:

Name Type Description Default
bits Mapping[str, Sequence[int]]

Mapping from output-layer name to bit positions.

required
mode str

"any" marks a pixel when any listed bit is set; "all" requires every listed bit to be set (rare — useful for confidence-pair sub-fields).

'any'
qa_band BandSelector

Optional integer or named band selector. When omitted, the input carrier itself is treated as the QA band.

None
axis int

Position of the band axis when qa_band selects from a stack.

0

Returns:

Type Description

A multi-band boolean GeoTensor with shape

(n_layers, height, width). The output band_names attr

is set from the keys of bits.

Examples:

>>> from geotoolz.qa import DecodeBitmask
>>> # Landsat-8 QA_PIXEL — one band per flag.
>>> op = DecodeBitmask(
...     bits={"cloud": [3], "cirrus": [2], "shadow": [4]},
...     qa_band="QA_PIXEL",
... )
>>> layers = op(landsat_stack)  # (3, H, W) bool GeoTensor
Source code in src/geotoolz/qa/_src/operators.py
class DecodeBitmask(Operator):
    """Unpack a QA bitmask into named boolean mask layers.

    A more general sibling of `geotoolz.cloud.MaskFromQABits`: instead
    of returning a single OR-ed mask, this operator returns a *stacked*
    multi-band boolean carrier with one layer per named entry in
    ``bits``.

    Args:
        bits: Mapping from output-layer name to bit positions.
        mode: ``"any"`` marks a pixel when any listed bit is set;
            ``"all"`` requires every listed bit to be set (rare —
            useful for confidence-pair sub-fields).
        qa_band: Optional integer or named band selector. When omitted,
            the input carrier itself is treated as the QA band.
        axis: Position of the band axis when ``qa_band`` selects from a
            stack.

    Returns:
        A multi-band boolean ``GeoTensor`` with shape
        ``(n_layers, height, width)``. The output ``band_names`` attr
        is set from the keys of ``bits``.

    Examples:
        >>> from geotoolz.qa import DecodeBitmask
        >>> # Landsat-8 QA_PIXEL — one band per flag.
        >>> op = DecodeBitmask(
        ...     bits={"cloud": [3], "cirrus": [2], "shadow": [4]},
        ...     qa_band="QA_PIXEL",
        ... )
        >>> layers = op(landsat_stack)  # (3, H, W) bool GeoTensor
    """

    def __init__(
        self,
        *,
        bits: Mapping[str, Sequence[int]],
        mode: str = "any",
        qa_band: BandSelector = None,
        axis: int = 0,
    ) -> None:
        if not bits:
            raise ValueError("DecodeBitmask: `bits` must not be empty")
        if mode not in {"any", "all"}:
            raise ValueError("mode must be 'any' or 'all'")
        self.bits = {
            name: tuple(int(bit) for bit in bit_list) for name, bit_list in bits.items()
        }
        for name, bit_list in self.bits.items():
            if not bit_list:
                raise ValueError(f"bits[{name!r}] must not be empty")
        self.mode = mode
        self.qa_band = qa_band
        self.axis = axis

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        qa = _select_qa(gt, self.qa_band, self.axis)
        names = list(self.bits)
        layers = [
            _decode_bits_with_mode(qa, self.bits[name], self.mode) for name in names
        ]
        mask = np.stack(layers, axis=0)
        return type(gt)(
            mask,
            transform=gt.transform,
            crs=gt.crs,
            fill_value_default=False,
            attrs=_attrs_with_band_names(gt, names),
        )

    def get_config(self) -> dict[str, Any]:
        return {
            "bits": {name: list(bits) for name, bits in self.bits.items()},
            "mode": self.mode,
            "qa_band": self.qa_band,
            "axis": self.axis,
        }

LandsatQA_PIXEL

Bases: Operator

Landsat Collection-2 QA_PIXEL mask preset.

Returns True where any requested targets flag is set in QA_PIXEL. Defaults to cloud, cloud shadow, and cirrus targets (the standard "drop cloudy" mask for L8/L9).

Sensor selection: pass sensor="l7" for Landsat 4-7 Collection-2 — same bit layout as L8/L9 except bit 2 ("cirrus") is unused on TM/ETM+. For sensor="l89" (default) the full L8/L9 layout is used.

Parameters:

Name Type Description Default
qa_band int | str

Band selector for QA_PIXEL.

'QA_PIXEL'
targets Sequence[str] | None

Target flag names to OR together. See SENSOR_QA_REGISTRY["landsat_qa_pixel"] for available target names.

None
sensor str

"l89" (Landsat 8/9, default) or "l7" (Landsat 4-7).

'l89'
axis int

Band axis position.

0

Examples:

>>> from geotoolz.qa import LandsatQA_PIXEL
>>> # L8/L9 default "drop everything not clear".
>>> mask = LandsatQA_PIXEL()(landsat8_stack)
>>> # L7 — same but no cirrus bit.
>>> mask = LandsatQA_PIXEL(
...     sensor="l7", targets=["cloud", "cloud_shadow"]
... )(landsat7_stack)
References

USGS, "Landsat 8-9 Collection 2 Level-2 Science Product Guide", LSDS-1619, 2022. USGS, "Landsat 4-7 Collection 2 Level-2 Science Product Guide", LSDS-1618, 2022.

Source code in src/geotoolz/qa/_src/operators.py
class LandsatQA_PIXEL(Operator):
    """Landsat Collection-2 QA_PIXEL mask preset.

    Returns True where any requested ``targets`` flag is set in
    ``QA_PIXEL``. Defaults to cloud, cloud shadow, and cirrus targets
    (the standard "drop cloudy" mask for L8/L9).

    Sensor selection: pass ``sensor="l7"`` for Landsat 4-7
    Collection-2 — same bit layout as L8/L9 except bit 2 ("cirrus") is
    unused on TM/ETM+. For ``sensor="l89"`` (default) the full L8/L9
    layout is used.

    Args:
        qa_band: Band selector for QA_PIXEL.
        targets: Target flag names to OR together. See
            ``SENSOR_QA_REGISTRY["landsat_qa_pixel"]`` for available
            target names.
        sensor: ``"l89"`` (Landsat 8/9, default) or ``"l7"`` (Landsat
            4-7).
        axis: Band axis position.

    Examples:
        >>> from geotoolz.qa import LandsatQA_PIXEL
        >>> # L8/L9 default "drop everything not clear".
        >>> mask = LandsatQA_PIXEL()(landsat8_stack)
        >>> # L7 — same but no cirrus bit.
        >>> mask = LandsatQA_PIXEL(
        ...     sensor="l7", targets=["cloud", "cloud_shadow"]
        ... )(landsat7_stack)

    References:
        USGS, "Landsat 8-9 Collection 2 Level-2 Science Product Guide",
        LSDS-1619, 2022.
        USGS, "Landsat 4-7 Collection 2 Level-2 Science Product Guide",
        LSDS-1618, 2022.
    """

    _SENSOR_KEYS: ClassVar[dict[str, str]] = {
        "l89": "landsat_qa_pixel",
        "l7": "landsat_qa_pixel_l7",
    }
    _DEFAULT_TARGETS_L89: ClassVar[tuple[str, ...]] = (
        "cloud",
        "cloud_shadow",
        "cirrus",
    )
    _DEFAULT_TARGETS_L7: ClassVar[tuple[str, ...]] = ("cloud", "cloud_shadow")

    def __init__(
        self,
        *,
        qa_band: int | str = "QA_PIXEL",
        targets: Sequence[str] | None = None,
        sensor: str = "l89",
        axis: int = 0,
    ) -> None:
        if sensor not in self._SENSOR_KEYS:
            raise ValueError(
                f"sensor must be one of {sorted(self._SENSOR_KEYS)}; got {sensor!r}"
            )
        if targets is None:
            targets = (
                self._DEFAULT_TARGETS_L7
                if sensor == "l7"
                else self._DEFAULT_TARGETS_L89
            )
        self.qa_band = qa_band
        self.targets = tuple(str(target) for target in targets)
        self.sensor = sensor
        self.axis = axis

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        registry_key = self._SENSOR_KEYS[self.sensor]
        qa = _select_qa(gt, self.qa_band, self.axis)
        mask = _decode_targets_to_mask(qa, registry_key, self.targets)
        return gt.array_as_geotensor(mask, fill_value_default=False)

    def get_config(self) -> dict[str, Any]:
        return {
            "qa_band": self.qa_band,
            "targets": list(self.targets),
            "sensor": self.sensor,
            "axis": self.axis,
        }

MODISStateQA

Bases: Operator

MODIS state_1km (or state_500m) QA mask preset.

Returns True where any requested targets is set in the MODIS State QA band. Defaults to cloud and cloud-shadow targets.

The cloud and cirrus targets are decoded as 2-bit fields, not independent bit flags: bits [0, 1] are the cloud state (0=clear, 1=cloudy, 2=mixed, 3=not-set) and bits [8, 9] are cirrus level (0=none, 1=small, 2=average, 3=high). The default "cloud" target matches cloudy + mixed; "cirrus" matches small/average/high.

Parameters:

Name Type Description Default
qa_band int | str

Band selector for the state QA band.

'state_1km'
targets Sequence[str]

Target flag names to OR together. See SENSOR_QA_REGISTRY["modis_state_qa"].

('cloud', 'cloud_shadow')
axis int

Band axis position.

0

Examples:

>>> from geotoolz.qa import MODISStateQA
>>> # Default cloud + cloud-shadow mask.
>>> mask = MODISStateQA()(modis_state_band)
>>> # Include cirrus too.
>>> mask = MODISStateQA(targets=["cloud", "cloud_shadow", "cirrus"])(state)
References

Vermote, E. F., "MODIS Surface Reflectance User's Guide", 2015, Table 12.

Source code in src/geotoolz/qa/_src/operators.py
class MODISStateQA(Operator):
    """MODIS ``state_1km`` (or ``state_500m``) QA mask preset.

    Returns True where any requested ``targets`` is set in the MODIS
    State QA band. Defaults to cloud and cloud-shadow targets.

    The cloud and cirrus targets are decoded as *2-bit fields*, not
    independent bit flags: bits ``[0, 1]`` are the cloud state
    (0=clear, 1=cloudy, 2=mixed, 3=not-set) and bits ``[8, 9]`` are
    cirrus level (0=none, 1=small, 2=average, 3=high). The default
    "cloud" target matches cloudy + mixed; "cirrus" matches
    small/average/high.

    Args:
        qa_band: Band selector for the state QA band.
        targets: Target flag names to OR together. See
            ``SENSOR_QA_REGISTRY["modis_state_qa"]``.
        axis: Band axis position.

    Examples:
        >>> from geotoolz.qa import MODISStateQA
        >>> # Default cloud + cloud-shadow mask.
        >>> mask = MODISStateQA()(modis_state_band)
        >>> # Include cirrus too.
        >>> mask = MODISStateQA(targets=["cloud", "cloud_shadow", "cirrus"])(state)

    References:
        Vermote, E. F., "MODIS Surface Reflectance User's Guide",
        2015, Table 12.
    """

    def __init__(
        self,
        *,
        qa_band: int | str = "state_1km",
        targets: Sequence[str] = ("cloud", "cloud_shadow"),
        axis: int = 0,
    ) -> None:
        self.qa_band = qa_band
        self.targets = tuple(str(target) for target in targets)
        self.axis = axis

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        qa = _select_qa(gt, self.qa_band, self.axis)
        mask = _decode_targets_to_mask(qa, "modis_state_qa", self.targets)
        return gt.array_as_geotensor(mask, fill_value_default=False)

    def get_config(self) -> dict[str, Any]:
        return {
            "qa_band": self.qa_band,
            "targets": list(self.targets),
            "axis": self.axis,
        }

MaskCirrus

Bases: _QAMask

Return True where a QA band marks cirrus pixels.

Examples:

>>> from geotoolz.qa import MaskCirrus
>>> # Sentinel-2 QA60: bit 11 is "cirrus".
>>> mask = MaskCirrus(qa_band="QA60", bits=[11])(s2_geotensor)
Source code in src/geotoolz/qa/_src/operators.py
class MaskCirrus(_QAMask):
    """Return True where a QA band marks cirrus pixels.

    Examples:
        >>> from geotoolz.qa import MaskCirrus
        >>> # Sentinel-2 QA60: bit 11 is "cirrus".
        >>> mask = MaskCirrus(qa_band="QA60", bits=[11])(s2_geotensor)
    """

MaskCloudShadow

Bases: _QAMask

Return True where a QA band marks cloud-shadow pixels.

Examples:

>>> from geotoolz.qa import MaskCloudShadow
>>> # Landsat-8 QA_PIXEL: bit 4 is "cloud shadow".
>>> mask = MaskCloudShadow(qa_band="QA_PIXEL", bits=[4])(landsat_stack)
Source code in src/geotoolz/qa/_src/operators.py
class MaskCloudShadow(_QAMask):
    """Return True where a QA band marks cloud-shadow pixels.

    Examples:
        >>> from geotoolz.qa import MaskCloudShadow
        >>> # Landsat-8 QA_PIXEL: bit 4 is "cloud shadow".
        >>> mask = MaskCloudShadow(qa_band="QA_PIXEL", bits=[4])(landsat_stack)
    """

MaskClouds

Bases: _QAMask

Return True where a QA band marks cloud-contaminated pixels.

Examples:

>>> from geotoolz.qa import MaskClouds
>>> # Sentinel-2 QA60: bit 10 is "opaque clouds".
>>> mask = MaskClouds(qa_band="QA60", bits=[10])(s2_geotensor)
Source code in src/geotoolz/qa/_src/operators.py
class MaskClouds(_QAMask):
    """Return True where a QA band marks cloud-contaminated pixels.

    Examples:
        >>> from geotoolz.qa import MaskClouds
        >>> # Sentinel-2 QA60: bit 10 is "opaque clouds".
        >>> mask = MaskClouds(qa_band="QA60", bits=[10])(s2_geotensor)
    """

MaskNoData

Bases: Operator

Return True where pixels are no-data by QA value or carrier fill value.

Two operating modes:

  1. QA-driven: pass qa_band/bits/values to decode no-data from a dedicated QA band.
  2. Fill-driven (default): without QA arguments, pixels equal to the carrier's fill_value_default in any band are marked.

Parameters:

Name Type Description Default
qa_band BandSelector

Optional QA band selector.

None
bits Sequence[int] | None

Bit positions that mark no-data.

None
values Sequence[int] | None

Categorical values that mark no-data (e.g. SCL=0).

None
axis int

Position of the band axis.

0

Returns:

Type Description

Boolean GeoTensor mask.

Examples:

>>> from geotoolz.qa import MaskNoData
>>> # Sentinel-2 SCL: class 0 is NO_DATA.
>>> nodata = MaskNoData(qa_band="SCL", values=[0])(s2_l2a)
>>> # Fill-value fallback when no QA band is available.
>>> nodata = MaskNoData()(carrier_with_fill_value)
Source code in src/geotoolz/qa/_src/operators.py
class MaskNoData(Operator):
    """Return True where pixels are no-data by QA value or carrier fill value.

    Two operating modes:

    1. **QA-driven**: pass ``qa_band``/``bits``/``values`` to decode
       no-data from a dedicated QA band.
    2. **Fill-driven** (default): without QA arguments, pixels equal to
       the carrier's ``fill_value_default`` in *any* band are marked.

    Args:
        qa_band: Optional QA band selector.
        bits: Bit positions that mark no-data.
        values: Categorical values that mark no-data (e.g. SCL=0).
        axis: Position of the band axis.

    Returns:
        Boolean ``GeoTensor`` mask.

    Examples:
        >>> from geotoolz.qa import MaskNoData
        >>> # Sentinel-2 SCL: class 0 is NO_DATA.
        >>> nodata = MaskNoData(qa_band="SCL", values=[0])(s2_l2a)
        >>> # Fill-value fallback when no QA band is available.
        >>> nodata = MaskNoData()(carrier_with_fill_value)
    """

    def __init__(
        self,
        *,
        qa_band: BandSelector = None,
        bits: Sequence[int] | None = None,
        values: Sequence[int] | None = None,
        axis: int = 0,
    ) -> None:
        self.qa_band = qa_band
        self.bits = _normalize_int_sequence(bits, "bits")
        self.values = _normalize_int_sequence(values, "values")
        self.axis = axis

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        if self.bits is not None or self.values is not None or self.qa_band is not None:
            qa = _select_qa(gt, self.qa_band, self.axis)
            mask = _mask_from_definition(
                qa, bits=self.bits, values=self.values, mode="any"
            )
        else:
            if gt.fill_value_default is None:
                raise ValueError(
                    "MaskNoData: no qa_band/bits/values provided and the carrier "
                    "has no fill_value_default."
                )
            arr = np.asarray(gt)
            if arr.ndim <= 2:
                mask = arr == gt.fill_value_default
            else:
                mask = np.any(arr == gt.fill_value_default, axis=self.axis)
        return gt.array_as_geotensor(mask, fill_value_default=False)

    def get_config(self) -> dict[str, Any]:
        return {
            "qa_band": self.qa_band,
            "bits": None if self.bits is None else list(self.bits),
            "values": None if self.values is None else list(self.values),
            "axis": self.axis,
        }

MaskSaturated

Bases: Operator

Return True where pixels equal a saturation value.

Parameters:

Name Type Description Default
qa_band BandSelector

Optional band selector. When omitted, all bands are checked and the per-pixel OR-reduction across bands is returned.

None
saturation_value float | int | None

Explicit saturation value. If omitted for integer arrays, the dtype maximum is used; float arrays require an explicit value.

None
axis int

Position of the band axis (used for the cross-band reduction when qa_band is None).

0

Returns:

Type Description

Boolean GeoTensor mask with saturated pixels marked True.

Examples:

>>> from geotoolz.qa import MaskSaturated
>>> # uint16 Sentinel-2 — saturation_value defaults to 65535.
>>> sat = MaskSaturated()(s2_uint16_stack)
>>> # Explicit value for reflectance ratios.
>>> sat = MaskSaturated(saturation_value=1.0)(reflectance_stack)
Source code in src/geotoolz/qa/_src/operators.py
class MaskSaturated(Operator):
    """Return True where pixels equal a saturation value.

    Args:
        qa_band: Optional band selector. When omitted, all bands are
            checked and the per-pixel OR-reduction across bands is
            returned.
        saturation_value: Explicit saturation value. If omitted for
            integer arrays, the dtype maximum is used; float arrays
            require an explicit value.
        axis: Position of the band axis (used for the cross-band
            reduction when ``qa_band`` is None).

    Returns:
        Boolean ``GeoTensor`` mask with saturated pixels marked True.

    Examples:
        >>> from geotoolz.qa import MaskSaturated
        >>> # uint16 Sentinel-2 — saturation_value defaults to 65535.
        >>> sat = MaskSaturated()(s2_uint16_stack)
        >>> # Explicit value for reflectance ratios.
        >>> sat = MaskSaturated(saturation_value=1.0)(reflectance_stack)
    """

    def __init__(
        self,
        *,
        qa_band: BandSelector = None,
        saturation_value: float | int | None = None,
        axis: int = 0,
    ) -> None:
        self.qa_band = qa_band
        self.saturation_value = saturation_value
        self.axis = axis

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        arr = _select_qa(gt, self.qa_band, self.axis)
        saturation_value = self.saturation_value
        if saturation_value is None:
            if not np.issubdtype(arr.dtype, np.integer):
                raise ValueError(
                    "MaskSaturated: pass saturation_value for non-integer inputs."
                )
            saturation_value = np.iinfo(arr.dtype).max
        mask = arr == saturation_value
        if self.qa_band is None and np.asarray(gt).ndim > 2:
            mask = np.any(mask, axis=self.axis)
        return gt.array_as_geotensor(mask, fill_value_default=False)

    def get_config(self) -> dict[str, Any]:
        return {
            "qa_band": self.qa_band,
            "saturation_value": self.saturation_value,
            "axis": self.axis,
        }

MaskSnow

Bases: _QAMask

Return True where a QA band marks snow or ice pixels.

Examples:

>>> from geotoolz.qa import MaskSnow
>>> # Landsat-8 QA_PIXEL: bit 5 is "snow / ice".
>>> mask = MaskSnow(qa_band="QA_PIXEL", bits=[5])(landsat_stack)
Source code in src/geotoolz/qa/_src/operators.py
class MaskSnow(_QAMask):
    """Return True where a QA band marks snow or ice pixels.

    Examples:
        >>> from geotoolz.qa import MaskSnow
        >>> # Landsat-8 QA_PIXEL: bit 5 is "snow / ice".
        >>> mask = MaskSnow(qa_band="QA_PIXEL", bits=[5])(landsat_stack)
    """

MaskWater

Bases: _QAMask

Return True where a QA band marks water pixels.

Examples:

>>> from geotoolz.qa import MaskWater
>>> # Landsat-8 QA_PIXEL: bit 7 is "water".
>>> mask = MaskWater(qa_band="QA_PIXEL", bits=[7])(landsat_stack)
Source code in src/geotoolz/qa/_src/operators.py
class MaskWater(_QAMask):
    """Return True where a QA band marks water pixels.

    Examples:
        >>> from geotoolz.qa import MaskWater
        >>> # Landsat-8 QA_PIXEL: bit 7 is "water".
        >>> mask = MaskWater(qa_band="QA_PIXEL", bits=[7])(landsat_stack)
    """

OmniCloudMask

Bases: Operator

Placeholder for the optional ML-based OmniCloudMask detector.

Raises ImportError on call.

Examples:

>>> from geotoolz.qa import OmniCloudMask
>>> OmniCloudMask(checkpoint="default").get_config()
{'checkpoint': 'default'}
Source code in src/geotoolz/qa/_src/operators.py
class OmniCloudMask(Operator):
    """Placeholder for the optional ML-based OmniCloudMask detector.

    Raises ``ImportError`` on call.

    Examples:
        >>> from geotoolz.qa import OmniCloudMask
        >>> OmniCloudMask(checkpoint="default").get_config()
        {'checkpoint': 'default'}
    """

    forbid_in_yaml: ClassVar[bool] = True

    def __init__(self, *, checkpoint: str = "default") -> None:
        self.checkpoint = checkpoint

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        raise ImportError(
            "OmniCloudMask requires the optional ML mask extra, which is not "
            "packaged in this release."
        )

    def get_config(self) -> dict[str, Any]:
        return {"checkpoint": self.checkpoint}

S2Cloudless

Bases: Operator

Placeholder for the optional ML-based s2cloudless mask.

Raises ImportError on call. Reserved for the future [cloud-ml] extra so that pipelines can be configured today and light up once the dependency is installed.

Examples:

>>> from geotoolz.qa import S2Cloudless
>>> S2Cloudless(threshold=0.4).get_config()
{'threshold': 0.4}
Source code in src/geotoolz/qa/_src/operators.py
class S2Cloudless(Operator):
    """Placeholder for the optional ML-based s2cloudless mask.

    Raises ``ImportError`` on call. Reserved for the future
    ``[cloud-ml]`` extra so that pipelines can be configured today and
    light up once the dependency is installed.

    Examples:
        >>> from geotoolz.qa import S2Cloudless
        >>> S2Cloudless(threshold=0.4).get_config()
        {'threshold': 0.4}
    """

    # Calling this Operator raises ImportError — its `get_config` is
    # still serialisable, but actually instantiating the underlying
    # model is closure-like. Mark non-YAML so callers don't accidentally
    # depend on it round-tripping through configs in production.
    forbid_in_yaml: ClassVar[bool] = True

    def __init__(self, *, threshold: float = 0.4) -> None:
        self.threshold = threshold

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        raise ImportError(
            "S2Cloudless requires the optional ML mask extra, which is not "
            "packaged in this release."
        )

    def get_config(self) -> dict[str, Any]:
        return {"threshold": self.threshold}

S2QA60

Bases: Operator

Sentinel-2 L1C QA60 cloud + cirrus mask preset.

QA60 (per ESA's S2 L1C product specification) encodes opaque clouds in bit 10 and cirrus in bit 11. Returns True where either is set.

Note: QA60 is unreliable / zeroed-out on newer processing baselines (≥ 04.00). Prefer the L2A SCL band (S2SCL) or an ML-based detector when available.

Parameters:

Name Type Description Default
qa_band int | str

Band selector for QA60 within the input stack.

'QA60'
axis int

Band axis position.

0

Examples:

>>> from geotoolz.qa import S2QA60
>>> mask = S2QA60()(s2_l1c_stack_with_qa60_appended)
Source code in src/geotoolz/qa/_src/operators.py
class S2QA60(Operator):
    """Sentinel-2 L1C QA60 cloud + cirrus mask preset.

    QA60 (per ESA's S2 L1C product specification) encodes opaque clouds
    in bit 10 and cirrus in bit 11. Returns True where either is set.

    Note: QA60 is unreliable / zeroed-out on newer processing baselines
    (≥ 04.00). Prefer the L2A SCL band (`S2SCL`) or an ML-based
    detector when available.

    Args:
        qa_band: Band selector for QA60 within the input stack.
        axis: Band axis position.

    Examples:
        >>> from geotoolz.qa import S2QA60
        >>> mask = S2QA60()(s2_l1c_stack_with_qa60_appended)
    """

    def __init__(self, *, qa_band: int | str = "QA60", axis: int = 0) -> None:
        self.qa_band = qa_band
        self.axis = axis

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        qa = _select_qa(gt, self.qa_band, self.axis)
        mask = mask_from_qa_bits(qa, (10, 11))
        return gt.array_as_geotensor(mask, fill_value_default=False)

    def get_config(self) -> dict[str, Any]:
        return {"qa_band": self.qa_band, "axis": self.axis}

S2SCL

Bases: Operator

Sentinel-2 L2A SCL preset that masks pixels outside keep classes.

Returns True for pixels to mask: every SCL class except those named in keep. By default vegetation, soil, and water are kept.

Parameters:

Name Type Description Default
qa_band int | str

Band selector for the SCL band.

'SCL'
keep Sequence[str]

Class names to keep (do not mask). See SENSOR_QA_REGISTRY["s2_scl"] for the full vocabulary.

('vegetation', 'soil', 'water')
axis int

Band axis position.

0

Examples:

>>> from geotoolz.qa import S2SCL
>>> # Default: mask everything that isn't vegetation/soil/water.
>>> mask = S2SCL()(s2_l2a_with_scl)
>>> # Custom: keep only vegetation.
>>> mask = S2SCL(keep=["vegetation"])(s2_l2a_with_scl)
Source code in src/geotoolz/qa/_src/operators.py
class S2SCL(Operator):
    """Sentinel-2 L2A SCL preset that masks pixels outside ``keep`` classes.

    Returns True for pixels to mask: every SCL class *except* those
    named in ``keep``. By default vegetation, soil, and water are kept.

    Args:
        qa_band: Band selector for the SCL band.
        keep: Class names to keep (do not mask). See
            ``SENSOR_QA_REGISTRY["s2_scl"]`` for the full vocabulary.
        axis: Band axis position.

    Examples:
        >>> from geotoolz.qa import S2SCL
        >>> # Default: mask everything that isn't vegetation/soil/water.
        >>> mask = S2SCL()(s2_l2a_with_scl)
        >>> # Custom: keep only vegetation.
        >>> mask = S2SCL(keep=["vegetation"])(s2_l2a_with_scl)
    """

    def __init__(
        self,
        *,
        qa_band: int | str = "SCL",
        keep: Sequence[str] = ("vegetation", "soil", "water"),
        axis: int = 0,
    ) -> None:
        self.qa_band = qa_band
        self.keep = tuple(str(name) for name in keep)
        self.axis = axis

    def _apply(self, gt: GeoTensor) -> GeoTensor:
        keep_values = _registry_values("s2_scl", self.keep)
        qa = _select_qa(gt, self.qa_band, self.axis)
        # `invert=True` turns "in the keep set" into "NOT in the keep set"
        # — i.e. "mask this pixel out".
        mask = mask_from_scl(qa, keep_values, invert=True)
        return gt.array_as_geotensor(mask, fill_value_default=False)

    def get_config(self) -> dict[str, Any]:
        return {"qa_band": self.qa_band, "keep": list(self.keep), "axis": self.axis}

Sensor registry

Preset QA source Default mask targets Reference
S2QA60 Sentinel-2 L1C QA60 bitmask (bit 10 cloud, 11 cirrus) cloud + cirrus ESA S2 L1C product spec
S2SCL Sentinel-2 L2A SCL classes mask everything except vegetation (4), soil (5), water (6) Sen2Cor product spec
LandsatQA_PIXEL (sensor=l89) Landsat 8/9 C2 QA_PIXEL cloud (bit 3), cloud shadow (bit 4), cirrus (bit 2) USGS LSDS-1619
LandsatQA_PIXEL (sensor=l7) Landsat 4-7 C2 QA_PIXEL (no cirrus bit) cloud (bit 3), cloud shadow (bit 4) USGS LSDS-1618
MODISStateQA MODIS state_1km / state_500m cloud (bits [0,1] field, values 1,2) + cloud shadow (bit 2) MOD09 User's Guide, Table 12

All QA mask operators return boolean GeoTensor masks with the original CRS and transform preserved and fill_value_default=False. The convention is True means "mask this pixel out".

MODIS bit-field semantics

MODIS state_1km packs categorical fields into multi-bit slots:

  • Bits [0, 1] (2 bits): cloud state — 0=clear, 1=cloudy, 2=mixed, 3=not-set.
  • Bit 2: cloud shadow.
  • Bits [8, 9] (2 bits): cirrus level — 0=none, 1=small, 2=average, 3=high.

OR-ing the bits individually (the standard Landsat semantics) would flag value 3 (not-set) as cloudy, which is wrong. MODISStateQA therefore decodes these as field values via mask_from_bit_field.