Calibrated vs Uncalibrated Multispectral Workflows

There is a strong orthodoxy that multispectral data must be radiometrically calibrated, and a strong practice of not doing it. Both positions are defensible, and the disagreement usually comes from arguing about the method rather than about the question being answered.

Calibration converts digital numbers into surface reflectance, a physical quantity independent of the illumination. That matters enormously for some uses and not at all for others, and the difference is precise enough to decide in advance. This page sets out where each workflow is right, what each costs, and how to test which one a given programme needs — rather than adopting either by default. It draws on the machinery in radiometric calibration in Python.

Audience and prerequisites. Python 3.10+ and a multispectral dataset. The decision this page describes is made before a programme starts; revisiting it a season in is expensive, because uncalibrated data cannot be retrospectively calibrated.

Prerequisites

Item Needed for Approximate cost Notes
Reflectance panel Calibrated One-off, moderate Needs a certificate and re-measurement over time
Downwelling light sensor Calibrated, variable light Usually built into the rig Not optional under broken cloud
Panel capture discipline Calibrated Two minutes per flight The most common point of failure
Processing time Calibrated Tens of minutes per flight Mostly automatable
Permanent ground target Both One-off, trivial A concrete pad; the best diagnostic available
Consistent flight timing Both Scheduling constraint Reduces variation more cheaply than calibration

Conceptual architecture

The question “is calibration necessary” resolves into one sharper question: does the comparison being made cross a boundary that calibration removes?

Comparisons within one flight — which plot is best today, where in the field is the poorest area — are largely unaffected. Whatever the illumination was, it was much the same across the flight, and the ratio form of an index cancels most of what remains. Calibration changes the absolute values and rarely changes the ordering.

Comparisons between flights cross exactly the boundary calibration exists to remove. Two flights differ in sun angle, cloud, atmospheric clarity and camera settings, and an uncalibrated index picks all of that up. This is where the orthodoxy is right and where uncalibrated programmes produce time series that mostly track the weather.

Comparisons against external values — a published threshold, another operator’s data — need calibration and usually more besides, because band centres differ between sensors.

Where calibration changes the answer Three comparison types against whether calibration matters. Comparing plots within one flight is largely unaffected, because the illumination was common and the ratio form cancels most of the rest; the ordering of plots is almost always preserved. Comparing the same plot between flights is strongly affected, because the two flights differ in sun angle, cloud and camera settings, and an uncalibrated series tracks the weather. Comparing against an external published value requires calibration and matched band centres, and is rarely achievable even then. within one flight illumination is common the ratio cancels the rest calibration optional ordering almost always preserved between flights sun angle, cloud, settings differ all of it enters the index calibration required otherwise the series tracks the weather against external values band centres differ too so does the processing chain necessary, rarely sufficient treat a threshold as a starting point The decision is about the comparison, not about the data. Which is why it can be made before the first flight rather than argued about afterwards.

Figure 1 — Three questions, three answers. Most disagreements about calibration are disagreements about which column applies.

Step 1: Test whether it changes the ordering

For within-flight work the practical question is whether calibration changes which plots rank where. It is directly testable on any dataset that has both products.

import numpy as np
from scipy.stats import spearmanr


