Skip to content

Quality Masks

Utilities for creating quality masks and filtering Sentinel-2 scenes.

Overview

The gee_acolite.utils.masks module provides functions to create the masks used in quality control of satellite imagery, including:

  • Water / land classification
  • Cirrus cloud detection
  • TOA reflectance cloud threshold
  • Cloud probability and shadow detection
  • Negative reflectance filtering

Mask Pipeline

graph TD A[S2 Image] --> B{Mask types} B --> C[non_water] B --> D[cirrus_mask] B --> E[toa_mask] B --> F[add_cld_shdw_mask] C --> G[B11 < threshold] D --> H[B10 < threshold] E --> I[band < threshold] F --> J[Cloud prob + shadow projection] G --> K[Combined mask\nupdateMask] H --> K I --> K J --> K style A fill:#e1f5ff style K fill:#e1ffe1

Functions

mask_negative_reflectance

mask_negative_reflectance(image: Image, band: str) -> Image

Mask out negative reflectance values.

Removes pixels with negative reflectance, which are physically impossible and indicate processing errors or noise.

Parameters:

Name Type Description Default
image Image

Image with reflectance band.

required
band str

Band name to check for negative values.

required

Returns:

Type Description
Image

Masked image with only non-negative values.

Source code in gee_acolite/utils/masks.py
def mask_negative_reflectance(image : ee.Image, band : str) -> ee.Image:
    """
    Mask out negative reflectance values.

    Removes pixels with negative reflectance, which are physically
    impossible and indicate processing errors or noise.

    Parameters
    ----------
    image : ee.Image
        Image with reflectance band.
    band : str
        Band name to check for negative values.

    Returns
    -------
    ee.Image
        Masked image with only non-negative values.
    """
    return image.updateMask(image.select(band).gte(0)).rename(band)

toa_mask

toa_mask(image: Image, band: str = 'rhot_B11', threshold: float = 0.03)

Create mask based on TOA reflectance threshold.

Masks pixels with high TOA reflectance, typically indicating clouds, bright surfaces, or other anomalies.

Parameters:

Name Type Description Default
image Image

Image with TOA reflectance bands.

required
band str

Band name to check (default: 'rhot_B11' - SWIR).

'rhot_B11'
threshold float

Maximum reflectance threshold (default: 0.03).

0.03

Returns:

Type Description
Image

Binary mask (1 = valid, 0 = masked).

Source code in gee_acolite/utils/masks.py
def toa_mask(image : ee.Image, band : str = 'rhot_B11', threshold : float = 0.03):
    """
    Create mask based on TOA reflectance threshold.

    Masks pixels with high TOA reflectance, typically indicating
    clouds, bright surfaces, or other anomalies.

    Parameters
    ----------
    image : ee.Image
        Image with TOA reflectance bands.
    band : str, optional
        Band name to check (default: 'rhot_B11' - SWIR).
    threshold : float, optional
        Maximum reflectance threshold (default: 0.03).

    Returns
    -------
    ee.Image
        Binary mask (1 = valid, 0 = masked).
    """
    return image.select(band).lt(threshold)

cirrus_mask

cirrus_mask(image: Image, band: str = 'rhot_B10', threshold: float = 0.005)

Create cirrus cloud mask.

Uses cirrus band (1375nm) to detect high-altitude cirrus clouds.

Parameters:

Name Type Description Default
image Image

Image with cirrus band.

required
band str

Cirrus band name (default: 'rhot_B10').

'rhot_B10'
threshold float

Maximum cirrus reflectance (default: 0.005).

0.005

Returns:

Type Description
Image

Binary mask (1 = no cirrus, 0 = cirrus detected).

Source code in gee_acolite/utils/masks.py
def cirrus_mask(image : ee.Image, band : str = 'rhot_B10', threshold : float = 0.005):
    """
    Create cirrus cloud mask.

    Uses cirrus band (1375nm) to detect high-altitude cirrus clouds.

    Parameters
    ----------
    image : ee.Image
        Image with cirrus band.
    band : str, optional
        Cirrus band name (default: 'rhot_B10').
    threshold : float, optional
        Maximum cirrus reflectance (default: 0.005).

    Returns
    -------
    ee.Image
        Binary mask (1 = no cirrus, 0 = cirrus detected).
    """
    return image.select(band).lt(threshold)

