Applying DLS Irradiance Correction to MicaSense Captures

One-sentence answer: read each frame’s downwelling irradiance, compensate it for the sensor’s tilt, smooth lightly, and scale the frame by the ratio of the panel-capture irradiance to the frame’s own — and refuse the flight when that ratio spans more than about a factor of five.

Context

A calibration panel anchors a flight to physical units at one instant. Under a stable clear sky that is enough. Under broken cloud it is not: the light can halve between two passes, and a panel captured at take-off says nothing about what the fourth transect was flown under. The visible result is a mosaic with brighter and darker bands that follow the flight lines, and an index map whose “stress” pattern is a record of the weather rather than the crop.

The downwelling light sensor exists to close that gap. It measures incident irradiance per frame, which turns the correction from a single number into a per-frame ratio. This guide implements the irradiance stage of radiometric calibration and reflectance conversion, following converting digital numbers to reflectance with calibration panels.

Reading a flight's irradiance trace An irradiance trace over a twenty-minute flight. Narrow sharp dips recur at regular intervals and are labelled as tilt artefacts at banked turns. A broader sustained depression in the middle of the flight is labelled real cloud. A smoothed line follows the broad feature while ignoring the narrow ones, and a note gives the acceptance range for the panel-to-frame ratio. high low flight time (20 minutes, ~600 captures) tilt at turns — artefact real cloud — correct for this smoothed series drives the correction Accept while panel_irradiance / frame_irradiance stays within 0.2–5.0; beyond that the sky's spectrum has changed too, and a scalar ratio cannot fix it.

Prerequisites

Beyond the parent topic’s stack: captures carrying per-frame irradiance and sensor orientation tags, a panel capture processed as in the panel guide, and solar position for the flight — computed from the capture time and location, not assumed.

Step-by-step

1. Read irradiance and orientation per frame.

Turning a noisy sensor trace into a per-frame correction Four steps: read the downwelling irradiance recorded with each frame, compensate it for the sensor's tilt using the aircraft pose and the solar vector, smooth the series with a short median filter to remove turn artefacts, and scale each frame by the ratio of panel irradiance to frame irradiance. Read irradiance per frame, per band Compensate tilt pose and solar vector Smooth median over 9 frames Scale panel ÷ frame irradiance The median filter is what separates a two-second dip at a banked turn from a two-minute dip under cloud.

2. Compensate for tilt using the sensor’s pose and the solar vector.

3. Smooth with a short median filter — long enough to remove turn artefacts, short enough to keep a genuine cloud edge.

4. Scale each frame by the panel-to-frame irradiance ratio.

5. Judge the flight on the range of that ratio.

PYTHON
import numpy as np
from scipy.ndimage import median_filter


def tilt_compensated(irradiance: float, pose: dict, solar_vec: np.ndarray,
                     min_cos: float = 0.35) -> float:
    """Remove the cosine-response effect of sensor tilt from a downwelling reading."""
    roll, pitch, yaw = (np.deg2rad(pose[k]) for k in ("roll", "pitch", "yaw"))
    # Sensor normal in the world frame, small-angle composition is adequate for ±30°.
    normal = np.array([
        np.sin(pitch) * np.cos(yaw) + np.sin(roll) * np.sin(yaw),
        np.sin(pitch) * np.sin(yaw) - np.sin(roll) * np.cos(yaw),
        np.cos(roll) * np.cos(pitch),
    ])
    normal /= np.linalg.norm(normal)
    cos_inc = float(np.dot(normal, solar_vec))
    if cos_inc < min_cos:
        return float("nan")          # too tilted to trust — interpolate from neighbours
    cos_flat = float(solar_vec[2])   # what a level sensor would have seen
    return irradiance * cos_flat / cos_inc


def smoothed_irradiance(values: np.ndarray, window: int = 9) -> np.ndarray:
    """Median filter: removes single-frame attitude spikes, keeps a real cloud edge."""
    filled = values.copy()
    nan = ~np.isfinite(filled)
    if nan.any():                    # linear fill before filtering so gaps do not spread
        idx = np.arange(filled.size)
        filled[nan] = np.interp(idx[nan], idx[~nan], filled[~nan])
    return median_filter(filled, size=window, mode="nearest")