def ranking_agreement(uncalibrated: dict[str, float],
                      calibrated: dict[str, float]) -> dict:
    """Does calibration change the ordering of plots within a flight?

    Spearman correlation rather than Pearson, because the question is about
    rank rather than value: a calibration that shifts every plot by the same
    factor changes every number and no decision.
    """
    common = sorted(set(uncalibrated) & set(calibrated))
    if len(common) < 8:
        return {"note": "too few common plots to judge"}

    u = np.array([uncalibrated[k] for k in common])
    c = np.array([calibrated[k] for k in common])
    rho, p = spearmanr(u, c)

    top_u = set(np.array(common)[np.argsort(-u)][:max(len(common) // 5, 1)])
    top_c = set(np.array(common)[np.argsort(-c)][:max(len(common) // 5, 1)])
    overlap = len(top_u & top_c) / max(len(top_c), 1)

    return {"plots": len(common), "rank_correlation": float(rho),
            "top_quintile_agreement": float(overlap),
            "calibration_changes_decisions": bool(rho < 0.95 or overlap < 0.8)}

On stable-light flights this routinely returns a rank correlation above 0.98 and a top-quintile agreement of 1.0, which is a concrete answer to “does it matter here” — and one worth having before committing a programme either way.

Step 2: Test whether it stabilises a series

For between-flight work the test is different: does calibration reduce the variation of something that should not vary.

import numpy as np


def series_stability(target_values: dict[str, dict[str, float]]) -> dict:
    """Variation of a permanent target's index across flights, both ways.

    `target_values` maps workflow name to flight date to the index of a
    surface that does not change — a concrete pad, a roof. Whatever variation
    remains is the workflow's own noise.
    """
    out = {}
    for workflow, by_flight in target_values.items():
        values = np.array(list(by_flight.values()), dtype=float)
        out[workflow] = {"flights": int(values.size),
                         "spread": float(values.max() - values.min()),
                         "sd": float(np.std(values, ddof=1)) if values.size > 1 else 0.0}

    if {"calibrated", "uncalibrated"} <= set(out):
        reduction = 1 - out["calibrated"]["sd"] / max(out["uncalibrated"]["sd"], 1e-9)
        out["improvement"] = float(reduction)
        out["verdict"] = ("calibration materially stabilises the series"
                          if reduction > 0.4 else
                          "calibration adds little here — the variation is elsewhere")
    return out

A reduction of forty percent or more says the calibration is doing its job. A reduction near zero says something else dominates — most often that the flights were flown at very different times of day, which no radiometric correction addresses.

A permanent target's index across eight flights, both workflows Two series of a concrete pad's index across eight monthly flights. The uncalibrated series varies between zero point zero four and zero point nineteen, tracking the weather on each flight day. The calibrated series stays between zero point ten and zero point twelve throughout. A note records that the target did not change, so all of the uncalibrated variation is workflow noise, and that a crop change of interest is typically zero point zero five. 123 456 78 monthly flights uncalibrated — tracks the weather calibrated — flat a crop change of interest is about this big The target did not change, so every wiggle in the red series is workflow noise.

Figure 2 — The test that settles the argument, using a surface that cannot have changed.

Step 3: Count the real cost of each workflow

The cost argument is usually made loosely — “calibration is expensive” — and it is worth being specific, because most of the cost is not where people assume.

The hardware is a one-off: a panel with a certificate, and a downwelling sensor that is built into most current rigs anyway. Amortised over a season it is negligible next to the flying.

The processing is largely automatable. Once the chain in radiometric calibration in Python exists, a flight costs tens of minutes of compute and no attention.

The field discipline is where the real cost sits, and it is measured in reliability rather than time. Two minutes per flight to photograph the panel is nothing; remembering to do it, in the right light, without a shadow, on every flight for a season, is a process problem. A programme that captures the panel on eighty percent of flights has a calibrated archive with holes in it, which is harder to use than either a fully calibrated or a fully uncalibrated one.

The hidden cost of not calibrating is the one that is never counted: the analyses that cannot be done, and the ones that are done anyway and quietly mislead. A series that tracks the weather is not obviously wrong; it looks like a crop responding to something.

def workflow_cost_model(flights_per_season: int, *, panel_capture_rate: float,
                        processing_minutes: int = 25) -> dict:
    """A concrete comparison of what each workflow costs over a season."""
    usable = int(flights_per_season * panel_capture_rate)
    return {
        "flights": flights_per_season,
        "calibrated_flights": usable,
        "field_minutes": flights_per_season * 2,
        "processing_hours": round(flights_per_season * processing_minutes / 60, 1),
        "archive_gaps": flights_per_season - usable,
        "warning": ("a partially calibrated archive is harder to use than either"
                    if 0 < flights_per_season - usable else None),
    }

Step 4: The hybrid that most programmes actually want

The two workflows are not exclusive, and the arrangement that serves most programmes takes the useful parts of each.

Capture for calibration on every flight, whether or not the current analysis needs it. The cost is two minutes and the benefit is an archive that can answer questions nobody has asked yet.

Process uncalibrated for rapid turnaround. Scouting products that need to be with an agronomist the same afternoon do not need the calibrated chain, and the ranking they support is unaffected by skipping it.

Process calibrated for the record. The time series, the trial analysis and anything that will be looked at next season run through the full chain, on whatever schedule suits.

Keep the raw frames. They are the only thing that makes a later reprocessing possible, and a season of them is a few terabytes.

def choose_pipeline(purpose: str, *, turnaround_hours: float) -> dict:
    """Route a flight to the appropriate processing depth."""
    fast = turnaround_hours < 6
    if purpose in {"scouting", "ranking"} and fast:
        return {"pipeline": "uncalibrated", "note": "ranking only; do not compare "
                                                    "across flights",
                "still_capture_panel": True}
    if purpose in {"monitoring", "trial", "archive"}:
        return {"pipeline": "calibrated", "note": "full chain; comparable across flights",
                "still_capture_panel": True}
    return {"pipeline": "calibrated", "note": "default to the recoverable option",
            "still_capture_panel": True}

still_capture_panel is True in every branch, which is the point. The processing decision can be revisited at any time; the capture decision cannot.

Step 5: Documenting which workflow produced a number

A programme running both needs every delivered number to say which chain produced it, or the two will eventually be compared to each other.

Three fields are enough: the workflow name, whether a panel and an irradiance record were used, and a one-line statement of what the number may be compared with. That last field is unusual and disproportionately useful — it puts the limitation next to the value rather than in a methods appendix.

def provenance_note(workflow: str, *, panel: bool, irradiance: bool) -> dict:
    """What a delivered index value may and may not be compared with."""
    if workflow == "calibrated" and panel:
        comparable = ("other calibrated flights of this programme; external values "
                      "only where band centres match")
    elif workflow == "calibrated" and not panel:
        comparable = "other flights of this programme, with reduced confidence"
    else:
        comparable = "other plots in this same flight only"
    return {"workflow": workflow, "panel_used": panel,
            "irradiance_used": irradiance, "comparable_with": comparable}

A client who receives that line with their numbers will not compare a scouting product against last month’s monitoring product, which is the failure this whole page exists to prevent. Everything else here is about choosing a workflow; this is about making the choice visible in the deliverable, which is what actually stops the misuse.

What calibration does not fix

It is worth naming the limits, because a calibrated programme that still sees unexplained variation often concludes the calibration failed when it did not.

Sun angle changes the shadow fraction within a canopy. At 9 am a crop row casts more shadow than at noon, and shadowed canopy has a genuinely different spectrum. Calibration converts digital numbers to reflectance and does nothing about the proportion of the scene that is shaded. Flying within a consistent window around solar noon addresses this; nothing in processing does.

Phenology moves faster than a monthly flight. A crop can change materially in a week, so a series flown monthly is undersampled for some questions regardless of how well each flight is calibrated.

Soil background changes with moisture. A field photographed the day after rain has a darker soil background, which shifts an index over sparse canopy even when the canopy itself is unchanged. Masking helps; calibration does not.

Band centres are not standardised. Two calibrated sensors measuring the same field can differ by several hundredths in an index, because their bands sit at different wavelengths. This is the reason external thresholds transfer poorly even between well-calibrated programmes.

Recognising these as separate problems is what keeps a calibration effort proportionate. A programme that has solved the calibration and still sees noise is usually looking at one of the four above, and the remedy is a scheduling or sampling change rather than more radiometry.

Parameter deep-dive

Consideration Uncalibrated Calibrated
Within-flight ranking Adequate Adequate
Between-flight comparison Unreliable The reason it exists
External thresholds Not possible Possible, with caveats
Panel discipline required None Two minutes per flight, reliably
Processing per flight Minutes Tens of minutes
Failure mode Silent: values track light Loud: a bad panel is detectable
Recoverable later Never Re-processable from raw frames
Hardware needed None Panel, ideally a DLS

The recoverability row is the one that should decide most programmes. A calibrated pipeline can always be run uncalibrated; an uncalibrated archive can never be calibrated afterwards, because the panel images were never taken. That asymmetry makes capturing the panel worthwhile even for a programme that currently only needs within-flight ranking.

Verification and output inspection

Whichever workflow is chosen, the permanent target is the check that keeps it honest.

def workflow_health(target_index_by_flight: dict[str, float],
                    *, calibrated: bool, tolerance: float = 0.03) -> dict:
    """Continuous check on whichever workflow is in use."""
    import numpy as np
    values = np.array(list(target_index_by_flight.values()), dtype=float)
    spread = float(values.max() - values.min()) if values.size > 1 else 0.0

    if calibrated:
        ok = spread <= tolerance
        note = ("calibration is consistent across flights" if ok else
                f"target moved by {spread:.3f} — check panel handling and the DLS")
    else:
        ok = True
        note = (f"target varies by {spread:.3f} across flights — expected without "
                "calibration; do not compare index values between flights")
    return {"spread": spread, "ok": ok, "note": note}

Reporting the uncalibrated case as “expected” rather than as a failure is deliberate. The variation is not a fault in an uncalibrated workflow; it is the property that defines its limits, and stating it is what stops somebody using the data for a comparison it cannot support.

What calibration buys, stated as questions it makes answerable Three rows. Uncalibrated data answers which part of this field is most vigorous today, and ranks areas within a single flight, which covers a large share of practical scouting use. Calibrated data additionally answers whether this field is more vigorous than it was a fortnight ago, and whether two fields flown on different days are comparable. Cross-calibrated data additionally answers whether this sensor's values can be placed alongside another operator's or a satellite product's, which is the requirement that most often arrives late and cannot be satisfied retrospectively. uncalibrated which part of this field is best today — ranking within one flight calibrated is this field better than a fortnight ago; are two days' flights comparable cross-calibrated can these values sit beside another operator's or a satellite's The third requirement usually arrives after the flights, and cannot be met retrospectively.

Figure 3 — Calibration is bought per question, not per flight.

Troubleshooting

Calibrated values still differ between flights. Check the panel first — shading, handling, a single value applied across bands — then the downwelling sensor’s attitude correction, then whether the flights were at very different sun angles.

Uncalibrated rankings disagree between two flights. Expected if anything about the illumination differed within either flight. Ranking is reliable within a flight, not across.

Calibration made the numbers worse. Usually a bad panel measurement, which applies a wrong constant to everything. The panel step fails loudly if checked and silently if not.

A client compares values against a published threshold. Band centres differ between sensors by tens of nanometres. Provide the value and the caveat together.

The programme has uncalibrated history and now needs calibration. The history cannot be recovered. Start the calibrated series and keep both for an overlap period so the step is documented.

Panel capture is being skipped under time pressure. Make it part of the pre-flight checklist rather than a processing requirement. Two minutes on the ground is cheaper than an uncomparable flight, and a crew that treats it as part of take-off rather than as an extra step does not skip it.

Two operators produce different values from the same flight. Compare the panel measurement first: the interior region each of them used is the most likely difference, and it scales every value in the survey.

Multispectral, Thermal & Index Mapping Pipelines