non_water

non_water(image: Image, band: str = 'rhot_B11', threshold: float = 0.05) -> Image

Create water/land classification mask.

Uses SWIR reflectance to distinguish water from land. Water has low SWIR reflectance due to strong absorption.

Parameters:

Name Type Description Default
image Image

Image with SWIR band.

required
band str

SWIR band name (default: 'rhot_B11').

'rhot_B11'
threshold float

Maximum reflectance for water (default: 0.05).

0.05

Returns:

Type Description
Image

Binary mask (1 = water, 0 = land).

Source code in gee_acolite/utils/masks.py
def non_water(image : ee.Image, band : str = 'rhot_B11', threshold : float = 0.05) -> ee.Image:
    """
    Create water/land classification mask.

    Uses SWIR reflectance to distinguish water from land.
    Water has low SWIR reflectance due to strong absorption.

    Parameters
    ----------
    image : ee.Image
        Image with SWIR band.
    band : str, optional
        SWIR band name (default: 'rhot_B11').
    threshold : float, optional
        Maximum reflectance for water (default: 0.05).

    Returns
    -------
    ee.Image
        Binary mask (1 = water, 0 = land).
    """
    return image.select(band).lt(threshold)

add_cloud_bands

add_cloud_bands(img: Image, cloud_prob_threshold: float = 50) -> Image

Add cloud mask band based on cloud probability.

Uses Sentinel-2 Cloud Probability dataset to identify cloudy pixels.

Parameters:

Name Type Description Default
img Image

Image with 'cloud_prob' property containing Cloud Probability image.

required
cloud_prob_threshold float

Cloud probability threshold 0-100 (default: 50).

50

Returns:

Type Description
Image

Input image with 'clouds' band added (1 = cloud, 0 = clear).

Source code in gee_acolite/utils/masks.py
def add_cloud_bands(img : ee.Image, cloud_prob_threshold : float = 50) -> ee.Image:
    """
    Add cloud mask band based on cloud probability.

    Uses Sentinel-2 Cloud Probability dataset to identify cloudy pixels.

    Parameters
    ----------
    img : ee.Image
        Image with 'cloud_prob' property containing Cloud Probability image.
    cloud_prob_threshold : float, optional
        Cloud probability threshold 0-100 (default: 50).

    Returns
    -------
    ee.Image
        Input image with 'clouds' band added (1 = cloud, 0 = clear).
    """
    # Get cloud probability (already as band)
    cld_prb = ee.Image(img.get('cloud_prob')).select('probability')

    # Cloud mask based on probability threshold
    is_cloud = cld_prb.gt(cloud_prob_threshold).rename('clouds')

    return img.addBands(is_cloud)

add_shadow_bands

add_shadow_bands(img: Image, nir_dark_threshold: float = 0.15, cloud_proj_distance: float = 1) -> Image

Add cloud shadow mask bands to L1C image.

Identifies cloud shadows by projecting cloud locations in the direction of solar illumination and intersecting with dark NIR pixels.

Parameters:

Name Type Description Default
img Image

Image with 'clouds' band and solar geometry metadata.

required
nir_dark_threshold float

Threshold for dark pixels in NIR (0-1, default: 0.15).

0.15
cloud_proj_distance float

Maximum shadow projection distance in km (default: 1).

1

Returns:

Type Description
Image

Input image with 'dark_pixels', 'cloud_transform', and 'shadows' bands.

