Fixing Band Misregistration Artifacts in Index Rasters

The NDVI raster has a bright line along the north side of every hedge and a dark line along the south side. Around each bare patch in the field there is a thin rim of implausibly high values. The agronomist asks what the bright lines mean, and the honest answer is that they do not mean anything: they are an arithmetic consequence of two bands that do not quite overlap.

The artefact is easy to recognise once seen, easy to measure, and impossible to remove from the index raster itself. The fix is always upstream, in the alignment covered by band alignment and stacking for multispectral sets.

The arithmetic that produces the fringe

Consider a boundary between crop and soil, and suppose the near-infrared band is shifted two pixels north relative to red. At the boundary there is a strip two pixels wide where the near-infrared value still belongs to the crop while the red value already belongs to the soil.

For a normalised difference index, that strip combines the highest near-infrared with the highest red-contrast — crop NIR over soil red — and the index there exceeds anything real in the scene. Two pixels away on the other side, the combination is reversed and the index drops below anything real.

Two properties follow, and together they identify the artefact conclusively. The fringe is always paired: a bright line on one side of an edge and a dark line on the other, with the pairing oriented along the misregistration direction. And it is oriented consistently across the whole raster, because the offset between two sensors is the same everywhere in a frame.

How a two-pixel band offset creates a paired fringe at an edge A cross-section through a crop-to-soil boundary. The red band steps at the boundary and the near-infrared band steps two pixels later. Between the two step positions, the index combines crop near-infrared with soil red and rises above every real value in the scene. On the far side of the boundary a second, narrower region combines soil near-infrared with crop red and falls below every real value. A plan view beside it shows the resulting bright and dark lines running along a hedge, oriented identically everywhere in the raster. red NIR, shifted 2 px above every real value below every real value index profile hedge line bare patch paired, and oriented the same way everywhere Paired, consistently oriented, and at every edge in the raster. Those three properties identify the artefact without needing the source frames.

Figure 1 — The arithmetic, and the signature it leaves in plan view.

Minimal reproducible solution

The offset can be measured from the index raster itself, which is useful when the source frames are no longer to hand: the direction in which index extremes concentrate at edges is the misregistration direction.

import numpy as np
from scipy import ndimage


def estimate_misregistration(index: np.ndarray, *, edge_percentile: float = 92.0,
                             search_px: int = 4) -> dict:
    """Recover the band offset direction and magnitude from the index raster.

    Extreme index values cluster on one side of every edge. Correlating the
    extremes against a directional gradient recovers the offset without the
    source bands.
    """
    a = np.nan_to_num(index.astype(np.float32))
    gy, gx = np.gradient(a)
    grad = np.hypot(gx, gy)
    edges = grad > np.nanpercentile(grad, edge_percentile)
    if edges.sum() < 500:
        return {"note": "not enough edge pixels to judge"}

    extreme_hi = a > np.nanpercentile(a[np.isfinite(a)], 99.5)
    best = None
    for dy in range(-search_px, search_px + 1):
        for dx in range(-search_px, search_px + 1):
            if dy == 0 and dx == 0:
                continue
            shifted = ndimage.shift(edges.astype(np.float32), (dy, dx), order=0)
            overlap = float(np.mean(shifted[extreme_hi]))
            if best is None or overlap > best[0]:
                best = (overlap, dy, dx)

    overlap, dy, dx = best
    return {"offset_px": (dy, dx),
            "magnitude_px": float(np.hypot(dy, dx)),
            "confidence": overlap,
            "note": "offset direction recovered from the index raster alone"}

The result identifies the problem and does not fix it. Which brings us to the part that matters.

Why the index raster cannot be repaired

It is tempting to filter the fringes out — a median filter, or masking the extreme percentiles, or eroding the plot boundaries away from edges. None of these is a fix, for a reason worth being precise about.

The fringe is not noise added to a correct value. It is what you get when two different places on the ground are divided by each other. The information needed to produce the correct index at those pixels — the near-infrared value at the same ground position as the red value — is not present in the raster at all; it is in the source frames, two pixels away.

Filtering therefore replaces a wrong value with a smoothed wrong value. It looks better, the extremes disappear, and the plot statistics are still biased because the bias was never in the extremes alone. The full-width effect of a two-pixel offset extends across every transition zone in the scene, not merely the pixels that clipped.

import numpy as np


def fringe_contribution(index: np.ndarray, edges: np.ndarray,
                        offset_px: float) -> dict:
    """How much of a plot's mean comes from the affected transition zone.

    The affected width is the misregistration magnitude, so on a plot with
    many internal edges — crop rows, tramlines — the affected fraction can
    be large even for a sub-pixel offset.
    """
    from scipy import ndimage
    affected = ndimage.binary_dilation(edges, iterations=max(int(round(offset_px)), 1))
    valid = np.isfinite(index)
    frac = float(np.count_nonzero(affected & valid) / max(np.count_nonzero(valid), 1))

    clean_mean = float(np.nanmean(index[valid & ~affected]))
    all_mean = float(np.nanmean(index[valid]))
    return {"affected_fraction": frac,
            "mean_all": all_mean, "mean_clean": clean_mean,
            "bias": all_mean - clean_mean}

