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.geojsonand uses CRSEPSG: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
globaccordingly
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
rioxarrayto 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
nodatavalue 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.05to0.1if 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
cmapto any Matplotlib colormap (PuBuGn,YlGn, etc.) - Keep
vmin=0.0,vmax=1.0since 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
bwrorRdBualso 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