Skip to content

Streamlit App Guide — (streamlit_app.py)

This page explains how to run the Streamlit map, compare years, tune the visualization, and adjust code paths when needed.

Purpose:
The Streamlit app renders yearly NDVI composites and an interactive change layer (ΔNDVI) over your AOI using a Leaflet map inside Streamlit.


Overview

The Streamlit app is the front-end interface of the Deforestation Viewer.
It allows you to explore vegetation trends interactively and visualize NDVI change over time using pre-computed composites generated by search_download.py.


1. Quick Start

Prerequisites

  • You have generated NDVI composites with src/search_download.py.
    Output files should be located in:
    data/composites/ndvi_median_<YEAR>.tif
  • Your AOI exists at data/aoi/roi.geojson and uses CRS EPSG:4326.

Run the App

Be sure you’re in the repository root and run:

streamlit run src/streamlit_app.py

The app automatically discovers available composite rasters in data/composites/ and populates the year selector.


2. Using the App

Modes

You can switch between two modes at the top of the app:

  • View single year
    Displays NDVI for one selected year using a green-to-red scale.
  • Green → healthy vegetation
  • Red → low vegetation or disturbance

  • Compare change (ΔNDVI)
    Choose a “From” year and a “To” year. The app computes the difference on the fly:
    ΔNDVI = NDVI(To) − NDVI(From)

  • Positive values indicate greening
  • Negative values indicate vegetation loss

Map Basics

  • Basemap: Esri.WorldImagery (high-resolution satellite imagery)
  • Map centers automatically on your AOI centroid
  • Use the layer control (upper-right corner) to toggle NDVI and change layers

3. Getting the Most Out of It

Pick Useful Year Pairs

  • Try comparing 2016 → 2020, 2000 → 2021, or pre-event → post-event periods
  • Smaller AOIs improve performance and render faster

Normalize Change Smartly

ΔNDVI can have extreme values. The app computes symmetric limits from the 2nd and 98th percentiles:

vmin, vmax = robust_delta_range(delta_tif)

This keeps the visualization balanced and comparable across regions.


4. File and Path Layout

data/
├── aoi/
│   └── roi.geojson
├── composites/
│   ├── ndvi_median_1985.tif
│   ├── ndvi_median_1986.tif
│   └── ...
└── change/
    └── ndvi_delta_<FROM>_<TO>.tif
  • AOI: data/aoi/roi.geojson
  • Composites: pre-generated annual NDVI rasters
  • Change rasters: created automatically during comparison

5. Understanding the Code

Directory Assumptions

BASE_DIR = pl.Path(__file__).resolve().parents[1]
COMP_DIR = BASE_DIR / "data" / "composites"
CHANGE_DIR = BASE_DIR / "data" / "change"
CHANGE_DIR.mkdir(parents=True, exist_ok=True)

Update these paths if you store data elsewhere.


Discovering Available Years

files = sorted(list(COMP_DIR.glob("ndvi_median_*.tif")) + list(COMP_DIR.glob("ndvi_median_*.tiff")))
years = sorted({int(p.stem.split("_")[-1]) for p in files})
  • Automatically detects years from filenames
  • If you change the naming pattern, update the glob accordingly

Centering on the AOI

aoi = gpd.read_file(BASE_DIR / "data" / "aoi" / "roi.geojson")
center = [aoi.geometry.centroid.y.mean(), aoi.geometry.centroid.x.mean()]
  • Computes the centroid of your AOI for the initial map view
  • Falls back to [0, 0] if AOI is missing

Reading Rasters and Aligning Grids

def open_ndvi(y: int):
    p = ndvi_path(y)
    da = rxr.open_rasterio(str(p)).squeeze()
    return da
  • Uses rioxarray to read the COG file
  • squeeze() removes extra dimensions (e.g., if the raster has a singleton band dimension)
def write_delta_tif(y1: int, y2: int) -> pl.Path:
    ndvi1 = open_ndvi(y1)
    ndvi2 = open_ndvi(y2)
    if not (ndvi2.rio.crs == ndvi1.rio.crs and ndvi2.rio.transform() == ndvi1.rio.transform()):
        ndvi2 = ndvi2.rio.reproject_match(ndvi1)
    delta = (ndvi2 - ndvi1)
    delta = delta.rio.write_nodata(-9999, inplace=False).fillna(-9999)
    delta = delta.rio.write_crs(ndvi1.rio.crs, inplace=False)
    delta.rio.to_raster(out, driver="COG", compress="DEFLATE")
  • Ensures both rasters share the same CRS and grid before differencing
  • Writes ΔNDVI once and reuses it for subsequent runs
  • You can change the nodata value from -9999, but keep it consistent across outputs

Color Range for ΔNDVI

def robust_delta_range(delta_path: pl.Path):
    p2, p98 = np.percentile(vals[mask], [2, 98])
    mx = float(max(abs(p2), abs(p98), 0.05))
    return (-mx, mx)
  • Creates a symmetric range around zero for fair visualization of gain/loss
  • Increase the minimum floor from 0.05 to 0.1 if the changes look muted

Map Layer Styling

Single Year Mode

m.add_raster(raster_path, cmap="RdYlGn", opacity=0.9, layer_name=f"NDVI {year}")
m.add_colormap(cmap="RdYlGn", vmin=0.0, vmax=1.0, label=f"NDVI {year}")
  • You can change cmap to any Matplotlib colormap (PuBuGn, YlGn, etc.)
  • Keep vmin=0.0, vmax=1.0 since NDVI is normalized to [0, 1]

Change Mode

m.add_raster(str(delta_tif), colormap="coolwarm", vmin=vmin, vmax=vmax, opacity=0.85, layer_name=f"ΔNDVI {y_from}{y_to}")
m.add_colormap(cmap="coolwarm", vmin=vmin, vmax=vmax, label=f"ΔNDVI {y_from}{y_to}")
  • Diverging colormaps like bwr or RdBu also work well for ΔNDVI

6. Tips for Performance

  • Use smaller AOIs for faster rendering
  • Avoid comparing very distant years when testing
  • If tiles render slowly, verify composites were generated with reasonable chunk sizes and close other resource-heavy programs

7. Troubleshooting

Symptom Likely Cause Fix
No years found Composites not generated or filename pattern changed Run the NDVI pipeline or update the COMP_DIR.glob(...) pattern
Map centers to (0, 0) AOI missing or invalid Place a valid AOI at data/aoi/roi.geojson with CRS EPSG:4326
ΔNDVI looks noisy Grids not aligned between years The app reprojects automatically; rerun if inputs changed
Colors appear off vmin/vmax range too narrow or wide Adjust robust_delta_range floor or set fixed limits manually

Next Steps

Once you’re satisfied with your composites and visualization:

  • Document your AOI and NDVI results
  • Consider exporting ΔNDVI rasters for GIS-based analysis
  • See search_download.md for details on generating input composites