Running that on a real plot is often the argument that settles the discussion: on a field with tramlines every twenty-four metres, a one-pixel offset at 5 cm resolution can affect several percent of the area and shift the plot mean by more than the seasonal change being measured.

Isolating a misregistration artefact from the alternatives A four-stage diagnosis. Stage one checks whether the extreme values follow edges in the scene; if they do not, the cause is not registration. Stage two checks whether they appear on both sides of each edge with opposite sign, which is the registration signature and distinguishes it from a genuine edge effect. Stage three checks whether the offset direction is consistent across the whole scene, which separates a band offset from a terrain-induced parallax. Stage four checks whether it varies with height, which indicates parallax and needs a surface model rather than a homography. 1. follows edges? if not, the cause is not registration 2. opposite signs? both sides of the edge — the registration signature 3. consistent direction? separates a band offset from terrain parallax 4. varies with height? parallax — needs a surface model, not a homography Stages 3 and 4 are what stop a parallax problem being treated as a calibration one.

Figure 3 — Four questions, and the last two matter most on undulating ground.

Edge-case matrix

Situation Appearance Handling
Uniform offset across the raster Paired fringes, consistent orientation Re-align the bands; re-derive the index
Offset varies with terrain height Fringes stronger over relief Per-frame alignment, not a fixed transform
Sub-pixel offset No visible fringe, biased statistics Measure the bias; decide if it matters
Offset only in one band pair Fringes in one index and not another Band-specific alignment problem
Fringes at the survey edge only Frames with little overlap Expected; mask the survey margin
Apparent fringes with no offset Real sharp boundaries Check the pairing; real edges are not paired
Offset changed mid-flight Fringes appear in part of the mosaic A sensor moved; investigate the hardware
Index masked before inspection Artefact hidden, bias retained Inspect the unmasked index

The last row is the trap. A pipeline that masks extreme index values before anyone looks at the raster hides the diagnostic while keeping the error, which is the worst of both.

Verification snippet

import numpy as np
from scipy import ndimage


def fringe_test(index: np.ndarray, *, edge_percentile: float = 92.0) -> dict:
    """Do extreme index values cluster on one side of edges?

    A real feature is not systematically one-sided. A misregistration fringe
    is, and the asymmetry between the two sides of an edge is a direct,
    single-number test.
    """
    a = np.nan_to_num(index.astype(np.float32))
    gy, gx = np.gradient(a)
    grad = np.hypot(gx, gy)
    edges = grad > np.nanpercentile(grad, edge_percentile)

    hi = a > np.nanpercentile(a[np.isfinite(a)], 99.0)
    lo = a < np.nanpercentile(a[np.isfinite(a)], 1.0)

    near = ndimage.binary_dilation(edges, iterations=2)
    hi_near = float(np.count_nonzero(hi & near) / max(np.count_nonzero(hi), 1))
    lo_near = float(np.count_nonzero(lo & near) / max(np.count_nonzero(lo), 1))

    return {"extreme_high_near_edges": hi_near,
            "extreme_low_near_edges": lo_near,
            "misregistration_likely": hi_near > 0.6 and lo_near > 0.6,
            "note": ("extremes concentrate at edges — band misregistration"
                     if hi_near > 0.6 and lo_near > 0.6
                     else "extremes are distributed; edges are probably real")}

Running this as a gate on every index raster, before any masking, catches the problem at the point where re-aligning is still a rerun rather than a re-flight.

Plot mean bias against misregistration for two field structures Two curves of plot mean index bias against band offset from zero to three pixels. For an open field with few internal edges the bias rises slowly, reaching about zero point zero one at three pixels. For a field with tramlines every twenty-four metres the bias rises much faster, passing zero point zero three at one pixel and reaching zero point zero nine at three. A horizontal line marks a typical seasonal change of zero point zero four, which the tramlined field's bias exceeds at just over one pixel of offset. 0 1 px 2 px 3 px band misregistration plot mean bias typical seasonal change open field tramlined field On a structured field, one pixel of offset is already larger than the effect being measured.

Figure 2 — Why “it is only a pixel” depends entirely on how many edges the field has.

When to escalate

  • The source frames are gone. The index cannot be corrected. Report the measured offset and its estimated bias alongside the product, and re-process from frames next time.
  • Alignment is good and fringes persist. Check whether the index was computed from an orthomosaic assembled from differently aligned frames — a per-frame alignment applied after mosaicking does not help.
  • The offset changed partway through the flight. A sensor has moved physically. That is a maintenance finding, and every subsequent survey with that rig is affected until it is fixed.

Band Alignment and Stacking for Multispectral Sets