Source code in gee_acolite/utils/masks.py
def add_shadow_bands(img : ee.Image, nir_dark_threshold : float = 0.15, 
                     cloud_proj_distance : float = 1) -> ee.Image:
    """
    Add cloud shadow mask bands to L1C image.

    Identifies cloud shadows by projecting cloud locations in the direction
    of solar illumination and intersecting with dark NIR pixels.

    Parameters
    ----------
    img : ee.Image
        Image with 'clouds' band and solar geometry metadata.
    nir_dark_threshold : float, optional
        Threshold for dark pixels in NIR (0-1, default: 0.15).
    cloud_proj_distance : float, optional
        Maximum shadow projection distance in km (default: 1).

    Returns
    -------
    ee.Image
        Input image with 'dark_pixels', 'cloud_transform', and 'shadows' bands.
    """
    # Dark pixels in NIR (possible shadows) - for L1C use B8 directly
    dark_pixels = img.select('rhot_B8').lt(nir_dark_threshold * 10000).rename('dark_pixels')

    # Shadow projection direction
    shadow_azimuth = ee.Number(90).subtract(ee.Number(img.get('MEAN_SOLAR_AZIMUTH_ANGLE')))

    # Project shadows from clouds
    cld_proj = img.select('clouds').directionalDistanceTransform(shadow_azimuth, cloud_proj_distance * 10) \
        .reproject(**{'crs': img.select(0).projection(), 'scale': 100}) \
        .select('distance') \
        .mask() \
        .rename('cloud_transform')

    # Identify shadows as intersection of dark pixels and cloud projection
    shadows = cld_proj.multiply(dark_pixels).rename('shadows')

    return img.addBands(ee.Image([dark_pixels, cld_proj, shadows]))

add_cld_shdw_mask

add_cld_shdw_mask(img: Image, cloud_prob_threshold: float = 50, nir_dark_threshold: float = 0.15, cloud_proj_distance: float = 1, buffer: int = 50) -> Image

Add combined cloud and shadow mask with buffer.

Creates comprehensive cloud and shadow mask by combining: 1. Cloud probability mask 2. Cloud shadow detection 3. Buffer around masked areas

Parameters:

Name Type Description Default
img Image

Image with 'cloud_prob' property containing Cloud Probability data.

required
cloud_prob_threshold float

Cloud probability threshold 0-100 (default: 50).

50
nir_dark_threshold float

Threshold for dark NIR pixels 0-1 (default: 0.15).

0.15
cloud_proj_distance float

Shadow projection distance in km (default: 1).

1
buffer int

Buffer distance around clouds/shadows in meters (default: 50).

50

Returns:

Type Description
Image

Input image with 'cloudmask' band (1 = cloud/shadow, 0 = clear).

Source code in gee_acolite/utils/masks.py
def add_cld_shdw_mask(img : ee.Image, cloud_prob_threshold : float = 50, 
                      nir_dark_threshold : float = 0.15, cloud_proj_distance : float = 1, 
                      buffer : int = 50) -> ee.Image:
    """
    Add combined cloud and shadow mask with buffer.

    Creates comprehensive cloud and shadow mask by combining:
    1. Cloud probability mask
    2. Cloud shadow detection
    3. Buffer around masked areas

    Parameters
    ----------
    img : ee.Image
        Image with 'cloud_prob' property containing Cloud Probability data.
    cloud_prob_threshold : float, optional
        Cloud probability threshold 0-100 (default: 50).
    nir_dark_threshold : float, optional
        Threshold for dark NIR pixels 0-1 (default: 0.15).
    cloud_proj_distance : float, optional
        Shadow projection distance in km (default: 1).
    buffer : int, optional
        Buffer distance around clouds/shadows in meters (default: 50).

    Returns
    -------
    ee.Image
        Input image with 'cloudmask' band (1 = cloud/shadow, 0 = clear).
    """
    # Add cloud bands
    img_cloud = add_cloud_bands(img, cloud_prob_threshold)

    # Add shadow bands
    img_cloud_shadow = add_shadow_bands(img_cloud, nir_dark_threshold, cloud_proj_distance)

    # Combine masks
    is_cld_shdw = img_cloud_shadow.select('clouds').add(img_cloud_shadow.select('shadows')).gt(0)

    # Apply buffer
    is_cld_shdw = is_cld_shdw.focal_min(2).focal_max(buffer * 2 / 20) \
        .reproject(**{'crs': img.select([0]).projection(), 'scale': 20}) \
        .rename('cloudmask')

    return img_cloud_shadow.addBands(is_cld_shdw)

Masking Sequence

sequenceDiagram participant Image participant NonWater participant Cirrus participant TOA participant CloudProb participant Final Image->>NonWater: B11 < threshold (water/land) NonWater->>Cirrus: water pixels only Cirrus->>Cirrus: B10 < 0.005 Cirrus->>TOA: no cirrus TOA->>TOA: band < threshold TOA->>CloudProb: no bright clouds CloudProb->>CloudProb: cloud prob + shadow projection CloudProb->>Final: valid pixels