def irradiance_scaled(reflectance: np.ndarray, frame_irr: float, panel_irr: float) -> np.ndarray:
    """Scale a panel-calibrated frame by the change in illumination since the panel capture."""
    assert panel_irr > 0 and frame_irr > 0, "irradiance readings must be positive"
    ratio = panel_irr / frame_irr
    assert 0.2 < ratio < 5.0, (
        f"irradiance ratio {ratio:.2f} — the light changed by more than a factor of five during "
        "this flight; a scalar correction cannot represent the accompanying spectral shift")
    return (reflectance * ratio).astype("float32")

Inline verification — look at the whole flight before trusting any single frame:

PYTHON
raw = np.array([f["irradiance_nir"] for f in frames])
comp = np.array([tilt_compensated(f["irradiance_nir"], f["pose"], solar_vec) for f in frames])
smooth = smoothed_irradiance(comp)

spread_raw = raw.max() / raw.min()
spread_smooth = smooth.max() / smooth.min()
print(f"irradiance spread: raw ×{spread_raw:.2f}, tilt-compensated and smoothed ×{spread_smooth:.2f}")

assert spread_smooth < 5.0, (
    f"illumination varied ×{spread_smooth:.1f} across the flight — split it at the cloud edge "
    "or refly; do not correct through it")
assert spread_smooth < spread_raw, (
    "tilt compensation did not reduce the spread — check the pose tags and the solar vector")
assert np.isfinite(smooth).all(), "gaps remain in the smoothed irradiance series"

Settings worth being deliberate about

Setting Default here Why it matters
Ratio acceptance band 0.2–5.0 Beyond a factor of five the sky’s spectrum has changed as well as its intensity, and a scalar correction leaves every band ratio wrong
min_cos 0.35 Below this incidence cosine the tilt compensation amplifies noise more than it removes bias; those frames are better interpolated
Median window 9 frames A few seconds at typical capture rates — long enough to remove turn artefacts, short enough to preserve a real cloud edge
Solar vector computed per flight From capture time and location. A hard-coded sun inverts the correction in the other hemisphere
Interpolation policy fill, do not drop Dropped frames leave holes in the mosaic; neighbours a second away are accurate well within the correction’s own precision
Flight duration ≤ 25 minutes Longer flights span more illumination change than a panel pair at each end can anchor

Under a clear, stable sky none of this changes anything measurable — the ratio stays within a few percent of one all flight. Its value is entirely in the flights you would otherwise have to discard, and in knowing which those are.

Gotchas and edge cases

  • Tilt artefacts and cloud look alike frame by frame and nothing alike in sequence. Turn dips are narrow, deep and periodic; cloud is broad and irregular. That is why the median filter is applied over the sequence rather than a threshold over individual readings.
The limit of what one number can fix A decision diagram on the irradiance ratio across a flight. Within a factor of five, a scalar correction is valid because the sky's spectral composition has not changed much. Beyond it, cloud light is measurably bluer than direct sun, so band ratios stay wrong however the intensity is scaled. Flight irradiance spread measured is the ratio within 0.2 to 5.0? yes Correct and continue the sky's spectral composition has not changed enough for a scalar ratio to distort band ratios no Split the flight, or refly beyond a factor of five the light has changed colour as well as intensity, and a single number cannot represent that — the index would still be wrong after correction
  • A wrong solar vector makes tilt compensation worse than none. Compute solar position from the capture timestamp and location; a hard-coded midday sun in the wrong hemisphere inverts the correction.

  • The sensor sees the sky, the camera sees the ground. Under a partly cloudy sky the two can disagree legitimately — a shadow crossing the field while the sensor is in sun. This is the limit of the method, and it is why cloud shadow detection still has a job to do afterwards, as in detecting cloud shadows in drone imagery.

  • A scalar ratio cannot fix a spectral shift. Overcast light is bluer than direct sun. Correcting total intensity across a sun-to-cloud transition leaves the band ratios wrong, which is precisely what an index is made of — hence the hard limit rather than a warning.

  • Do not smooth so hard that a real cloud edge disappears. A window of nine frames at typical capture rates is a few seconds; a window of a hundred spans a transect and would flatten a genuine event into a slow drift, spreading the error across the whole flight instead of isolating it.

  • Frames with no usable irradiance should be interpolated, not dropped. Dropping them leaves holes in the mosaic; interpolating from neighbours a second or two away is accurate to well within the correction’s own precision.


This guide is part of Radiometric Calibration & Reflectance Conversion — see there for the full chain from raw counts to comparable reflectance.