import os
import earthaccess
import h5py
import numpy as np
import matplotlib.pyplot as plt
import glob
from scipy.interpolate import griddata
from osgeo import gdal, osr
Community-Generated Lesson: Mapping Wildfire Burned Areas Using VIIRS/AVIRIS-3 Data (Python)
Overview
In this lesson, you will learn to access, preprocess, and analyze AVIRIS-3 data to map burned areas from the Palisades Fire in Los Angeles. Using satellite imagery obtained through the VIIRS tool, you will examine pre- and post-fire conditions, assess the fire’s impact on vegetation and infrastructure.
Learning Objectives
By the end of this lesson, you should be able to:
Access and download AVIRIS-3 and VIIRS Burn Area data for wildfire analysis.
Preprocess and clean AVIRIS-3 spectral radiance data for visualization.
Compare pre- and post-fire imagery to assess vegetation and burned area changes.
Overlay VIIRS Burn Area data with AVIRIS-3 images using GIS tools to map burned areas.
Evaluate wildfire impact by analyzing the overlap of burn data with infrastructure (e.g., buildings, population, critical infrastructure).
Quantify damage and assess risk to urban zones and infrastructure.
Introduction
Wildfires are one of the most devastating natural disasters, affecting ecosystems, wildlife, and human settlements. The Palisades Fire, which ignited on January 6, 2025, in the Los Angeles area, is a recent example of a wildfire that spread rapidly, impacting both natural environments and urban infrastructure. In this lesson, we will use AVIRIS-3 satellite imagery, accessed via the VIIRS tool, and VIIRS burn area data to analyze the fire’s effects.
By comparing pre-fire and post-fire imagery, we will evaluate how vegetation changed and overlay this information with infrastructure data to assess the damage. This exercise will help you understand how remote sensing can support wildfire monitoring and risk analysis.
Palisades Fire Overview
The Palisades Fire affected large parts of the Los Angeles region, particularly residential areas and natural vegetation. Satellite imagery from AVIRIS-3 and VIIRS provides us with valuable data to assess the extent of the fire’s impact. By overlaying burn area data with infrastructure maps, we can determine how much of the urban and natural landscapes were affected. The data allows us to quantify the impact and identify areas at high risk.
Accessing Wildfire Data
Understanding AVIRIS-3 Data
AVIRIS-3 provides high-resolution hyperspectral imagery across a wide range of wavelengths, allowing for detailed analysis of land surface properties.
A NASA Earthdata account is needed to download NASA Earthdata files.
# Authenticate with NASA Earthdata (you need an account)
= earthaccess.login()
Auth # Search for VIIRS Burned Area data
= earthaccess.search_data(
viirs_data ="AV3_L1B_RDN_2356",
short_name=('2025-01-16', '2025-01-16'), # Palisades's wildfire (January 6 - January 17 2025)
temporal=(-118.8, 33.9, -118.46, 34.28) # Bounding box for Palisades California
bounding_box
)
print(f"Found {len(viirs_data)} files.")
Processing and Visualizing Satellite Images
# Extract RDN.nc file URLs from VIIRS data
= [
granule_urls for file_url in file.data_links() if file_url.endswith('RDN.nc')]
[file_url for file in viirs_data[-6:]
]
for granule in granule_urls:
# Download files
'data/granule_files')
earthaccess.download(granule,
print("Processing complete for all granules.")
Plotting Data
# Plot first granule - Frist Flight
= "data/granule_files/AV320250116t193840_005_L1B_RDN_3f4aef90_RDN.nc"
file_path
# Open the file and plot radiance with geolocation
with h5py.File(file_path, 'r') as f:
= f['radiance']['radiance']
radiance = f['lat'][:]
lat = f['lon'][:]
lon
# Take a specific band (like band 25)
= radiance[25, :, :]
data
# Create cell edges
= (lon[:-1, :-1] + lon[1:, 1:]) / 2
lon_edges = (lat[:-1, :-1] + lat[1:, 1:]) / 2
lat_edges
# Sort to fix the monotonic issue
= np.sort(lon_edges, axis=1)
lon_edges = np.sort(lat_edges, axis=0)
lat_edges
=(12, 4))
plt.figure(figsize-1, :-1], shading='auto', cmap='viridis')
plt.pcolormesh(lon_edges, lat_edges, data[:='Radiance')
plt.colorbar(label'Radiance (Band 25) with Geolocation')
plt.title('Longitude')
plt.xlabel('Latitude')
plt.ylabel( plt.show()
# Plot first bacth of granules - First Flight
= glob.glob('data/*/*40_*RDN.nc')
granule_paths
= plt.subplots(1, 6, figsize=(16, 2)) # 1 row, 6 columns
fig, axes
for i, path in enumerate(granule_paths):
with h5py.File(path, 'r') as f:
= f['radiance']['radiance']
radiance = f['lat'][:]
lat = f['lon'][:]
lon = radiance[25, :, :]
data = (lon[:-1, :-1] + lon[1:, 1:]) / 2
lon_edges = (lat[:-1, :-1] + lat[1:, 1:]) / 2
lat_edges = np.sort(lon_edges, axis=1)
lon_edges = np.sort(lat_edges, axis=0)
lat_edges = axes[i].pcolormesh(lon_edges, lat_edges, data[:-1, :-1], shading='auto', cmap='viridis')
pcm
plt.tight_layout() plt.show()
= ['01', '03', '05']
flights
for flight in flights:
# List of file paths (ordered west to east)
= sorted(glob.glob(f'data/granule_files/*{flight}_*RDN.nc'))
file_paths
# Band to extract
= 25
band
# Define resolution for the common grid
= 1000
num_points
# Initialize common grid bounds
= float('inf'), float('-inf')
lon_min, lon_max = float('inf'), float('-inf')
lat_min, lat_max
# First loop to define grid bounds
for file_path in file_paths:
with h5py.File(file_path, 'r') as f:
= f['lat'][:]
lat = f['lon'][:]
lon = min(lon_min, lon.min())
lon_min = max(lon_max, lon.max())
lon_max = min(lat_min, lat.min())
lat_min = max(lat_max, lat.max())
lat_max
# Create common grid
= np.linspace(lon_min, lon_max, num_points)
grid_lon = np.linspace(lat_min, lat_max, num_points)
grid_lat = np.meshgrid(grid_lon, grid_lat)
grid_lon, grid_lat
# Initialize container for merged data
= []
merged_radiance_list
file = 1
# Second loop to process each granule and interpolate to the grid
for file_path in file_paths:
print(f'--> Flight: {flight}, File {file} out of {len(file_paths)}')
with h5py.File(file_path, 'r') as f:
= f['radiance']['radiance'][band, :, :]
radiance = f['lat'][:]
lat = f['lon'][:]
lon
# Flatten arrays for interpolation
= np.array([lon.flatten(), lat.flatten()]).T
points = radiance.flatten()
radiance_flat
# Interpolate to common grid
= griddata(
interpolated_radiance ='linear'
points, radiance_flat, (grid_lon, grid_lat), method
)
# Store result for averaging
merged_radiance_list.append(interpolated_radiance)
# Average the overlapping areas across all granules
= np.nanmean(merged_radiance_list, axis=0)
merged_radiance
# Plot the merged result
=(25, 3))
plt.figure(figsize='auto', cmap='viridis')
plt.pcolormesh(grid_lon, grid_lat, merged_radiance, shading='Radiance (uW nm-1 cm-2 sr-1)')
plt.colorbar(labelf'Merged Radiance (Band {band}) with Geolocation - Fixed')
plt.title('Longitude')
plt.xlabel('Latitude')
plt.ylabel( plt.show()
--> Flight: 01, File 1 out of 1
--> Flight: 03, File 1 out of 1
--> Flight: 05, File 1 out of 1
# Output file path
= f'data/granule_files/merged_radiance_{flight}.tif'
output_file
# Create a new GeoTIFF file
= gdal.GetDriverByName('GTiff')
driver = merged_radiance.shape
rows, cols = driver.Create(output_file, cols, rows, 1, gdal.GDT_Float32)
dataset
# Set geotransform and projection
= grid_lon.min(), grid_lon.max()
xmin, xmax = grid_lat.min(), grid_lat.max()
ymin, ymax = (xmax - xmin) / cols
xres = (ymax - ymin) / rows
yres
= (xmin, xres, 0, ymax, 0, -yres)
geotransform
dataset.SetGeoTransform(geotransform)
# Set projection to WGS84
= osr.SpatialReference()
srs 4326) # WGS84
srs.ImportFromEPSG(
dataset.SetProjection(srs.ExportToWkt())
# Write data to the file
1).WriteArray(merged_radiance)
dataset.GetRasterBand(1).SetNoDataValue(np.nan)
dataset.GetRasterBand(
# Close the dataset
= None
dataset
print(f"✅ GeoTIFF saved to: {output_file}")
Assessing Impcact
# ✅ Path to single granule file
= 'data/granule_files/AV320250116t193840_003_L1B_RDN_3f4aef90_RDN.nc'
file_path
print(f"🚀 Processing file: {file_path}")
# ✅ Open the file and read data
with h5py.File(file_path, 'r') as f:
# ✅ Radiance data
= f['radiance']['radiance']
radiance = f['lat'][:]
lat = f['lon'][:]
lon
# ✅ Take a specific band (like band 25)
= radiance[25, :, :]
data
# ✅ Create cell edges for proper alignment
= (lon[:-1, :-1] + lon[1:, 1:]) / 2
lon_edges = (lat[:-1, :-1] + lat[1:, 1:]) / 2
lat_edges
# ✅ Sort to fix the monotonic issue
= np.sort(lon_edges, axis=1)
lon_edges = np.sort(lat_edges, axis=0)
lat_edges
# ✅ Plot radiance
= plt.subplots(1, 2, figsize=(16, 6))
fig, axes
= axes[0].pcolormesh(lon_edges, lat_edges, data[:-1, :-1], shading='auto', cmap='viridis')
pcm =axes[0], label='Radiance (uW nm-1 cm-2 sr-1)')
fig.colorbar(pcm, ax0].set_title('Radiance (Band 25)')
axes[0].set_xlabel('Longitude')
axes[0].set_ylabel('Latitude')
axes[
# ✅ Choose NIR and SWIR bands
# NIR ≈ 858.6 nm → Band 63
# SWIR ≈ 2205.2 nm → Band 181
= radiance[63, :, :]
nir_band = radiance[181, :, :]
swir_band
# ✅ Compute NBR
= (nir_band - swir_band) / (nir_band + swir_band)
nbr
# ✅ Mask out invalid values
= np.ma.masked_invalid(nbr)
nbr
# ✅ Clip NBR values to between -1 and 1
= np.clip(nbr, -1, 1)
nbr
# ✅ Plot NBR
= axes[1].pcolormesh(lon_edges, lat_edges, nbr[:-1, :-1], shading='auto', cmap='RdYlBu_r', vmin=-1, vmax=1)
pcm =axes[1], label='NBR')
fig.colorbar(pcm, ax1].set_title('Normalized Burn Ratio (NBR)')
axes[1].set_xlabel('Longitude')
axes[1].set_ylabel('Latitude')
axes[
# ✅ Show all plots
plt.tight_layout() plt.show()
🚀 Processing file: data/granule_files/AV320250116t193840_003_L1B_RDN_3f4aef90_RDN.nc
Radiance (Band 25) Interpretation
The left image shows the radiance in the near-infrared (NIR) spectrum, which highlights vegetation and surface reflectance.
In healthy, undisturbed vegetation, NIR reflectance tends to be higher because of the internal leaf structure.
Burned or damaged areas typically show reduced NIR reflectance since the leaf structure is destroyed or altered.
Normalized Burn Ratio (NBR) Interpretation
The NBR image (right) measures fire severity by comparing the difference between NIR (healthy vegetation) and SWIR (sensitive to moisture loss and charring).
High NBR values (close to +1, shown in red) indicate healthy vegetation.
Low NBR values (close to -1, shown in blue) indicate burned or damaged areas.
Evidence of Fire Impact in NBR:
Large patches of light red/orange color over the coastal and mountainous areas indicate healthy vegetation.
However, the areas closer to the coastline and lowlands show lower NBR values (closer to 0 or negative), suggesting:
Loss of vegetation
Scorched or charred areas
Fire impact on the ground cover and tree canopy
The smooth red area along the coast suggests widespread burn scars or barren ground post-fire.
Key Findings:
The fire’s most severe damage seems to be concentrated along the lower coastal and urban-adjacent regions, where NBR values are lowest.
The higher, more rugged inland terrain shows less fire damage, consistent with the tendency of fires to spread more rapidly in low-lying dry areas.
The radiance data confirms a reduction in NIR reflectance in the areas showing low NBR, consistent with fire-induced vegetation loss.
The transition from low (red) to high (yellow) NBR values inland suggests a patchy burn pattern, where some areas were spared due to moisture or natural firebreaks.
Conclusion
The analysis of radiance and NBR data from the Palisades fire area indicates significant burn damage in the central and northern regions. The low NBR values in these areas confirm the presence of scorched and barren land, consistent with post-fire effects. The southern and coastal regions show higher NBR values, suggesting healthy vegetation and lower fire impact. This analysis demonstrates the effectiveness of hyperspectral remote sensing in identifying and quantifying fire damage. Further analysis could improve understanding of vegetation recovery and long-term environmental impact.
- The Palisades Fire caused moderate to severe vegetation loss along the coastal and urban-facing regions.
- Higher elevations and mountainous terrain were less affected.
- The fire left behind a visible burn scar, with lower vegetation recovery in the most intensely burned areas.