Skip to content
Multispectral & Scientific Imaging

Processing a multispectral image stack well is mostly about order and discipline: apply dark subtraction, flat-field correction and reflectance normalisation first, register the bands to sub-pixel accuracy, and only then run statistical separation such as PCA or ICA. Do those steps in a scripted, logged pipeline and your results stay consistent and reproducible across an entire project; do them ad hoc and every folio becomes an unrepeatable one-off. This guide gives the working checklist I apply to every batch.

What is the canonical processing order?

The sequence matters because each step assumes the previous one is done. Skipping or reordering bakes errors into everything downstream:

  1. Dark-frame subtraction — remove sensor bias and thermal noise.
  2. Flat-field correction — divide out lamp hotspots and lens vignetting.
  3. Reflectance normalisation — scale each band against the spectral target.
  4. Registration — align all bands to a reference to sub-pixel accuracy.
  5. Standardisation — zero-mean, unit-variance per band for PCA/ICA input.
  6. Separation — PCA, ICA or band ratios to surface the latent content.
  7. Visualisation — false-colour mapping and contrast for human reading.

How do I script it so every folio gets the same treatment?

Put parameters in a config and loop. The point is that no folio receives a hand-tweaked exception:

python
import json, numpy as np
from skimage import io
from skimage.registration import phase_cross_correlation
from scipy.ndimage import shift

cfg = json.load(open("pipeline.json"))     # wavelengths, ref band, etc.

def process(folio_dir):
    dark = io.imread(f"{folio_dir}/dark.tif").astype(np.float32)
    flat = io.imread(f"{folio_dir}/flat.tif").astype(np.float32) - dark
    ref = None
    cube = []
    for nm in cfg["wavelengths"]:
        raw = io.imread(f"{folio_dir}/b_{nm}.tif").astype(np.float32) - dark
        corr = raw / np.clip(flat / flat.mean(), 1e-3, None)   # flat-field
        if nm == cfg["ref"]:
            ref = corr
        cube.append(corr)
    # sub-pixel register every band to the reference band
    reg = []
    for band in cube:
        dy_dx, _, _ = phase_cross_correlation(ref, band, upsample_factor=20)
        reg.append(shift(band, dy_dx, order=1))
    return np.stack(reg, -1)

Logging the config hash and an output checksum per folio turns "I think I did the same thing" into proof.

Why does 16-bit matter so much here?

Faint undertext or a corroded pigment may modulate brightness by only a few levels out of 65,536. In 8-bit there are just 256 levels total, so quantisation can erase the signal before separation runs. Keep everything in 16-bit integer or 32-bit float until the final display export; downsample to 8-bit only for derivatives meant for human viewing.

Which separation method should I reach for?

MethodStrengthWatch out for
PCAFast, deterministic, great first lookMixes overlapping signals into one component
ICASeparates statistically independent layersComponent order/sign is random; needs review
Band ratioTransparent, easy to justifyOnly works when one pair has the contrast
MNFOrders by signal-to-noise, good on noisy stacksMore parameters to tune

Start with PCA to scout, escalate to ICA or MNF when a specific layer refuses to separate.

How do I keep quality consistent across a long project?

Define a small set of acceptance checks and run them on every output: registration error below a threshold (e.g. under 0.5 px), no clipped highlights in the corrected bands, and the spectral target reading within tolerance. Reject and reshoot anything that fails rather than rescuing it in software. Consistency comes from rejecting bad capture, not from heroic post-processing.

How do I make the stack defensible later?

Every processed output should carry its paradata — wavelengths, exposures, target readings, registration method, software versions and parameters. Store the calibrated 16-bit stack as the archival master and treat false-colour images as derivatives that point back to it. Two years on, a reviewer should be able to rerun your config and obtain the same image to the pixel.

What are the most common processing mistakes?

  • Registering before flat-fielding, so hotspots smear across bands.
  • Stretching contrast per-folio, destroying batch comparability.
  • Sharpening before separation, which injects edge artefacts into PCA.
  • Trusting PC1 and never inspecting later components.
  • Discarding raw bands once a nice JPEG exists.

Key Takeaways

  • Fixed order: dark, flat-field, reflectance, register, standardise, separate, visualise.
  • Process in 16-bit/32-bit float; reserve 8-bit for derivatives only.
  • Script the pipeline from a config so every folio gets identical treatment.
  • PCA scouts; ICA, MNF and band ratios resolve stubborn layers.
  • Enforce acceptance checks and reshoot failures rather than patching them.
  • Archive the calibrated stack plus full paradata for reproducibility.

Frequently Asked Questions

What is the correct order of operations for a stack?

Dark subtraction, flat-field correction, reflectance normalisation against a target, sub-pixel registration, then statistical separation. Doing registration before flat-fielding leaves illumination artefacts baked into the result.

Should I work in 8-bit or 16-bit?

Always process in 16-bit (or 32-bit float). Faint undertext occupies a tiny fraction of the tonal range, and 8-bit quantisation destroys it before you ever apply PCA.

How do I keep results consistent across hundreds of folios?

Script the pipeline so every folio receives identical operations, store parameters in a config file, and log a checksum and processing record per output. Manual per-image tweaking is the main source of inconsistency.

Do I normalise each band independently?

Normalise against a reflectance target captured in the same session so bands are radiometrically comparable, then standardise (zero mean, unit variance) only as a pre-step for PCA or ICA.

What metadata must accompany a processed stack?

Wavelengths, exposures, calibration target readings, registration method, software versions and every processing parameter — the paradata that lets another person reproduce your output exactly.

How do I avoid creating artefacts that look like text?

Validate against the raw bands, avoid extreme contrast stretches, and never sharpen before separation. If a feature appears only after aggressive processing and not in any single band, treat it as suspect.