Basic NDVI Analysis#
A comprehensive guide to vegetation monitoring with NDVI (Normalized Difference Vegetation Index).
What is NDVI?#
NDVI measures vegetation health by comparing red and near-infrared (NIR) light reflected by plants:
Values: -1 to +1
Interpretation:
< 0: Water, clouds, snow
0 - 0.2: Bare soil, sand
0.2 - 0.5: Sparse vegetation, grassland
0.5 - 0.8: Dense vegetation, healthy crops
0.8: Very dense vegetation, forests
Basic NDVI Workflow#
Step 1: Setup#
import ee
import geemap
from ndvi2gif import NdviSeasonality
import matplotlib.pyplot as plt
# Initialize
ee.Initialize(project='your-project-id')
Step 2: Define Study Area#
# Example: Agricultural area in Spain
roi = ee.Geometry.Rectangle([-6.2, 37.8, -6.0, 38.0])
# Visualize
Map = geemap.Map()
Map.addLayer(roi, {}, 'Study Area')
Map.centerObject(roi, 11)
Map
Step 3: Create NDVI Processor#
ndvi = NdviSeasonality(
roi=roi,
sat='S2', # Sentinel-2 (10m resolution)
periods=12, # Monthly composites
start_year=2023,
end_year=2024,
index='ndvi',
key='median' # Robust to clouds (cloud filtering is on by default)
)
Step 4: Generate Composite#
# get_year_composite() returns an ImageCollection (one image per year)
composites = ndvi.get_year_composite()
composite_2023 = composites.first()
# Check band names
print("Bands:", composite_2023.bandNames().getInfo())
# Output: ['january', 'february', 'march', 'april', 'may', 'june',
# 'july', 'august', 'september', 'october', 'november', 'december']
Step 5: Visualize Results#
# Visualization parameters
vis_params = {
'bands': ['july', 'april', 'january'], # RGB = Summer, Spring, Winter
'min': 0,
'max': 0.8
}
# Add to map (a 3-band RGB composite does not take a palette)
Map = geemap.Map()
Map.addLayer(composite_2023, vis_params, 'NDVI 2023')
Map.addLayer(roi, {}, 'ROI')
Map.centerObject(roi, 11)
Map
Step 6: Create Animated GIF#
# Generate temporal animation (one frame per year, RGB from three periods)
ndvi.get_gif(
name='ndvi_monthly.gif',
bands=['july', 'april', 'january'] # [R, G, B] periods
)
Advanced NDVI Analysis#
Multi-Year Comparison#
Compare vegetation patterns across years:
# Process multiple years
ndvi_multi = NdviSeasonality(
roi=roi,
sat='S2',
periods=12,
start_year=2020,
end_year=2023,
index='ndvi',
key='median'
)
# One call returns all years (2020–2023) as an ImageCollection
composites = ndvi_multi.get_year_composite()
print("Years processed:", composites.size().getInfo())
Seasonal Analysis#
Focus on specific seasons:
# Quarterly analysis
ndvi_seasonal = NdviSeasonality(
roi=roi,
sat='S2',
periods=4, # Winter, Spring, Summer, Fall
start_year=2023,
end_year=2024,
index='ndvi'
)
composite = ndvi_seasonal.get_year_composite().first()
# Visualize seasons (period names for periods=4: winter, spring, summer, autumn)
Map = geemap.Map()
Map.addLayer(
composite.select('winter'),
{'min': 0, 'max': 0.8, 'palette': ['red', 'yellow', 'green']},
'Winter'
)
Map.addLayer(
composite.select('summer'),
{'min': 0, 'max': 0.8, 'palette': ['red', 'yellow', 'green']},
'Summer'
)
Map.centerObject(roi, 11)
Map
High Temporal Resolution#
Bi-monthly for detailed crop monitoring:
ndvi_detailed = NdviSeasonality(
roi=roi,
sat='S2',
periods=24, # Every ~15 days
start_year=2024,
end_year=2024,
index='ndvi',
key='percentile',
percentile=75
)
Statistical Methods for NDVI#
Median (Recommended)#
# Best for cloud-prone areas
ndvi_median = NdviSeasonality(
roi=roi,
sat='S2',
periods=12,
start_year=2023,
end_year=2024,
index='ndvi',
key='median' # Robust to outliers
)
Maximum#
# Maximum greenness
ndvi_max = NdviSeasonality(
roi=roi,
sat='S2',
periods=12,
start_year=2023,
end_year=2024,
index='ndvi',
key='max' # Peak values
)
Percentile (Flexible)#
# 85th percentile - good balance
ndvi_p85 = NdviSeasonality(
roi=roi,
sat='S2',
periods=12,
start_year=2023,
end_year=2024,
index='ndvi',
key='percentile',
percentile=85
)
Dispersion (How Much NDVI Moves)#
The reducers above summarise the level of NDVI in each period. std,
variance, range and cv summarise its variability instead, which is what
you want when the question is “where is the vegetation changing”, not “how green
is it”:
# Within-season NDVI variability
ndvi_std = NdviSeasonality(
roi=roi,
sat='S2',
periods=4,
start_year=2023,
end_year=2024,
index='ndvi',
key='std' # Standard deviation inside each season
)
Map.addLayer(
ndvi_std.get_year_composite().first().select('spring'),
{'min': 0, 'max': 0.2, 'palette': ['white', 'orange', 'red']},
'Spring NDVI variability'
)
Stable surfaces (bare soil, water, evergreen forest, asphalt) come out low;
croplands, deciduous canopies and recently disturbed or flooded areas come out
high. Use range for a more intuitive amplitude in index units, and cv only
for indices that stay positive.
Extracting Statistics#
Zonal Statistics#
Extract mean NDVI per month:
# Get time series for ROI
composite = ndvi.get_year_composite().first()
# Extract values for each month
stats = []
for month in range(1, 13):
band_name = composite.bandNames().get(month-1).getInfo()
value = composite.select(band_name).reduceRegion(
reducer=ee.Reducer.mean(),
geometry=roi,
scale=10,
maxPixels=1e9
).getInfo()
stats.append({
'month': month,
'ndvi': value[band_name]
})
# Plot
import pandas as pd
df = pd.DataFrame(stats)
df.plot(x='month', y='ndvi', kind='line', marker='o')
plt.title('Mean NDVI by Month')
plt.xlabel('Month')
plt.ylabel('NDVI')
plt.grid(True)
plt.show()
Point Sampling#
Extract NDVI at specific locations:
# Define sample points
points = ee.FeatureCollection([
ee.Feature(ee.Geometry.Point([-6.1, 37.9]), {'name': 'Site A'}),
ee.Feature(ee.Geometry.Point([-6.15, 37.95]), {'name': 'Site B'})
])
# Sample composite
composite = ndvi.get_year_composite().first()
samples = composite.sampleRegions(
collection=points,
scale=10
).getInfo()
print(samples['features'])
Export Options#
Export as GeoTIFF#
# Exports each year's composite; filenames auto-generated as {sat}_{index}_{key}_{year}.tif
ndvi.get_export(scale=10)
Export to Google Drive#
composite = ndvi.get_year_composite().first()
ndvi.export_to_drive(
image=composite,
description='ndvi_2023',
folder='earthengine_exports',
scale=10,
crs='EPSG:4326'
)
Interpretation Guide#
Agricultural Applications#
Early Season (Mar-Apr): NDVI 0.2-0.4 indicates crop emergence
Peak Season (Jun-Jul): NDVI > 0.6 shows healthy, mature crops
Harvest (Sep-Oct): NDVI drops < 0.3 after harvest
Bare Soil (Nov-Feb): NDVI ~ 0.1-0.2
Natural Vegetation#
Evergreen Forests: Consistent NDVI 0.7-0.9 year-round
Deciduous Forests: NDVI varies 0.3 (winter) to 0.8 (summer)
Grasslands: NDVI 0.3-0.6, responsive to rainfall
Wetlands: Variable, influenced by water levels
Common Issues & Solutions#
Issue: Low NDVI values everywhere#
Cause: Cloud contamination or wrong bands
Solution:
# Enable cloud masking
ndvi = NdviSeasonality(
roi=roi,
sat='S2',
periods=12,
start_year=2023,
end_year=2024,
index='ndvi',
cloud_filter=True, # Cloud filtering (on by default)
max_cloud_cover=10 # ← Stricter scene cloud threshold
)
Issue: Patchy results#
Cause: Insufficient images
Solution: Use longer time periods or lower percentiles:
# Extend time period
start_year=2022, end_year=2024 # More images
# Or use median instead of max
key='median'
Issue: Negative NDVI on land#
Cause: Water bodies or very bare soil
Solution: This is normal! Mask water if needed:
# Mask water bodies
import ee
# Add water mask (example)
water = ee.Image('JRC/GSW1_3/GlobalSurfaceWater').select('occurrence')
composite_masked = composite.updateMask(water.lt(50)) # Keep only low water occurrence
Next Steps#
Indices Reference - Explore other vegetation indices
Time Series Analysis - Detect trends and phenology
Classification - Use NDVI for land cover mapping