08. Hyperspectral and VNIR Sensor Convolution
08-hyperspectral-sensors.Rmd
library(ToolsRTM)Tutorial 07 covered sensor convolution broadly – multispectral
sensors like Sentinel-2, with 10-13 bands. This page is a deep-dive into
the other end of the spectral-resolution spectrum: hyperspectral and
custom VNIR instruments, from a few hundred narrow bands (PRISMA, EnMAP)
down to real research-grade multi-camera and pushbroom rigs, all through
the same get.spectral.convolution.*() family already
introduced.
row <- data.frame(
LAI = 3, hspot = 0.01, LIDFa = -0.35, LIDFb = -0.15, TypeLidf = 1,
tts = 30, tto = 0, psi = 0,
N = 1.5, Cab = 40, Car = 8, Anth = 2, Cbrown = 0, EWT = 0.009, LMA = 0.009, alpha = 40
)
rsoil <- 0.5 * dataSpec_PDB[, 11] + 0.5 * dataSpec_PDB[, 12]
sail <- foursail(inputLUT = row, rsoil = rsoil, LeafModel = "PROSPECT-D")
reflectance <- Compute_BRF(rdot = sail$rdot, rsot = sail$rsot, tts = row$tts, data.light = dataSpec_PDB)
df <- data.frame(wave = dataSpec_PDB[, 1], rfl = reflectance)1. Sentinel-2: the multispectral reference point
se2_bands <- get.spectral.convolution.srf(df = df, srf = ToolsRTM::srf.sentinel2a)
cat("Sentinel-2A:", nrow(se2_bands), "bands\n")
#> Sentinel-2A: 13 bands10-13 discrete bands (Tutorial 07) – everything below samples the same underlying spectrum far more densely.
2. PRISMA: a real hyperspectral satellite mission
prisma_bands <- get.spectral.convolution.srf(df = df, srf = ToolsRTM::srf.prisma)
cat("PRISMA:", nrow(prisma_bands), "bands\n")
#> PRISMA: 234 bands
plot(df$wave, df$rfl, type = "l", col = "grey70",
xlab = "Wavelength (nm)", ylab = "Reflectance", main = "Native vs. PRISMA vs. Sentinel-2A")
lines(prisma_bands$wl, prisma_bands$RFL, col = "#2E8B57", lwd = 1)
points(se2_bands$wl, se2_bands$RFL, col = "#B2182B", pch = 19, cex = 1.2)
legend("topright", c("Native 1nm", "PRISMA", "Sentinel-2A"),
col = c("grey70", "#2E8B57", "#B2182B"), lty = c(1, 1, NA), pch = c(NA, NA, 19))
PRISMA is dense enough to nearly retrace the native curve – a real
measured SRF (get.spectral.convolution.srf(), same function
as Sentinel-2A, Tutorial 07), not an approximation.
3. EnMAP: 242 channels, nominal characteristics
EnMAP has no measured SRF bundled in this package – only nominal
center/FWHM characteristics
(ToolsRTM::EnMap.characteristics), so it goes through
get.spectral.convolution.gaussian() instead (Tutorial 07’s
third convolution path):
enmap_bands <- get.spectral.convolution.gaussian(df, sensor.i = "EnMAP")
cat("EnMAP:", nrow(enmap_bands), "bands\n")
#> EnMAP: 242 bands
plot(df$wave, df$rfl, type = "l", col = "grey70",
xlab = "Wavelength (nm)", ylab = "Reflectance", main = "EnMAP (242 Gaussian-approximated channels)")
lines(enmap_bands$wl, enmap_bands$RFL, col = "#6A3D9A", lwd = 1)
4. A real 15-band, 3-camera VNIR rig
Not a hypothetical – this exact center/FWHM table is the same worked
example already verified in Apps/RTMs/app.R’s own “How in
R” tutorial tab (a synchronized 3-camera system, one calibration
sheet):
own_centers <- c(444, 475, 502, 531, 550, 560, 570, 650, 668, 678, 705, 717, 740, 754, 842)
own_fwhm <- c(28, 32, 18, 14, 12, 27, 14, 16, 14, 14, 10, 12, 18, 10, 57)
own_bands <- get.spectral.convolution.gaussian(df, centers = own_centers, fwhm = own_fwhm)
own_bands
#> band wl fwhm RFL
#> 1 1 444 28 0.01567923
#> 2 2 475 32 0.01650100
#> 3 3 502 18 0.02098656
#> 4 4 531 14 0.04553117
#> 5 5 550 12 0.04843935
#> 6 6 560 27 0.04530615
#> 7 7 570 14 0.04167810
#> 8 8 650 16 0.02089493
#> 9 9 668 14 0.01787229
#> 10 10 678 14 0.01801478
#> 11 11 705 10 0.08189282
#> 12 12 717 12 0.15184254
#> 13 13 740 18 0.28362162
#> 14 14 754 10 0.32738876
#> 15 15 842 57 0.36633320
plot(df$wave, df$rfl, type = "l", col = "grey70",
xlab = "Wavelength (nm)", ylab = "Reflectance", main = "Native 1nm vs. own 15-band rig")
# Points, not a line -- only 15 sparse bands, a line would connect straight
# across large gaps (e.g. 570->650nm) in a way that misrepresents the sensor.
points(own_bands$wl, own_bands$RFL, col = "#2E8B57", pch = 19, cex = 1.2)
legend("topright", c("Native 1nm", "Own 15-band rig"), col = c("grey70", "#2E8B57"), lty = c(1, NA), pch = c(NA, 19))
Both center AND FWHM are known here (from the camera rig’s own calibration sheet) – each band is approximated as a Gaussian response with that exact center/width. Contrast with the Headwall example next, where only centers are available.
5. Headwall Nano-Hyperspec: VNIR and VNIR+SWIR
A pushbroom hyperspectral camera where only band CENTERS are known –
copied from its ENVI header’s wavelength = {...} block,
with no fwhm = {...} block alongside it. This is genuinely
how these headers ship: get.spectral.convolution.gaussian()
estimates FWHM from band spacing automatically when fwhm is
omitted.
VNIR variant
The same real header snippet already used in
Apps/RTMs/app.R’s tutorial tab – five bands shown there
explicitly (# (full header has 272)), at a fixed step
confirmed directly from those five real values (400.247-398.0166 =
402.4774-400.247 = … = 2.2304nm exactly):
headwall_step <- 400.247 - 398.0166
cat("Confirmed real step size from the 5-value header snippet:", round(headwall_step, 4), "nm\n")
#> Confirmed real step size from the 5-value header snippet: 2.2304 nm
# Extrapolating the real header's own documented step to its full stated
# length (272 bands) -- not a fabricated instrument, the real spacing
# applied for its full real extent.
headwall_vnir_centers <- 398.0166 + (0:271) * headwall_step
cat("VNIR range:", round(range(headwall_vnir_centers), 1), "nm,", length(headwall_vnir_centers), "bands\n")
#> VNIR range: 398 1002.5 nm, 272 bands
headwall_vnir_bands <- get.spectral.convolution.gaussian(df, centers = headwall_vnir_centers)
plot(df$wave, df$rfl, type = "l", col = "grey70",
xlab = "Wavelength (nm)", ylab = "Reflectance", main = "Headwall Nano-Hyperspec VNIR (272 bands, centers only)")
# Points, not a solid line -- at 272 bands / 2.23nm spacing a solid line is
# dense enough to fully cover the native grey line underneath; points let
# the native spectrum show through between them. Same green used for the
# hyperspectral/dense-sensor overlay throughout this page (Sections 2-4).
points(headwall_vnir_bands$wl, headwall_vnir_bands$RFL, col = "#2E8B57", pch = 19, cex = 0.8)
legend("topright", c("Native 1nm", "Headwall VNIR"), col = c("grey70", "#2E8B57"), lty = c(1, NA), pch = c(NA, 19))
VNIR+SWIR variant
A real limitation, stated rather than papered over: this repository has no independently-verified SWIR-companion header for this instrument – only the VNIR header above is a genuine, sourced calibration snippet. Rather than inventing precise SWIR band centers and presenting them as if real, the SWIR portion below uses round, clearly-labeled representative values (typical published Nano-Hyperspec VNIR+SWIR coverage: extending to ~2500nm at coarser spacing than the VNIR segment) – illustrative of the pattern, not an authoritative instrument header:
# Representative only -- see the note above. The VNIR segment is the real
# header from Section 5's VNIR variant; the SWIR segment is a round,
# clearly-marked stand-in for a real second-camera header this repo
# doesn't have verified.
swir_representative_centers <- seq(1000, 2500, by = 5) # illustrative spacing, NOT a sourced header
headwall_vnirswir_centers <- c(headwall_vnir_centers, swir_representative_centers)
headwall_vnirswir_bands <- get.spectral.convolution.gaussian(df, centers = headwall_vnirswir_centers)
cat("VNIR+SWIR (illustrative):", nrow(headwall_vnirswir_bands), "bands,",
round(min(headwall_vnirswir_bands$wl)), "-", round(max(headwall_vnirswir_bands$wl)), "nm\n")
#> VNIR+SWIR (illustrative): 573 bands, 398 - 2500 nm
plot(df$wave, df$rfl, type = "l", col = "grey70",
xlab = "Wavelength (nm)", ylab = "Reflectance", main = "Headwall VNIR+SWIR (illustrative)")
points(headwall_vnirswir_bands$wl, headwall_vnirswir_bands$RFL, col = "#2E8B57", pch = 19, cex = 0.8)
legend("topright", c("Native 1nm", "Headwall VNIR+SWIR"), col = c("grey70", "#2E8B57"), lty = c(1, NA), pch = c(NA, 19))
Same get.spectral.convolution.gaussian() call, same
centers-only / FWHM-estimated-from-spacing pattern as the pure VNIR case
– only the band list changes.
6. Spectral sampling density, side by side
comparison <- data.frame(
Sensor = c("Sentinel-2A", "PRISMA", "EnMAP", "Own 15-band rig", "Headwall VNIR", "Headwall VNIR+SWIR (illustrative)"),
Bands = c(nrow(se2_bands), nrow(prisma_bands), nrow(enmap_bands),
nrow(own_bands), nrow(headwall_vnir_bands), nrow(headwall_vnirswir_bands)),
Approx_spacing_nm = c(round(diff(range(se2_bands$wl)) / (nrow(se2_bands) - 1), 0),
round(diff(range(prisma_bands$wl)) / (nrow(prisma_bands) - 1), 1),
round(diff(range(enmap_bands$wl)) / (nrow(enmap_bands) - 1), 1),
NA,
round(headwall_step, 2),
NA)
)
knitr::kable(comparison)| Sensor | Bands | Approx_spacing_nm |
|---|---|---|
| Sentinel-2A | 13 | 147.00 |
| PRISMA | 234 | 8.90 |
| EnMAP | 242 | 8.40 |
| Own 15-band rig | 15 | NA |
| Headwall VNIR | 272 | 2.23 |
| Headwall VNIR+SWIR (illustrative) | 573 | NA |
Approx_spacing_nm is NA for the two
irregular/mixed-sampling rows – the 15-band rig places bands at specific
absorption features rather than uniform steps, and the illustrative
VNIR+SWIR row mixes the real 2.23nm VNIR step with the representative
5nm SWIR step, so a single averaged number would misrepresent both.
op <- par(mfrow = c(2, 1), mar = c(4, 4, 2, 1))
plot(df$wave, df$rfl, type = "l", col = "grey70", ylim = c(0, max(df$rfl)),
xlab = "Wavelength (nm)", ylab = "Reflectance", main = "Multispectral: Sentinel-2A (13 bands)")
points(se2_bands$wl, se2_bands$RFL, col = "#B2182B", pch = 19)
plot(df$wave, df$rfl, type = "l", col = "grey70", ylim = c(0, max(df$rfl)),
xlab = "Wavelength (nm)", ylab = "Reflectance", main = "Hyperspectral: PRISMA (dense) vs. Headwall VNIR (272 bands)")
lines(prisma_bands$wl, prisma_bands$RFL, col = "#2E8B57", lwd = 1)
points(headwall_vnir_bands$wl, headwall_vnir_bands$RFL, col = "#E31A1C", pch = ".", cex = 2)
legend("topright", c("PRISMA", "Headwall VNIR"), col = c("#2E8B57", "#E31A1C"), lty = 1, cex = 0.8)
par(op)What “hyperspectral” buys over multispectral, concretely: Sentinel-2A’s 13 bands can only sample the absorption features and red-edge shape at a handful of fixed points; PRISMA/EnMAP/the Headwall’s 200+ narrow bands resolve the actual continuous shape of those features – the difference that lets hyperspectral data support things multispectral can’t (e.g. narrowband indices centered exactly on an absorption feature, or fitting absorption-feature depth/position directly rather than inferring it from a handful of wide bands).
What’s next
- Tutorial 09 – vegetation indices, including which ones need the fine spectral sampling this page demonstrated vs. which work fine at Sentinel-2 resolution.
- Tutorial 12 – training an inversion model on convolved, sensor-realistic reflectance – try substituting one of this page’s hyperspectral sensors for Sentinel-2A there.