Fixing Missing or Mismatched Band Metadata
A reflectance stack arrives from a subcontractor with five bands and no descriptions. The pipeline needs red and near-infrared. Band 3 and band 5 look about right, and an NDVI computed from them produces a plausible map of the field.
It may well be an NDVI. It may also be a normalised difference between red edge and near-infrared, which is NDRE and has different values, a different saturation point and different agronomic meaning. Nothing in the raster distinguishes them, and the map looks the same either way.
This page covers recovering band identity from the data when the metadata is absent, detecting a mismatch when it is present but wrong, and writing it back so the question is asked once. It is part of the diagnostic sequence in troubleshooting multispectral and thermal failures.
Why band identity is not obvious from the data
The temptation is to identify bands by their statistics — near-infrared is the brightest over vegetation, red the darkest — and that works until it does not.
Over a field of healthy crop the ordering is reliable: near-infrared is far brighter than red edge, which is brighter than green, which is brighter than red and blue. Over bare soil the ordering compresses and can invert between adjacent bands. Over a site that is mostly hardstanding, all five bands are similar and the ordering is close to arbitrary.
Correlation between bands is more robust. Adjacent bands are strongly correlated because the underlying reflectance spectrum is smooth, so the correlation matrix has a characteristic band structure: each band correlates most with its neighbours in wavelength. That structure survives a wide range of scenes and recovers the ordering even when it cannot pin an absolute identity.
Figure 1 — Structure that survives most scenes, and recovers the ordering when the labels are gone.
Minimal reproducible solution
import numpy as np
import rasterio
CANONICAL = [("blue", 475.0), ("green", 560.0), ("red", 668.0),
("red_edge", 717.0), ("nir", 840.0)]
def infer_band_order(stack_path: str, *, sample: int = 200_000) -> dict:
"""Recover the wavelength ordering of an unlabelled stack.
The correlation matrix of a smooth spectrum decays monotonically from the
diagonal, so the permutation that makes it do so is the wavelength order.
With five bands there are only 120 permutations, so an exhaustive search
is cheaper and more reliable than any heuristic.
"""
import itertools
with rasterio.open(stack_path) as src:
n = src.count
arr = src.read(masked=True).filled(np.nan).reshape(n, -1)
good = np.isfinite(arr).all(axis=0)
cols = np.flatnonzero(good)
if cols.size > sample:
cols = np.random.default_rng(0).choice(cols, size=sample, replace=False)
corr = np.corrcoef(arr[:, cols])
def monotonic_score(order):
c = corr[np.ix_(order, order)]
score = 0.0
for i in range(n):
for j in range(i + 2, n):
score += float(c[i, j - 1] - c[i, j]) # should decay with distance
return score
best = max(itertools.permutations(range(n)), key=monotonic_score)
# The ordering is recovered up to reversal; near-infrared is the brightest
# band over vegetation, which breaks the tie.
means = np.nanmean(arr, axis=1)
if means[best[0]] > means[best[-1]]:
best = tuple(reversed(best))
return {"order_indices": [int(i) + 1 for i in best],
"suggested_names": [name for name, _ in CANONICAL][:n],
"confidence": float(monotonic_score(best)),
"note": "ordering inferred from correlation structure; verify before use"}
The reversal tie-break uses the one statistic that is reliable across almost every scene: over any vegetated area, near-infrared is the brightest band by a wide margin. Over a site with no vegetation at all the inference should not be trusted, and the function’s note says so.
Detecting a mismatch when metadata is present
Wrong metadata is worse than absent metadata, because nothing prompts a check. The test is whether the declared identities are consistent with the data.
import numpy as np
import rasterio
def validate_band_labels(stack_path: str) -> dict:
"""Are the declared band names consistent with what the pixels show?"""
with rasterio.open(stack_path) as src:
names = [d or "" for d in src.descriptions]
arr = src.read(masked=True).filled(np.nan).reshape(src.count, -1)
means = {n: float(np.nanmean(arr[i])) for i, n in enumerate(names) if n}
problems = []
if "nir" in means and "red" in means and means["nir"] <= means["red"]:
problems.append("the band labelled nir is not brighter than the one labelled "
"red — the labels are probably wrong")
if "red_edge" in means and "nir" in means and means["red_edge"] > means["nir"]:
problems.append("red_edge is brighter than nir, which does not happen over "
"vegetation")
if "blue" in means and "green" in means and means["blue"] > means["green"] * 1.3:
problems.append("blue is much brighter than green — check the ordering")
return {"means": means, "problems": problems,
"verdict": "labels consistent with the data" if not problems else
"labels disagree with the pixel statistics"}
Figure 3 — Three failures no format validator will catch.
Edge-case matrix
| Situation | Handling |
|---|---|
| No descriptions at all | Infer the ordering, verify, then write them |
| Descriptions present but wrong | Validate against statistics; trust the data |
| Scene with no vegetation | Inference unreliable; obtain the sensor’s spec |
| Fewer than five bands | Inference still works; fewer permutations |
| Duplicate descriptions | A writer error; re-derive and re-label |
| Wavelength tags but no names | Sort by wavelength and name canonically |
| Bands from two sensors merged | Ordering is not monotonic; do not infer |
| A thermal band in the stack | Excluded from the correlation structure |
The merged-sensor row is the one where inference must be refused rather than attempted. A stack assembled from two rigs has no single smooth spectrum underlying it, so the correlation structure the method relies on is not there, and a confident wrong answer is the likely output.
Verification snippet
import numpy as np
def verify_with_known_surface(bands: dict[str, np.ndarray],
vegetation_mask: np.ndarray) -> dict:
"""Confirm band identity against the known spectrum of healthy vegetation.
A vegetated surface has a characteristic shape: low in blue and red,
moderate in green, rising steeply through red edge to a high plateau in
near-infrared. Any labelling that does not reproduce it is wrong.
"""
profile = {name: float(np.nanmedian(arr[vegetation_mask & np.isfinite(arr)]))
for name, arr in bands.items()}
expected_order = ["blue", "red", "green", "red_edge", "nir"]
present = [b for b in expected_order if b in profile]
values = [profile[b] for b in present]
ascending = all(values[i] <= values[i + 1] * 1.15 for i in range(len(values) - 1))
return {"profile": profile, "expected_order": present,
"matches_vegetation_spectrum": ascending,
"note": ("labels reproduce the vegetation spectrum" if ascending else
"the labelled bands do not form a vegetation spectrum")}
Checking against the vegetation spectrum is the strongest available verification because it uses physics rather than convention: the shape is a property of chlorophyll and leaf structure, not of any sensor’s naming.
Figure 2 — The shape every correct labelling reproduces.
Writing the metadata back
import rasterio
def relabel_stack(path: str, names: list[str], wavelengths: dict[str, float]) -> None:
"""Write band descriptions and wavelength tags into an existing stack in place."""
with rasterio.open(path, "r+") as dst:
if len(names) != dst.count:
raise ValueError(f"{len(names)} names for {dst.count} bands")
for i, name in enumerate(names, start=1):
dst.set_band_description(i, name)
dst.update_tags(i, WAVELENGTH_NM=f"{wavelengths[name]:.1f}",
UNITS="reflectance")
dst.update_tags(BAND_ORDER=",".join(names),
BAND_LABELS_SOURCE="inferred and verified against "
"the vegetation spectrum")
Recording how the labels were established matters as much as the labels themselves. A stack labelled from the sensor’s documentation and one labelled by inference deserve different levels of trust, and the tag says which this is.
When to escalate
- The inference and the declared labels disagree. Get the sensor’s specification before overriding either. The declared labels may be right and the scene unusual.
- The stack contains bands from more than one sensor. Do not infer; the method’s assumption does not hold. Split the stack and label each source separately.
- No vegetation anywhere in the scene. The verification cannot run. Use a known surface of any kind whose spectrum is documented, or obtain the specification.