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.

Index response against canopy density for three index choices Three response curves against leaf area index from zero to seven. NDVI rises steeply from zero to about a leaf area index of three and then flattens almost completely, saturating near zero point eight-five. NDRE rises more gradually and continues rising through a leaf area index of six, still responding where NDVI has flattened. GNDVI sits between them, flattening at around a leaf area index of four. A shaded region beyond leaf area index three marks where NDVI no longer discriminates, and a note records that this is where most management decisions are made. NDVI no longer discriminates here 0 1 2 3 5 7 leaf area index index value NDVI — flat past LAI 3 NDRE — still rising GNDVI Most management decisions are made in the shaded region, which is why NDVI alone disappoints.

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.

Where each index saturates, and what that costs Three rows. NDVI saturates in dense canopy, where further biomass increase produces almost no change in value, which makes it poor for distinguishing a good crop from an excellent one late in the season. NDRE uses the red edge, which continues responding well past NDVI's ceiling, and is therefore the right choice for late-season vigour work and for nitrogen status. GNDVI uses green rather than red and saturates later than NDVI while remaining more sensitive to chlorophyll concentration, which suits mid-season assessment where NDVI has begun to flatten and a red edge band is unavailable. NDVI saturates in dense canopy — poor at telling a good crop from an excellent one NDRE red edge keeps responding past NDVI's ceiling — late season and nitrogen status GNDVI saturates later than NDVI, tracks chlorophyll — mid season without a red edge band The index follows the growth stage, which is why a season needs more than one.

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")}
Discrimination ratio by index across a growing season Three series of discrimination ratio across five growth stages from emergence to grain fill. NDVI peaks at tillering with a ratio of about eleven and falls to about two by grain fill. NDRE starts low at emergence with a ratio of about three and rises steadily to about nine at grain fill. GNDVI tracks between them throughout. A crossover is marked at stem extension, where NDRE overtakes NDVI, and a note states that the right index changes during the season rather than being fixed for a programme. emergence tillering stem ext. flowering grain fill discrimination NDVI NDRE GNDVI crossover The right index changes during the season; fixing one for a programme guarantees a poor half.

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.

Computing Vegetation Indices with Rasterio