Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

04 — Image processing (Caldor fire dNBR)

Image processing — radiometry, cloud masks, indices

Walk through the three RS operator modules end-to-end on a real burn-scar scenario: the 2021 Caldor fire in El Dorado County, California, just south-west of Lake Tahoe. We compose:

  1. gz.radiometry — ToFloat32 → DNToReflectance → PercentileClip → Gamma for a display pipeline; the same DNToReflectance powers the index chain.
  2. gz.cloud — MaskFromSCL + gz.mask.ApplyMask to drop clouds and invalid pixels.
  3. gz.indices — NBR (burn-sensitive), NDVI (vegetation), NDWI (water) computed from the same reflectance carrier via gz.Fanout. The burn signature lives in dNBR = NBR_pre − NBR_post.

Scenario.

AOI bbox (EPSG:4326)(-120.30, 38.55, -119.95, 38.95) (Caldor fire core)
Pre-fire sceneS2B_..20210809T.. (5 days before ignition, 0.1 % cloud)
Post-fire sceneS2A_..20211112T.. (3 weeks after containment, 1.4 % cloud)
MGRS tile10SGH
BandsB02, B03, B04, B08, B12, SCL (BGRN + SWIR + scene-classification)

Caldor burned ~221,835 acres between 14 August and 21 October 2021, the second wildfire ever to cross the Sierra Nevada crest.

1. Load pre- and post-fire Sentinel-2 scenes

Search MPC for one cloud-free scene before ignition and one after containment, both on MGRS tile 10SGH. Load the spectral bands + SCL, resample everything to the 10 m grid.

pre  → S2B_MSIL2A_20210809T183919_R070_T10SGH_20210810T105317  cloud=0.07%
post → S2B_MSIL2A_20211127T184719_R070_T10SGH_20211128T194120  cloud=0.93%
pre_refl:  shape=(5, 2973, 3182)  dtype=uint16
post_refl: shape=(5, 2973, 3182)  dtype=uint16

2. Display pipeline — ToFloat32 → DNToReflectance → PercentileClip → Gamma

Build the standard four-stage gz.radiometry chain. ToFloat32 escapes the uint16 DN dtype, DNToReflectance applies the Sentinel-2 L2A scaling, PercentileClip is a robust per-band 2-98 % stretch, and Gamma brightens the midtones. All four are stock gz.radiometry operators — no custom code.

Sequential([ToFloat32(), DNToReflectance(scale=0.0001, offset=0.0, axis=0), PercentileClip(p_min=2.0, p_max=98.0, axis=(-2, -1)), Gamma(g=1.2)])
<Figure size 1300x700 with 2 Axes>

3. Cloud masking — MaskFromSCL

gz.cloud.MaskFromSCL returns True where the SCL band’s value is in the supplied class set. Pair with gz.cloud.SCL_CLOUDS_AND_INVALID to drop clouds + cirrus + cloud shadows + no-data in one pass.

pre  mask: 0.0007 fraction masked
post mask: 0.0302 fraction masked

4. Indices via Fanout — NDVI + NBR + NDWI from one reflectance carrier

gz.Fanout is sugar over a single-input Graph: one input, many named outputs. The Graph topologically sorts the nodes, so DNToReflectance runs exactly once and its output is shared by all three index ops — no redundant arithmetic.

  • NDVI ((NIR − Red)/(NIR + Red)) — vegetation
  • NBR ((NIR − SWIR)/(NIR + SWIR)) — burn severity (drops over scarred ground)
  • NDWI ((Green − NIR)/(Green + NIR)) — water / saturation

We also slot gz.mask.ApplyMask per scene to NaN out cloudy pixels.

pre keys : ['ndvi', 'nbr', 'ndwi']
post keys: ['ndvi', 'nbr', 'ndwi']
  pre  ndvi : range=[-0.996,  0.999]
  pre  nbr  : range=[-0.999,  0.877]
  pre  ndwi : range=[-0.999,  0.997]

5. dNBR — the burn-severity signal

Subtract post-fire NBR from pre-fire NBR. Healthy unchanged vegetation cancels out; burned pixels show a strong positive differenced-NBR. USGS uses the threshold table:

dNBRSeverity
< 0.10Unburned
0.10 – 0.27Low
0.27 – 0.44Moderate-low
0.44 – 0.66Moderate-high
> 0.66High
dNBR range: [-1.949, 1.708]  mean=0.010
<Figure size 1800x700 with 4 Axes>

The right-hand panel is the burn footprint: cyan / blue pixels were unchanged forest, orange / red pixels are moderate-to-high burn severity — the Caldor scar.

6. NDVI loss — vegetation impact

The same Fanout shape lets us pull NDVI out of both scenes for free. Where vegetation burned, NDVI drops sharply; we visualise the raw post-fire NDVI alongside ΔNDVI.

<Figure size 1300x700 with 4 Axes>

7. Summary

The three operator modules compose cleanly on real data:

  • gz.radiometry — ToFloat32 | DNToReflectance | PercentileClip | Gamma is the canonical display chain; DNToReflectance alone is the index-feed chain.
  • gz.cloud — MaskFromSCL(classes=SCL_CLOUDS_AND_INVALID) produces a True-where-bad mask in one line; pair with gz.mask.ApplyMask to drop those pixels as NaN.
  • gz.indices — every index (NDVI, NBR, NDWI, EVI, SAVI, MNDWI, …) has the same constructor shape (band indices, axis, epsilon), so they plug into Fanout interchangeably.

The composition primitives (|, Fanout) keep the multi-scene + multi-index logic linear and explicit: one reflectance carrier per scene, one Fanout per scene, then NumPy for the cross-scene diff. No custom operator subclasses required.

For the bigger picture — STAC discovery, tiled inference, multi-package composition — see the canonical multi-repo notebook: geocatalog/docs/notebooks/end_to_end_lake_tahoe.ipynb.