API Reference
Spectral Indices
indices.py
Spectral index computation from multispectral xarray DataArrays.
Supported indices
- MNDWI Modified Normalised Difference Water Index (Xu 2006)
= (Green - SWIR) / (Green + SWIR)
- NDWI Normalised Difference Water Index (McFeeters 1996)
= (Green - NIR) / (Green + NIR)
- NDVI Normalised Difference Vegetation Index
= (NIR - Red) / (NIR + Red)
- NDTI Normalised Difference Turbidity Index
= (Red - Green) / (Red + Green)
- AWEIsh Automated Water Extraction Index Shadow (Feyisa et al. 2014)
= Blue + 2.5*Green - 1.5*(NIR + SWIR) - 0.25*SWIR2
- AWEInsh Automated Water Extraction Index No Shadow (Feyisa et al. 2014)
= Blue + 2.5*Green - 1.5*(NIR + SWIR)
Functions
compute_mndwi() : Single MNDWI computation
compute_ndwi() : Single NDWI computation
compute_ndvi() : Single NDVI computation
compute_ndti() : Single NDTI computation
compute_aweish() : Single AWEIsh computation
compute_aweinsh() : Single AWEInsh computation
compute_indices() : MNDWI, NDVI, NDTI (+ optionally AWEIsh, AWEInsh for WCT)
compute_water_indices() : All water indices (MNDWI, NDWI, AWEIsh, AWEInsh)
MNDWI is recommended for most wetland applications. AWEIsh and AWEInsh may perform better in areas with strong topographic shadow (mountains, deep valleys) or confusion with built-up surfaces.
- wetlandmapper.indices.compute_aweinsh(ds: Dataset | DataArray, green_band: str = 'green', nir_band: str = 'nir', swir_band: str = 'swir') DataArray[source]
Compute the Automated Water Extraction Index without shadow suppression.
AWEInsh = 4*(Green - SWIR1) - (0.25*NIR + 2.75*SWIR1)
A simplified water index that omits SWIR2 and is computationally lighter than AWEIsh. Less effective at suppressing shadow, but robust in areas affected by atmospheric haze or aerosol loading.
- Parameters:
- Returns:
AWEInsh values. A threshold of 0.0 separates water from non-water.
- Return type:
xr.DataArray
Notes
The 0.0001 DN scaling factor from the original paper is omitted here because this function expects surface reflectance in [0, 1].
References
Feyisa, G. L., Meilby, H., Fensholt, R., & Proud, S. R. (2014). Remote Sensing of Environment, 140, 23-35. https://doi.org/10.1016/j.rse.2013.08.029
- wetlandmapper.indices.compute_aweish(ds: Dataset | DataArray, blue_band: str = 'blue', green_band: str = 'green', nir_band: str = 'nir', swir_band: str = 'swir', swir2_band: str = 'swir2') DataArray[source]
Compute the Automated Water Extraction Index with shadow suppression.
AWEIsh = Blue + 2.5*Green - 1.5*(NIR + SWIR1) - 0.25*SWIR2
Designed to suppress topographic shadow and built-up surface confusion that affects simpler water indices such as MNDWI. Positive values indicate surface water. Preferred over MNDWI in mountainous terrain or urban areas.
- Parameters:
ds (xr.Dataset or xr.DataArray) – Multi-spectral dataset. Input bands should be surface reflectance in the range [0, 1] (not raw DN values).
blue_band (str) – Default
"blue". Landsat 8/9:"SR_B2"; Sentinel-2:"B2".green_band (str) – Default
"green".nir_band (str) – Default
"nir".swir_band (str) – SWIR1. Default
"swir".swir2_band (str) – SWIR2. Default
"swir2". Landsat 8/9:"SR_B7"; Sentinel-2:"B12".
- Returns:
AWEIsh values. A threshold of 0.0 separates water (positive) from non-water (negative).
- Return type:
xr.DataArray
Notes
The original Feyisa et al. (2014) formula was defined for raw Landsat DN values and includes a 0.0001 scaling factor. This implementation operates on surface reflectance already scaled to [0, 1], so the constant is omitted.
References
Feyisa, G. L., Meilby, H., Fensholt, R., & Proud, S. R. (2014). Automated Water Extraction Index: A new technique for surface water mapping using Landsat imagery. Remote Sensing of Environment, 140, 23-35. https://doi.org/10.1016/j.rse.2013.08.029
- wetlandmapper.indices.compute_indices(ds: Dataset | DataArray, green_band: str = 'green', red_band: str = 'red', nir_band: str = 'nir', swir_band: str = 'swir', swir2_band: str = 'swir2', blue_band: str = 'blue', include_awei: bool = False) Dataset[source]
Compute spectral indices and return as a Dataset.
Always computes MNDWI, NDVI, and NDTI (required for WCT classification). Optionally adds AWEIsh and AWEInsh.
- Parameters:
ds (xr.Dataset or xr.DataArray) – Multi-spectral image (single-date or seasonal composite).
green_band (str) – Band names within
ds. Defaults match the wetlandmapper harmonised naming convention.red_band (str) – Band names within
ds. Defaults match the wetlandmapper harmonised naming convention.nir_band (str) – Band names within
ds. Defaults match the wetlandmapper harmonised naming convention.swir_band (str) – Band names within
ds. Defaults match the wetlandmapper harmonised naming convention.swir2_band (str) – SWIR2 band name. Required only when
include_awei=True. Default"swir2".blue_band (str) – Blue band name. Required only for AWEIsh when
include_awei=True. Default"blue".include_awei (bool) – If
True, also compute AWEIsh and AWEInsh and include them in the returned Dataset. DefaultFalsefor backward compatibility.
- Returns:
Dataset with variables
MNDWI,NDVI,NDTI(always), and optionallyAWEIsh,AWEInsh.- Return type:
xr.Dataset
Examples
Standard WCT workflow:
>>> indices = compute_indices(ds, green_band="B3", red_band="B4", ... nir_band="B5", swir_band="B6")
Including AWEI for shadow-affected scenes:
>>> indices = compute_indices(ds, green_band="B3", red_band="B4", ... nir_band="B5", swir_band="B6", ... swir2_band="B7", blue_band="B2", ... include_awei=True)
- wetlandmapper.indices.compute_mndwi(ds: Dataset | DataArray, green_band: str = 'green', swir_band: str = 'swir') DataArray[source]
Compute the Modified Normalised Difference Water Index (MNDWI).
MNDWI = (Green - SWIR) / (Green + SWIR)
Values range from -1 to +1. Positive values indicate surface water.
- Parameters:
ds (xr.Dataset or xr.DataArray) – Multi-spectral dataset containing at least a green and a SWIR band.
green_band (str) – Name of the green band (default:
"green"). Common alternatives:"B3"(Landsat 8/9),"B03"(Sentinel-2).swir_band (str) – Name of the SWIR band (default:
"swir"). Common alternatives:"B6"(Landsat 8/9 SWIR1),"B11"(Sentinel-2).
- Returns:
MNDWI values in the range [-1, 1], preserving input dimensions.
- Return type:
xr.DataArray
References
Xu, H. (2006). Modification of normalised difference water index (NDWI) to enhance open water features in remotely sensed imagery. International Journal of Remote Sensing, 27(14), 3025–3033. https://doi.org/10.1080/01431160600589179
- wetlandmapper.indices.compute_ndti(ds: Dataset | DataArray, red_band: str = 'red', green_band: str = 'green') DataArray[source]
Compute the Normalised Difference Turbidity Index (NDTI).
NDTI = (Red - Green) / (Red + Green)
Higher values indicate more turbid (suspended-sediment-rich) water.
- wetlandmapper.indices.compute_ndvi(ds: Dataset | DataArray, nir_band: str = 'nir', red_band: str = 'red') DataArray[source]
Compute the Normalised Difference Vegetation Index (NDVI).
NDVI = (NIR - Red) / (NIR + Red)
- Parameters:
- Returns:
NDVI values in the range [-1, 1].
- Return type:
xr.DataArray
- wetlandmapper.indices.compute_ndwi(ds: Dataset | DataArray, green_band: str = 'green', nir_band: str = 'nir') DataArray[source]
Compute the Normalised Difference Water Index (NDWI).
NDWI = (Green - NIR) / (Green + NIR)
Values range from -1 to +1. Positive values indicate surface water. Less sensitive to built-up land and vegetation than MNDWI.
- Parameters:
ds (xr.Dataset or xr.DataArray) – Multi-spectral dataset containing at least a green and a NIR band.
green_band (str) – Name of the green band (default:
"green"). Common alternatives:"B3"(Landsat 8/9),"B03"(Sentinel-2).nir_band (str) – Name of the NIR band (default:
"nir"). Common alternatives:"B5"(Landsat 8/9),"B08"(Sentinel-2).
- Returns:
NDWI values in the range [-1, 1], preserving input dimensions.
- Return type:
xr.DataArray
References
McFeeters, S. K. (1996). The use of the Normalized Difference Water Index (NDWI) in the delineation of open water features. International Journal of Remote Sensing, 17(7), 1425–1432. https://doi.org/10.1080/01431169608948714
- wetlandmapper.indices.compute_water_indices(ds: Dataset | DataArray, green_band: str = 'green', red_band: str = 'red', nir_band: str = 'nir', swir_band: str = 'swir', swir2_band: str = 'swir2', blue_band: str = 'blue') Dataset[source]
Compute all available water indices and return as a Dataset.
Computes MNDWI, NDWI, AWEIsh, and AWEInsh for comprehensive water detection. Useful for comparing different water indices or selecting the best performing one for a specific study area.
- Parameters:
ds (xr.Dataset or xr.DataArray) – Multi-spectral image (single-date or time series).
green_band (str) – Band names within
ds.red_band (str) – Band names within
ds.nir_band (str) – Band names within
ds.swir_band (str) – Band names within
ds.swir2_band (str) – SWIR2 band name. Default
"swir2".blue_band (str) – Blue band name. Default
"blue".
- Returns:
Dataset with variables
MNDWI,NDWI,AWEIsh,AWEInsh.- Return type:
xr.Dataset
Examples
>>> water_indices = compute_water_indices( ... ds, ... blue_band="B2", green_band="B3", red_band="B4", ... nir_band="B5", swir_band="B6", swir2_band="B7", # Landsat 8/9 ... ) >>> water_indices <xarray.Dataset> Dimensions: (time: ..., y: ..., x: ...) Data variables: MNDWI ... NDWI ... AWEIsh ... AWEInsh ...
Wetland Dynamics
Wetland dynamics classification and temporal aggregation utilities.
Copyright (c) 2026, Manudeo Singh Author: Manudeo Singh, March 2026
- wetlandmapper.dynamics.aggregate_time(da: DataArray | Dataset, freq: str = 'annual', method: str = 'median') DataArray | Dataset[source]
Temporally aggregate a multi-temporal xarray object before classification.
Reduces a time series to one composite per chosen period by computing a pixel-wise statistic within each period. Useful for:
Dynamics: produce annual composites from all available scenes rather than using every raw overpass.
WCT: produce monthly or seasonal composites, then classify each with
classify_wct()/classify_wct_ema().
- Parameters:
da (xr.DataArray or xr.Dataset) – Input data with a
timedimension. Accepts both GEE-fetched and locally constructed objects.freq ({"annual", "monthly", "seasonal", "all"}) –
Aggregation period:
"annual"One composite per calendar year (resampled to year-end).
"monthly"One composite per calendar month.
"seasonal"One composite per meteorological season per year: DJF (Dec–Jan–Feb), MAM (Mar–Apr–May), JJA (Jun–Jul–Aug), SON (Sep–Oct–Nov). Uses
pandasquarterly resampling anchored to December."all"No aggregation — returns
daunchanged.
method ({"median", "mean", "max", "min"}) – Pixel-wise statistic computed within each period. Default
"median".
- Returns:
Same type as
dawith a reducedtimedimension (one step per period). For"seasonal", the time coordinate is labelled with the first day of each quarter (e.g.,2003-12-01for DJF 2004).- Return type:
xr.DataArray or xr.Dataset
- Raises:
ValueError – If
freqormethodis not one of the valid options.
Examples
Produce annual MNDWI composites from a dense time series:
>>> from wetlandmapper.dynamics import aggregate_time >>> mndwi_annual = aggregate_time(mndwi_ts, freq="annual") >>> dynamics = classify_dynamics(mndwi_annual, nYear=3)
Produce seasonal composites for WCT classification:
>>> from wetlandmapper import compute_indices, classify_wct_ema >>> from wetlandmapper.dynamics import aggregate_time >>> indices_ts = fetch(aoi, "2010-01-01", "2023-12-31", ... index=["MNDWI","NDVI","NDTI"]) >>> seasonal = aggregate_time(indices_ts, freq="seasonal") >>> # Classify each season independently >>> for t in seasonal.time: ... wct = classify_wct_ema(seasonal.sel(time=t))
Notes
Pixels that are NaN (masked by cloud or no-data) in all scenes within a period remain NaN in the composite. The statistic is computed ignoring NaNs (
skipna=Trueis the xarray default).
- wetlandmapper.dynamics.classify_dynamics(water_index: DataArray, nYear: int = 3, thresholdWet: float = 25.0, thresholdPersis: float = 75.0, water_threshold: float = 0.0, nan_policy: str = 'total', min_valid_obs: int | None = None, mndwi_threshold: float | None = None) DataArray[source]
Classify wetland pixels into six mutually exclusive temporal dynamics classes.
Each pixel is assigned to exactly one class based on three temporal summary statistics derived from a multi-year water index stack:
Overall wet frequency W% (% of years above
water_threshold)Historic wet count W_historic (first
nYearyears)Recent wet count W_recent (last
nYearyears)
Class priority (strictly exclusive — a pixel receives exactly one class):
Each priority is applied only to unclassified pixels (code = 0), preventing any pixel from receiving more than one class code even when multiple conditions are simultaneously true (e.g. a pixel that is both Persistent and Intensifying will be classified as Persistent only).
- Parameters:
water_index (xr.DataArray) – Multi-temporal water index time series with a
timedimension. Accepts MNDWI, AWEIsh, AWEInsh, NDWI, or any index where positive values indicate surface water.nYear (int) – Length of the historic and recent windows in years. Default 3.
thresholdWet (float) – Minimum wet frequency (%) for a pixel to be any wetland class. Default 25.
thresholdPersis (float) – Wet frequency (%) above which a pixel is Persistent. Must be greater than
thresholdWet. Default 75.water_threshold (float) – Index value above which a pixel is counted as wet in a given year. Default 0.0 (positive MNDWI = water-dominated; Xu 2006).
nan_policy ({"total", "valid"}) –
Denominator used when computing wet frequency.
"total"(default)Denominator = total number of time steps. NaN pixels count as dry. Reproduces the original Singh & Sinha (2022) method. Appropriate when NaN values are rare or randomly distributed.
"valid"Denominator = per-pixel count of non-NaN observations. Wet frequency is the fraction of cloud-free years that were wet. The historic/recent windows are also normalised by their own valid counts so that
deltais expressed as a fraction in [−1, +1]. Use when cloud masking produces substantial or spatially clustered NaN values.
min_valid_obs (int, optional) – (Only active when nan_policy=”valid”.) Minimum number of non-NaN observations required for classification. Pixels with fewer valid observations are set to NaN in the output. Default
None(no minimum enforced).mndwi_threshold (float, optional) – Deprecated. Use
water_thresholdinstead.
- Returns:
Integer DataArray (dtype int8) of shape
(y, x)with class codes fromDYNAMICS_CLASSES. Guaranteed to contain only valid class codes — no additive artefacts.- Return type:
xr.DataArray
- Raises:
ValueError – If
water_indexlacks atimedimension, ifnYear * 2 > n_time, if thresholds are out of range, or ifnan_policyis not one of the accepted values.
References
Singh, M. & Sinha, R. (2022). Remote Sensing Letters, 13(1), 1–13. https://doi.org/10.1080/2150704X.2021.1980919
- wetlandmapper.dynamics.compute_wet_frequency(water_index: DataArray, water_threshold: float = 0.0, nan_policy: str = 'total', mndwi_threshold: float | None = None) DataArray[source]
Return the pixel-wise wet frequency (%) across the full time series.
- Parameters:
water_index (xr.DataArray) – Multi-temporal water index with a
timedimension.water_threshold (float) – Index value above which a pixel is counted as wet. Default 0.0.
nan_policy ({"total", "valid"}) –
"total"(default): denominator = total time steps; NaN = dry."valid": denominator = per-pixel non-NaN count.mndwi_threshold (float, optional) – Deprecated. Use
water_thresholdinstead.
- Returns:
Wet frequency in percent (0–100), shape
(y, x).- Return type:
xr.DataArray
Wetland Cover Types (WCT)
wct.py
# Copyright (c) 2026, Manudeo Singh # # Author: Manudeo Singh, March 2026 # —— Wetland Cover Type (WCT) classification from combined MNDWI, NDVI, and NDTI.
- Two classification approaches are provided, both based on:
Singh, M., Allaka, S., Gupta, P. K., Patel, J. G., & Sinha, R. (2022). Deriving wetland-cover types (WCTs) from integration of multispectral indices based on Earth observation data. Environmental Monitoring and Assessment, 194(12), 878. https://doi.org/10.1007/s10661-022-10541-7
Functions
- classify_wct_ema(indices, n_parts=4)
Original EMA paper method. Each index is independently binned into n_parts equal intervals over its positive [0, 1] range (default 4, giving quartile step 0.25), producing a level 0-4 per index. Every pixel then carries a three-way combination code w_level × v_level × t_level (e.g. “w2v3t1”) and the WCT class is read from a pre-built 5×5×5 lookup table. No priority ordering: each unique combination has exactly one class.
Critical implication: a pixel with negative MNDWI (w0) but high NDVI (v3 or v4) is assigned WCT 4 (Emergent/Floating Veg) because dense aquatic vegetation completely suppresses the water signal in MNDWI.
- classify_wct(indices, thresholds=None)
Improved continuous-threshold method. Same five WCT classes, but boundaries are free-floating values that can be tuned per sensor, season, or region without changing the bin structure.
- WCT class codes (shared by both methods)
1 Open Clear Water High MNDWI, Low NDVI, Low NDTI 2 Turbid / Sediment-laden High MNDWI, Low NDVI, High NDTI 3 Submerged Aquatic Veg. High MNDWI, Mod. NDVI, Low NDTI 4 Emergent / Floating Veg. Any MNDWI, High NDVI, Low NDTI ← includes w0 5 Moist / Waterlogged Soil Low MNDWI, Low NDVI, Mod. NDTI 0 Non-wetland / Dry
- wetlandmapper.wct.build_ema_lookup_table(n_parts: int = 4) ndarray[source]
Build the (n_parts+1)³ lookup table mapping index levels → WCT class.
Every entry
table[w, v, t]is the WCT class code (0–5) for the combination of MNDWI level w, NDVI level v, and NDTI level t.- Level coding (same for all three indices):
0 — negative value (dry / non-wetland) 1 — [0, 1/n_parts) Low 2 — [1/n_parts, 2/n_parts) Moderate-Low … n — [(n-1)/n_parts, 1.0] High (n = n_parts)
WCT class rules (applied in this order; later entries overwrite earlier):
WCT MNDWI level NDVI level NDTI level Cover type — ———– ———- ———- ———-
5 w == 1 v <= 1 t <= 2 Moist / Waterlogged Soil 3 w >= 2 1 <= v <= 2 t <= 1 Submerged Aquatic Veg. 2 w >= 2 v <= 1 t >= 2 Turbid / Sediment Water 4 w >= 0 v >= 3 t <= 1 Emergent / Floating Veg. (*) 1 w >= 3 v <= 1 t <= 1 Open Clear Water
(*) WCT 4 matches even w == 0 (negative MNDWI): dense aquatic vegetation completely suppresses the MNDWI water signal, so the pixel reads as dry on MNDWI but shows high NDVI. The combination code w0v3t1 (for example) is unambiguously aquatic-vegetated, not dry land.
- Parameters:
n_parts (int) – Number of equal bins over [0, 1]. Default 4.
- Returns:
Lookup table indexed as [mndwi_level, ndvi_level, ndti_level].
- Return type:
np.ndarray, shape (n_parts+1, n_parts+1, n_parts+1), dtype int8
- wetlandmapper.wct.classify_wct(indices: Dataset, thresholds: dict[str, float] | None = None) DataArray[source]
Classify Wetland Cover Types using continuous, adjustable spectral thresholds.
An improvement of
classify_wct_ema(): boundaries are free-floating values (not locked to multiples of 1/n_parts), enabling sub-quartile calibration for different sensors, seasons, or geographic regions. The five WCT classes and their spectral logic are identical to the EMA method; only the boundary representation differs.WCT 4 (Emergent/Floating Veg.) is assigned whenever NDVI exceeds
ndvi_veg_highregardless of the MNDWI sign, matching the EMA method’s behaviour for vegetation-masked pixels.- Parameters:
indices (xr.Dataset) – Dataset with variables
"MNDWI","NDVI","NDTI".thresholds (dict, optional) –
Override any subset of the defaults (from Singh et al. 2022):
"mndwi_water"(0.0) MNDWI above which pixel is open water"mndwi_moist"(-0.2) MNDWI lower bound for moist-soil zone"ndvi_veg_high"(0.2) NDVI above which emergent veg. present"ndvi_veg_low"(0.05) NDVI lower bound for submerged veg."ndti_turbid"(0.0) NDTI above which water is turbid
- Returns:
Integer raster of WCT codes (dtype int8). Name:
"wetland_cover_type".- Return type:
xr.DataArray
References
Singh et al. (2022). Environmental Monitoring and Assessment, 194(12), 878.
- wetlandmapper.wct.classify_wct_ema(indices: Dataset, n_parts: int = 4) Dataset[source]
Classify Wetland Cover Types using the original Singh et al. (2022) EMA method.
Each pixel is independently binned into a level (0–n_parts) for MNDWI, NDVI, and NDTI. The three levels together form a combination code:
w_level v_level t_level → WCT class ——- ——- ——- ———
0 4 0 → 4 (Emergent veg — water signal masked) 3 0 0 → 1 (Open clear water) 2 0 3 → 2 (Turbid water) 2 2 0 → 3 (Submerged aquatic veg.) 1 0 1 → 5 (Moist soil)
The mapping is a pre-built lookup table (see
build_ema_lookup_table()). There is no priority ordering — each combination has exactly one class.A vegetation-masked pixel (negative MNDWI but high NDVI) is classified as WCT 4 (Emergent / Floating Vegetation) even though its MNDWI alone would suggest dry land, because the dense canopy fully attenuates the water signal.
- Parameters:
indices (xr.Dataset) – Dataset with variables
"MNDWI","NDVI","NDTI". Typically produced bywetlandmapper.compute_indices().n_parts (int) – Number of equal bins over the positive [0, 1] range. Default 4 (step 0.25; levels 1–4 at boundaries 0.25 / 0.50 / 0.75 / 1.00).
- Returns:
Dataset with two variables:
"wetland_cover_type"Integer raster of WCT class codes (dtype int8). CRS preserved from input if rioxarray is available.
"combination_code"Integer raster encoding the three index levels that produced each WCT class. Hundreds digit = MNDWI level, tens = NDVI, units = NDTI (e.g. 401 → MNDWI High / NDVI Negative / NDTI Low → WCT 1). Useful for inspecting which index fractions drove the classification at each pixel.
- Return type:
xr.Dataset
Notes
The combination code is distinct from the class: two pixels with the same WCT class may have different combination codes, revealing how far each index was from the bin boundaries.
Examples
>>> from wetlandmapper import compute_indices, classify_wct_ema >>> indices = compute_indices(ds, green_band="B3", red_band="B4", ... nir_band="B5", swir_band="B6") >>> wct = classify_wct_ema(indices) >>> wct.attrs['classification_method'] 'EMA-combination-lookup'
References
Singh et al. (2022). Environmental Monitoring and Assessment, 194(12), 878. https://doi.org/10.1007/s10661-022-10541-7
Terrain Analysis
terrain.py
DEM-based terrain analysis for masking high-altitude artefacts in wetland mapping.
Glaciers, permanent snowpacks, and steep mountain terrain produce strong positive wetness index signals that are not wetlands. This module provides tools to identify and mask these artefacts using a Digital Elevation Model (DEM) by characterising local terrain flatness — the key topographic criterion that separates true wetlands (flat, low-lying areas) from high-altitude false positives.
Three flatness metrics are provided, each serving a different purpose
- compute_slope(dem)
Most broadly applicable. A wetland pixel almost never sits on a slope >5°. Computed from numpy gradient on the DEM with approximate metre conversion for geographic coordinates. Independent of window size.
- compute_tpi(dem, window)
Adds discrimination between flat plateau (near-zero TPI, high elevation) and valley bottom (negative TPI). Slope alone can’t separate a flat glacier from a flat floodplain. TPI can, if you combine it with an elevation ceiling.
- compute_local_range(dem, window)
Directly replicates the GEE rolling-window approach. Most intuitive to threshold (e.g. “< 30 m variation in a 5×5 window”). Maximum minus minimum elevation within an NxN window. A low local range indicates flat terrain.
- map_dem_depressions(raw_dem, filled_dem, …)
Depression mapping from raw and pit-filled DEM using integer division and binary reclassification (depression=1, non-depression=0), following the protocol described by Sinha et al. (2017, Current Science).
- mask_terrain_artifacts(wetness, dem, …)
Combines any or all three metrics plus an elevation ceiling. Emits a warning if the mask retains <10% of pixels (likely thresholds are too strict).
Notes
All functions operate on xarray DataArrays with spatial dimensions
(y, x) or (lat, lon). CRS is preserved where rioxarray is available.
No GEE dependency — these functions are designed for use on locally downloaded
data (from wetlandmapper.gee.fetch() or any other source).
For server-side DEM masking within GEE (without downloading the DEM), use
the dem_mask parameter of wetlandmapper.gee.fetch().
- wetlandmapper.terrain.compute_local_range(dem: DataArray, window: int = 5) DataArray[source]
Compute local elevation range (max - min) within a rolling window.
A low local range indicates flat terrain. This directly replicates the rolling-window approach used in GEE scripts to retain only flat neighbourhoods for wetland mapping.
- Parameters:
dem (xr.DataArray) – Digital Elevation Model with spatial dimensions
(y, x).window (int) – Square window size in pixels. Default 5 (5 x 5 window).
- Returns:
Local elevation range in the same units as
dem(metres). Name:"local_range".- Return type:
xr.DataArray
- wetlandmapper.terrain.compute_slope(dem: DataArray, units: str = 'degrees') DataArray[source]
Compute terrain slope from a DEM DataArray.
Uses central-difference gradient (numpy.gradient) applied to the spatial dimensions. The result represents the steepness of terrain at each pixel — low values indicate flat ground suitable for wetlands.
- Parameters:
dem (xr.DataArray) – Digital Elevation Model with spatial dimensions
(y, x)or(lat, lon). Units should be metres.units ({"degrees", "radians", "percent"}) – Output slope units. Default
"degrees".
- Returns:
Slope values, same spatial shape as
dem. Name:"slope".- Return type:
xr.DataArray
- wetlandmapper.terrain.compute_tpi(dem: DataArray, window: int = 5) DataArray[source]
Compute the Topographic Position Index (TPI).
TPI = elevation - focal_mean(elevation, window x window)
Positive TPI indicates a pixel is higher than its surroundings (hilltop, ridge); negative TPI indicates a valley or depression; near-zero TPI indicates flat terrain or a mid-slope position.
- Parameters:
dem (xr.DataArray) – Digital Elevation Model with spatial dimensions
(y, x)or(lat, lon).window (int) – Size of the square focal window in pixels. Odd numbers produce a symmetric neighbourhood. Default 5 (5 x 5 = 25-pixel window).
- Returns:
TPI values in the same elevation units as
dem(usually metres). Name:"TPI".- Return type:
xr.DataArray
- wetlandmapper.terrain.map_dem_depressions(raw_dem: DataArray, filled_dem: DataArray, *, require_integer: bool = True, apply_cleanup: bool = True, cleanup_window: int = 3, min_neighbours: int = 2) DataArray[source]
Map topographic depressions from raw and pit-filled DEMs.
This implements a depression protocol used in floodplain wetland mapping:
Integer-divide
raw_dem / filled_dem.Pixels with value 1 are unchanged terrain (no pit).
Pixels with value 0 are depressions (raw < filled).
Reclassify to a binary mask: depression=1, non-depression=0.
- Parameters:
raw_dem (xr.DataArray) – Original (unfilled) DEM.
filled_dem (xr.DataArray) – Pit-filled DEM created from
raw_dem.require_integer (bool) – If
True(default), both DEMs must have integer dtype.apply_cleanup (bool) – If
True(default), remove isolated one-pixel/very small speckles using a neighbourhood-count filter.cleanup_window (int) – Square rolling window size for cleanup. Default 3.
min_neighbours (int) – Minimum number of depression pixels (including self) within
cleanup_windowto retain a depression pixel. Default 2.
- Returns:
Binary depression mask with values {0, 1}. Name:
"depression_mask".- Return type:
xr.DataArray
Notes
This method performs best in low-relief floodplains and may be less reliable in rugged terrain. Residual speckle can still occur and may need additional post-processing for specific study areas.
References
Sinha, R., Saxena, S., & Singh, M. (2017). Protocols for Riverine Wetland Mapping and Classification Using Remote Sensing and GIS. Current Science, 112(7), 1544-1552. http://www.jstor.org/stable/24912702
- wetlandmapper.terrain.mask_terrain_artifacts(wetness: DataArray | Dataset, dem: DataArray, max_slope: float | None = 5.0, max_tpi: float | None = None, max_local_range: float | None = None, local_range_window: int = 5, tpi_window: int = 5, max_elevation: float | None = None, invert: bool = False) DataArray | Dataset[source]
Mask wetness data using terrain flatness and elevation filters.
- Parameters:
wetness (xr.DataArray or xr.Dataset) – Wetness or index data to mask. Can include a
timedimension.dem (xr.DataArray) – Digital Elevation Model with spatial dimensions matching
wetness.max_slope (float or None) – Maximum slope in degrees. Pixels steeper than this are masked. Default 5.0. Set to
Noneto disable slope filtering.max_tpi (float or None) – Maximum absolute TPI (metres). Default
None(disabled).max_local_range (float or None) – Maximum local elevation range (metres). Default
None(disabled).local_range_window (int) – Window size for local range. Default 5.
tpi_window (int) – Window size for TPI. Default 5.
max_elevation (float or None) – Absolute elevation ceiling (metres). Pixels above this are masked. Default
None.invert (bool) – If
True, invert the mask so excluded terrain is retained.
- Returns:
wetnessmasked by terrain suitability. The type matches the input.- Return type:
xr.DataArray or xr.Dataset
Plotting
plotting.py
# Copyright (c) 2026, Manudeo Singh # # Author: Manudeo Singh, March 2026 # ———– Convenience functions for visualising WetlandMapper outputs.
All functions:
- Respect geographic coordinates when present (x/y or lon/lat),
so axes ticks show real coordinate values rather than pixel indices.
Detect y-axis direction (ascending vs descending) and set
originaccordingly so images are never vertically flipped.Place the class legend outside the plot area to avoid obscuring data.
Return (fig, ax) so callers can further customise or save.
Coordinate conventions
wetlandmapper.gee.fetch()Returns arrays with dims
(time, y, x)whereyis descending (north → south, standard raster convention).wetlandmapper.gee.fetch_xee()After the built-in
sortbyfix, also returnsydescending. If you have an older array withlat/londims, rename them first:da = da.rename({"lat": "y", "lon": "x"}).
- wetlandmapper.plotting.plot_dynamics(dynamics, ax=None, title: str = 'Wetland Dynamics', figsize: tuple = (8, 7), add_colorbar: bool = True, legend_loc: str = 'outside right', savepath: str | None = None, dpi: int = 150)[source]
Plot a wetland dynamics classification raster.
- Parameters:
dynamics (xr.DataArray) – Output of
wetlandmapper.classify_dynamics(). Spatial coordinates (x/yorlon/lat) are used for axis ticks when present.ax (matplotlib.axes.Axes, optional) – Axes to draw into. Created if not provided.
title (str) – Plot title.
figsize (tuple) – Figure size in inches.
add_colorbar (bool) – Add a class legend.
legend_loc (str) – Legend placement. Use
"outside right"(default),"outside bottom", or any standard Matplotliblocstring (e.g."lower right")."outside right"/"outside bottom"never overlap the data.savepath (str, optional) – If given, save the figure to this path (PNG, PDF, TIFF, etc.).
dpi (int) – Resolution for saved figure. Default 150.
- Returns:
fig, ax
- Return type:
matplotlib Figure and Axes
- wetlandmapper.plotting.plot_index(da, index_name: str = 'Index', ax=None, figsize: tuple = (8, 7), vmin: float = -1.0, vmax: float = 1.0, cmap: str = 'RdYlGn', time_step: int | None = None, savepath: str | None = None, dpi: int = 150)[source]
Plot a single spectral index (MNDWI, NDVI, or NDTI).
- Parameters:
da (xr.DataArray) – 2-D or 3-D (time, y, x) index DataArray.
index_name (str) – Used in the title and colorbar label.
time_step (int, optional) – Select this time index (0-based) when
dahas a time dim. Defaults to the temporal mean if not provided.savepath – Same as
plot_dynamics().dpi – Same as
plot_dynamics().
- Return type:
fig, ax
- wetlandmapper.plotting.plot_wct(wct, ax=None, title: str = 'Wetland Cover Types', figsize: tuple = (8, 7), add_colorbar: bool = True, legend_loc: str = 'outside right', savepath: str | None = None, dpi: int = 150)[source]
Plot a Wetland Cover Type classification raster.
- Parameters:
wct (xr.DataArray) – Output of
wetlandmapper.classify_wct()orwetlandmapper.classify_wct_ema().ax – Same as
plot_dynamics().title – Same as
plot_dynamics().figsize – Same as
plot_dynamics().add_colorbar – Same as
plot_dynamics().legend_loc – Same as
plot_dynamics().savepath – Same as
plot_dynamics().dpi – Same as
plot_dynamics().
- Return type:
fig, ax
- wetlandmapper.plotting.plot_wet_frequency(mndwi, ax=None, figsize: tuple = (8, 7), mndwi_threshold: float = 0.0, savepath: str | None = None, dpi: int = 150)[source]
Plot wet frequency (%) derived from an MNDWI time series.
- Parameters:
mndwi (xr.DataArray) – Multi-temporal MNDWI with a
timedimension.mndwi_threshold (float) – Pixels with MNDWI above this value are counted as wet.
savepath – Same as
plot_dynamics().dpi – Same as
plot_dynamics().
- Return type:
fig, ax
Google Earth Engine
gee.py
# Copyright (c) 2026, Manudeo Singh # # Author: Manudeo Singh, March 2026 # —— Optional Google Earth Engine (GEE) data acquisition module.
Supports all Landsat missions (4, 5, 7, 8, 9), Sentinel-2, and MODIS (Terra/Aqua),
including a "LandsatAll" option that automatically merges available missions for any
requested date range.
Two retrieval functions
- fetch(aoi, …)
Downloads data immediately via GEE’s
getDownloadURLAPI. Best for small-to-medium AOIs (up to ~100 km²).- fetch_xee(aoi, …)
Opens the GEE collection as a lazy Dask-backed xarray via
xee. No data is transferred until.compute()is called. Suitable for large AOIs and long time series.
Temporal aggregation
Both functions accept a temporal_aggregation parameter:
"all"— every individual scene (default)"annual"— one median composite per calendar year"monthly"— one median composite per calendar month"seasonal"— one composite per meteorological season (DJF/MAM/JJA/SON)
Sensors and date coverage
"Landsat" is an alias for "Landsat8" for backward compatibility.
Landsat 7 SLC-off note
The Scan Line Corrector (SLC) on Landsat 7 ETM+ failed on 2003-05-31.
Images acquired after this date have wedge-shaped data gaps covering roughly
22 % of each scene. Use use_slc_off=False (default) to exclude these
images and use only the good-quality 1999–2003 record. Set use_slc_off=True
to include post-failure images (useful when other sensors have no coverage, e.g.
1999–2012 before Landsat 8).
MODIS note
MODIS provides 500m resolution surface reflectance composites (8-day intervals).
Use "MODISAll" to automatically merge Terra and Aqua for continuous coverage.
MODIS has coarser resolution but longer temporal record (2000–present) compared
to Landsat. Suitable for regional-scale studies where 500m resolution is adequate.
Band mapping differs from Landsat — MODIS stores Red as Band 1, NIR as Band 2,
Blue as Band 3, Green as Band 4. Cloud masking uses StateQA bits 0-1 (cloud state)
and bit 2 (shadow). Scale factor is 0.0001 with no offset (unlike Landsat C02 L2’s
-0.2 offset). AWEIsh and AWEInsh are computed server-side for MODIS since all
required bands are available.
DEM masking
Server-side DEM masking using Copernicus GLO-30 can be activated with the
dem_mask parameter in fetch(). This applies terrain filters directly
in GEE using ee.Terrain.slope() and reduceNeighborhood, avoiding the
need to download the DEM. The snow question is addressed by min_temp_c in
climate-adaptive mode (ERA5 precipitation includes snowfall; filtering to months
≥5°C removes cold months where precipitation is frozen) and by max_elevation_m
(above the local glaciation line, always mask regardless of other criteria).
Requirements
earthengine-api : always required
rasterio : required by fetch()
xee + dask : required by fetch_xee()
Install: pip install wetlandmapper[gee]
Authenticate once: earthengine authenticate
- wetlandmapper.gee.authenticate() None[source]
Run interactive GEE authentication (opens browser; only needed once).
- wetlandmapper.gee.fetch(aoi: dict | str, start: str, end: str, sensor: str = 'Landsat8', index: str | Sequence[str] = 'MNDWI', custom_indices: dict[str, str] | None = None, scale: int = 30, max_cloud_cover: float = 20.0, temporal_aggregation: str = 'all', use_slc_off: bool = False, project: str | None = None, climate_adaptive: bool = False, min_precip_mm: float = 20.0, min_temp_c: float = 5.0, hydroperiod_months: int = 1, wetness_index: str = 'MNDWI', wetness_threshold: float = 0.0, dem_mask: bool = False, max_slope_deg: float | None = 5.0, max_tpi_m: float | None = None, tpi_window_px: int = 5, max_local_range_m: float | None = None, local_range_window_px: int = 5, max_elevation_m: float | None = None, months: list[int] | None = None, reduction_method: str = 'median', percentile: float = 50.0) DataArray | Dataset[source]
Retrieve spectral indices from GEE as an xarray object (immediate download).
Fetches an image collection, applies cloud masking and surface-reflectance scaling, computes MNDWI / NDVI / NDTI server-side, optionally composites by time period, and downloads the result.
- Parameters:
Area of interest. Accepts:
GeoJSON dict — plain geometry, Feature, or FeatureCollection.
Shapefile path —
str/Pathto a.shp(or any format readable bygeopandas). Multiple features are dissolved into one boundary. Requirespip install geopandas.GeoJSON file path — a
.geojson/.jsonfile.
start (str) – Start date ISO 8601, e.g.
"2000-01-01".end (str) – End date (inclusive) ISO 8601, e.g.
"2023-12-31".sensor (str) – Satellite sensor. One of:
"Landsat4","Landsat5","Landsat7","Landsat8"(default),"Landsat9","LandsatAll","Sentinel2","MODIS_Terra","MODIS_Aqua","MODISAll"."Landsat"is an alias for"Landsat8".index (str or list of str) – One or more of
"MNDWI","NDWI","NDVI","NDTI","AWEIsh","AWEInsh". Single str → DataArray; list → Dataset (both with time dim).custom_indices (dict[str, str], optional) –
User-defined index formulas evaluated server-side via
ee.Image.expression. Dictionary keys are output band names and values are expression strings. Available formula symbols areblue,green,red,nir,swir(aliasswir1),swir2, andqa.Example:
{"NDSI": "(green - swir) / (green + swir)"}. To request a custom index, include its name inindex.scale (int) – Spatial resolution in metres. Default 30 (Landsat native).
max_cloud_cover (float) – Maximum cloud cover (%) per image. Default 20.
temporal_aggregation ({"all", "annual", "monthly", "seasonal"}) – Server-side temporal compositing. Default
"all"(every scene). Using"annual"or"monthly"greatly reduces download volume.use_slc_off (bool) – Include Landsat 7 SLC-off images (acquired after 2003-05-31)? Default
False. Only relevant whensensoris"Landsat7"or"LandsatAll".project (str, optional) – GEE cloud project ID.
climate_adaptive (bool) – If
True, replace the standard temporal composite with a climate-adaptive annual composite guided by ERA5-Land precipitation and temperature. When enabled,temporal_aggregationis ignored (output is always one image per year). DefaultFalse.months (list of int, optional) – Restrict compositing to these calendar months (1=Jan, …, 12=Dec). Example: [6, 7, 8] for June–August only. Default None (all months).
reduction_method ({"median", "mean", "percentile"}) – Collection reducer applied within each temporal aggregation window. Default
"median".percentile (float) –
Percentile to use when
reduction_method="percentile". Must be between 0 and 100 inclusive. Default 50.0.When
climate_adaptive=True, the algorithm:Builds monthly Landsat composites internally.
Joins with ERA5-Land monthly precipitation and 2m temperature.
Filters months where precip >=
min_precip_mmAND temp >=min_temp_c(excludes dry season and snow months).For each year, selects per-pixel values from the month with peak precipitation using
qualityMosaic(captures maximum wetness rather than an arbitrary median).Masks pixels wet for fewer than
hydroperiod_monthsmonths per year on average (removes transient waterlogging).
min_precip_mm (float) – Minimum monthly precipitation (mm) to include a month in the composite window. Used only when
climate_adaptive=True. Default 20 mm.min_temp_c (float) – Minimum monthly mean 2m temperature (degrees C) to include a month. Excludes frozen-ground and snow months. Used only when
climate_adaptive=True. Default 5 degrees C.hydroperiod_months (int) – Minimum number of months per year a pixel must be wet (index above
wetness_threshold) on average across the full record to be retained. Pixels below this are masked as transient waterlogging. Used only whenclimate_adaptive=True. Default 1. Increase to 2-3 for stricter wetland delineation.wetness_index (str) – Which index band to use as the wetness indicator for hydroperiod counting and qualityMosaic selection. Must be one of the bands in
index. Default"MNDWI".wetness_threshold (float) – Index value above which a pixel is counted as wet for the hydroperiod calculation. Default 0.0.
dem_mask (bool) – If
True, apply a server-side terrain flatness mask using the Copernicus GLO-30 DEM before compositing. Masks out glaciers, snowpacks, and steep mountain terrain that produce false wetness signals. DefaultFalse.max_slope_deg (float or None) – Maximum terrain slope (degrees) to retain when
dem_mask=True. Default 5.0. Set toNoneto disable slope filtering.max_tpi_m (float or None) – Maximum absolute TPI (metres) when
dem_mask=True. DefaultNone(disabled).tpi_window_px (int) – Focal window radius in pixels for TPI. Default 5.
max_local_range_m (float or None) – Maximum local elevation range (metres) in the rolling window when
dem_mask=True. DefaultNone(disabled).local_range_window_px (int) – Window radius for local elevation range. Default 5.
max_elevation_m (float or None) – Absolute elevation ceiling (metres). Pixels above this elevation are always masked when
dem_mask=True. DefaultNone.
- Returns:
xr.DataArray – DataArray with dims
(time, y, x)when a single index is requested.xr.Dataset – Dataset with one variable per index, dims
(time, y, x).
Notes
Time steps where no valid cloud-free pixels exist are skipped with a
UserWarningrather than raising an error. The returned object will have fewer time steps than requested periods in such cases.Examples
Long-record annual MNDWI for dynamics using all available Landsat missions:
>>> mndwi = fetch(aoi, "1984-01-01", "2023-12-31", ... sensor="LandsatAll", ... temporal_aggregation="annual") >>> dynamics = classify_dynamics(mndwi, nYear=3)
Post-monsoon WCT composite from Landsat 5 era:
>>> indices = fetch(aoi, "2005-10-01", "2005-12-31", ... sensor="Landsat5", ... index=["MNDWI", "NDVI", "NDTI"])
Annual MNDWI from MODIS for regional-scale analysis:
>>> mndwi_modis = fetch(aoi, "2000-01-01", "2023-12-31", ... sensor="MODISAll", ... temporal_aggregation="annual", ... scale=500)
- wetlandmapper.gee.fetch_xee(aoi: dict | str, start: str, end: str, sensor: str = 'Landsat8', index: str | Sequence[str] = 'MNDWI', custom_indices: dict[str, str] | None = None, scale: int = 30, max_cloud_cover: float = 20.0, temporal_aggregation: str = 'all', use_slc_off: bool = False, project: str | None = None, climate_adaptive: bool = False, min_precip_mm: float = 20.0, min_temp_c: float = 5.0, hydroperiod_months: int = 1, wetness_index: str = 'MNDWI', wetness_threshold: float = 0.0, dem_mask: bool = False, max_slope_deg: float | None = 5.0, max_tpi_m: float | None = None, tpi_window_px: int = 5, max_local_range_m: float | None = None, local_range_window_px: int = 5, max_elevation_m: float | None = None, chunks: dict | None = None, months: list[int] | None = None, reduction_method: str = 'median', percentile: float = 50.0) DataArray | Dataset[source]
Retrieve spectral indices from GEE as a lazy Dask-backed xarray via xee.
- Parameters:
aoi (dict, str, or Path) – GeoJSON dict, shapefile path, or GeoJSON file path.
start (str) – ISO 8601 date strings.
end (str) – ISO 8601 date strings.
sensor (str) – Sensor key — see
fetch()for the full list. Default"Landsat8".index (str or list of str) – One or more of
"MNDWI","NDWI","NDVI","NDTI","AWEIsh","AWEInsh". Single str → DataArray; list → Dataset.custom_indices (dict[str, str], optional) –
User-defined index formulas evaluated server-side via
ee.Image.expression. Dictionary keys are output band names and values are expression strings. Available formula symbols areblue,green,red,nir,swir(aliasswir1),swir2, andqa.Example:
{"NDSI": "(green - swir) / (green + swir)"}. To request a custom index, include its name inindex.scale (int) – Pixel resolution in metres. Default 30.
max_cloud_cover (float) – Maximum per-image cloud cover (%). Default 20.
temporal_aggregation ({"all", "annual", "monthly", "seasonal"}) –
Server-side compositing mode. Default
"all".Warning
"all"passes the raw scene collection to xee with no compositing. Older xee versions may return integer time indices (0, 1, 2 …) instead of real timestamps for this mode. This function detects the issue, emits aUserWarning, and patches the time coordinate automatically by queryingsystem:time_startfrom GEE. Use"annual"or"monthly"to avoid the extra round-trip.use_slc_off (bool) – Include Landsat 7 SLC-off scenes. Default
False.project (str, optional) – GEE cloud project ID.
climate_adaptive
min_precip_mm
min_temp_c
hydroperiod_months
:param : :param wetness_index: :param wetness_threshold: :param dem_mask: :param max_slope_deg: :param : :param max_tpi_m: :param tpi_window_px: :param max_local_range_m: :param : :param local_range_window_px: Same semantics as
fetch(). :param max_elevation_m: Same semantics asfetch(). :param chunks: Dask chunk sizes, e.g.{"time": 1, "lon": 512, "lat": 512}. :type chunks: dict, optional :param months: Restrict compositing to these calendar months (1=Jan, …, 12=Dec).Example: [6, 7, 8] for June–August only. Default None (all months).
- Parameters:
reduction_method ({"median", "mean", "percentile"}) – Collection reducer applied within each temporal aggregation window. Default
"median".percentile (float) – Percentile to use when
reduction_method="percentile". Must be between 0 and 100 inclusive. Default 50.0.
- Returns:
Lazy object with dims
(time, lat, lon).- Return type:
xr.DataArray or xr.Dataset
Notes
After calling
.compute(), orient dimensions with:da = da.rename({"lat": "y", "lon": "x"}).transpose("time", "y", "x")
Examples
>>> mndwi_lazy = fetch_xee( ... aoi, "1984-01-01", "2023-12-31", ... sensor="LandsatAll", temporal_aggregation="annual", ... ) >>> mndwi = ( ... mndwi_lazy ... .rename({"lat": "y", "lon": "x"}) ... .transpose("time", "y", "x") ... .compute() ... )