Choosing Between NDVI, NDRE and GNDVI
Two plots are visibly different in the field — one noticeably thicker than the other — and their NDVI values differ by 0.01. The index has not failed; it has saturated. Above a certain canopy density NDVI stops responding, and that density arrives well before the crop stops growing.
Choosing an index is choosing where in the growth cycle you want sensitivity, and the choice is made per crop and per stage rather than once for a programme. This page covers what each of the common indices responds to, where each stops responding, and how to decide between them with the data rather than by convention. It extends the computation in computing vegetation indices with rasterio.
What each index is actually measuring
All three are normalised differences between near-infrared and a shorter-wavelength band, and the choice of that second band decides everything.
NDVI uses red, which chlorophyll absorbs strongly. At low canopy cover, adding leaves removes red reflectance rapidly and the index climbs. Once the canopy closes, almost all the red is already absorbed — there is nothing left to remove — and further growth changes the index very little. Saturation typically begins around a leaf area index of 3, which for cereals is around the start of stem extension.
NDRE uses the red edge, the steep transition between the red absorption and the near-infrared plateau. Chlorophyll absorbs there far less strongly, so the band does not saturate at the same density, and the index keeps responding through a closed canopy. The cost is sensitivity at low cover: over sparse crop or bare soil, NDRE is noisier and less discriminating than NDVI.
GNDVI uses green. Chlorophyll absorbs green less than red but more than red edge, so it sits between the two, and it is more sensitive to chlorophyll concentration than to canopy amount — which makes it useful for nitrogen status once cover is established.
Figure 1 — Three responses to the same crop. The index that is right depends entirely on where on this axis the crop is.
Minimal reproducible solution
Rather than choosing by convention, choose by measuring which index discriminates on the data in hand. The test is simple: across the plots being compared, which index has the largest spread relative to its own within-plot noise.
import numpy as np
def discrimination_ratio(plot_values: dict[str, list[float]]) -> dict:
"""Between-plot spread relative to within-plot noise, per index.
An index that varies a lot between plots and little within them is
discriminating; one whose between-plot spread is comparable to its
within-plot scatter has saturated, whatever its absolute values look like.
"""
out = {}
for index_name, per_plot in plot_values.items():
medians = np.array([np.median(v) for v in per_plot if len(v) > 50])
within = np.array([np.std(v, ddof=1) for v in per_plot if len(v) > 50])
if medians.size < 3:
continue
between = float(np.std(medians, ddof=1))
noise = float(np.median(within) / np.sqrt(np.median([len(v) for v in per_plot])))
out[index_name] = {"between_plot_sd": between,
"within_plot_se": noise,
"ratio": between / max(noise, 1e-9)}
best = max(out, key=lambda k: out[k]["ratio"]) if out else None
return {"per_index": out, "most_discriminating": best}
Running this once per growth stage, on a trial where the plots are genuinely known to differ, answers the question for that crop and stage far better than any general rule. It also produces the argument to give an agronomist who wants NDVI because that is what they have always used.
The composite approach
Where the crop spans a range of densities within one field — which is usual — no single index is best everywhere. A composite that uses NDVI where cover is low and NDRE where it is high captures both.
import numpy as np
def composite_index(ndvi: np.ndarray, ndre: np.ndarray,
*, switch_low: float = 0.55, switch_high: float = 0.75) -> np.ndarray:
"""Blend NDVI and NDRE, weighted by how close NDVI is to saturation.
A hard switch at one NDVI value would put a visible discontinuity in the
map wherever the crop crosses it. A linear ramp between two thresholds
blends the two smoothly, and the ramp width is wide enough that the
transition is invisible.
"""
ndvi = np.asarray(ndvi, dtype=np.float32)
ndre = np.asarray(ndre, dtype=np.float32)
w = np.clip((ndvi - switch_low) / max(switch_high - switch_low, 1e-6), 0.0, 1.0)
out = (1.0 - w) * ndvi + w * _rescale_to(ndre, ndvi, w)
out[~np.isfinite(ndvi) | ~np.isfinite(ndre)] = np.nan
return out
def _rescale_to(source: np.ndarray, target: np.ndarray,
weight: np.ndarray) -> np.ndarray:
"""Put NDRE on NDVI's scale in the transition zone, so the blend is continuous."""
zone = (weight > 0.05) & (weight < 0.95) & np.isfinite(source) & np.isfinite(target)
if zone.sum() < 100:
return source
a, b = np.polyfit(source[zone], target[zone], 1)
return a * source + b
A composite is not free: it is a derived quantity with no literature values to compare against, and its own definition has to travel with it. For ranking and change detection within a programme that is entirely acceptable; for anything to be compared externally it is not.
Figure 3 — Saturation is the property that decides, not the formula.
Edge-case matrix
| Situation | Best choice | Why |
|---|---|---|
| Early growth, sparse cover | NDVI | Steepest response at low density |
| Closed canopy, cereals at stem extension | NDRE | NDVI has saturated |
| Nitrogen status assessment | NDRE or GNDVI | Sensitive to chlorophyll concentration |
| Sensor with no red edge band | NDVI or GNDVI | NDRE is not available |
| Bare soil mapping | NDVI | Largest dynamic range at low cover |
| Water delineation | NDWI (green/NIR) | Different question entirely |
| Comparing to a published threshold | Whichever the study used | Band centres must match, and usually do not |
| Mixed densities within one field | Composite, or report both | No single index is best everywhere |
The sensor row is a practical constraint worth checking before promising anything. A three-band NIR-modified camera cannot produce NDRE, and no processing recovers a band the sensor never captured.
Verification snippet
import numpy as np
def saturation_check(values: np.ndarray, *, high_quantile: float = 0.9,
flat_tolerance: float = 0.02) -> dict:
"""Is this index saturated over the dense part of the field?
Compare the spread of the top decile against the spread of the whole
distribution. A saturated index has a top decile compressed into a much
narrower band than the rest, because everything dense reads the same.
"""
v = values[np.isfinite(values)]
if v.size < 1000:
return {"note": "too few pixels to judge"}
cut = np.quantile(v, high_quantile)
top = v[v >= cut]
spread_top = float(np.percentile(top, 90) - np.percentile(top, 10))
spread_all = float(np.percentile(v, 90) - np.percentile(v, 10))
return {"top_decile_spread": spread_top, "overall_spread": spread_all,
"compression": spread_top / max(spread_all, 1e-9),
"saturated": spread_top < flat_tolerance,
"note": ("index is saturated over the dense canopy — "
"switch to a red-edge index"
if spread_top < flat_tolerance else "index is discriminating")}
Figure 2 — The crossover, measured rather than assumed. It moves with crop and season.
Reporting more than one index
The cheapest resolution to most of this is to stop choosing. Computing three indices from a band stack costs seconds, storage is trivial next to the imagery, and a deliverable carrying all three with a note on which discriminates best at this stage is more useful than one carrying a single index chosen by convention.
Two conventions make that practical. Compute the discrimination ratio for each index on every flight and put it in the report, so the reader can see which number to weight. And keep the index definitions versioned in one place, so “NDRE” means the same band pair and the same denominator floor across a whole programme.
The one thing to avoid is reporting several indices without guidance. An agronomist handed five maps with no indication of which is informative will use the one they recognise, which returns the problem to where it started.
When to escalate
- No index discriminates. The plots may genuinely not differ, or the flight’s resolution may be too coarse for the effect. Check the discrimination ratio against a plot pair known to differ before concluding anything about the crop.
- The agronomist requires a specific index. Provide it, and provide the saturation check alongside. A number with its limitation stated is more useful than an argument about methods.
- Published thresholds are being applied. Band centres differ between sensors by tens of nanometres, which shifts an index by several hundredths. A threshold from a study on another rig is a starting point, not a criterion.