Usage Examples

Basic Water Mask

import ee
from gee_acolite.utils.masks import non_water

image = corrected_images.first()

# Keep pixels where B11 < 0.05 (water)
water_mask = non_water(image, band='B11', threshold=0.05)
image_water = image.updateMask(water_mask)

Cirrus Mask

from gee_acolite.utils.masks import cirrus_mask

# Detect cirrus using B10 (1375 nm)
no_cirrus = cirrus_mask(image, band='B10', threshold=0.005)
image_no_cirrus = image.updateMask(no_cirrus)

Full Combined Mask (as used internally by compute_water_mask)

from gee_acolite.utils import masks

settings = {
    'l2w_mask_threshold': 0.05,
    'l2w_mask_cirrus_threshold': 0.005,
    'l2w_mask_high_toa_threshold': 0.3,
}

# Water / land
mask = masks.non_water(image, 'B11', settings['l2w_mask_threshold'])

# Cirrus
mask = mask.And(masks.cirrus_mask(image, 'B10', settings['l2w_mask_cirrus_threshold']))

# Bright clouds (TOA threshold)
mask = mask.And(masks.toa_mask(image, 'rhot_B4', settings['l2w_mask_high_toa_threshold']))

clean_image = image.updateMask(mask)

Cloud and Shadow Masking (requires search_with_cloud_proba)

from gee_acolite.utils.masks import add_cld_shdw_mask, cld_shdw_mask
from gee_acolite.utils.search import search_with_cloud_proba

images = search_with_cloud_proba(roi, '2023-06-01', '2023-06-30')

def apply_cloud_mask(img):
    img = add_cld_shdw_mask(
        img,
        cloud_prob_threshold=50,
        nir_dark_threshold=0.15,
        cloud_proj_distance=10,
        buffer=50,
    )
    return img.updateMask(cld_shdw_mask(img))

masked = images.map(apply_cloud_mask)

Cloud Detection Decision Tree

flowchart TD A[Pixel] --> B{B10 > 0.005?} B -->|Yes| C[CIRRUS — masked] B -->|No| D{TOA band > threshold?} D -->|Yes| E[BRIGHT CLOUD — masked] D -->|No| F{Cloud prob > 50?} F -->|Yes| G[CLOUD / SHADOW — masked] F -->|No| H{B11 >= threshold?} H -->|Yes| I[LAND — masked] H -->|No| J[VALID WATER] style C fill:#ff6666 style E fill:#ffaaaa style G fill:#ffccaa style I fill:#c8a96e style J fill:#3399ff,color:#fff

Threshold Reference

Parameter Threshold Description Mode
l2w_mask_threshold > 0.05 SWIR B11 water/land split Conservative
l2w_mask_threshold > 0.0 SWIR B11 — permissive Turbid waters
l2w_mask_cirrus_threshold < 0.005 B10 cirrus detection Standard
l2w_mask_cirrus_threshold < 0.003 B10 cirrus detection Strict
l2w_mask_high_toa_threshold < 0.3 TOA cloud threshold Conservative
l2w_mask_high_toa_threshold < 0.2 TOA cloud threshold Strict
Cloud probability < 50 S2 Cloud Probability Standard
Cloud probability < 30 S2 Cloud Probability Strict

NDWI — Water Index

The water/land mask is based on SWIR reflectance (B11). Internally, non_water keeps pixels where B11 < threshold. For reference, the NDWI index used in some contexts is:

\[ \text{NDWI} = \frac{B3 - B8}{B3 + B8} \]
  • NDWI > 0: likely water
  • NDWI < 0: likely land/vegetation

References

  • McFeeters, S. K. (1996). The use of the Normalized Difference Water Index (NDWI) in the delineation of open water features. IJRS, 17(7), 1425–1432.
  • Martins, V. S., et al. (2017). Assessment of atmospheric correction methods for Sentinel-2 MSI images applied to Amazon floodplain lakes. Remote Sensing, 9(4), 322.