Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Hyperspectral

EMIT, PRISMA, EnMAP readers

UNEP
IMEO
MARS

Modules:

  • georeader/readers/emit.py (1102 LOC)

  • georeader/readers/prisma.py (571 LOC)

  • georeader/readers/enmap.py (865 LOC)

Role: read three hyperspectral satellite sensors — EMIT (NASA, ISS, 285 bands), PRISMA (ASI, 239 bands), EnMAP (DLR, 224 bands). All cover ~400–2500 nm with ~10 nm spectral sampling. The three readers share a design pattern but diverge on georeferencing — that’s the interesting part.


1. The three sensors at a glance

SensorAgencyLaunchedBandsSpectral rangeGSDGeoreferencing
EMITNASA2022 (ISS)285380–2500 nm~60 mGLT (lookup table)
PRISMAASI2019239400–2500 nm30 mper-pixel lat/lon (interpolation)
EnMAPDLR2022224420–2450 nm30 mmap-projected + RPCs

The trio matters because they’re the workhorses of the spaceborne hyperspectral era. They map onto three distinct georeferencing strategies — the structural axis along which the readers differ.


2. EMIT — NASA’s GLT-equipped spectrometer

Data format

Raw Data Structure (NetCDF file):
┌─────────────────────────────────────┐
│  radiance: (downtrack, crosstrack, bands)  │
│  └── Shape: (~1280, ~1242, 285)            │
│                                             │
│  location/glt_x: (rows, cols)              │
│  location/glt_y: (rows, cols)              │
│  └── Geographic Lookup Table (GLT)         │
└─────────────────────────────────────┘

EMIT ships data in sensor coordinates (raw pushbroom scan lines, not orthorectified), plus a Geographic Lookup Table that names — for each pixel of the output (orthorectified) grid — which sensor pixel to read from. This is the griddata.georreference fast path from Chapter 7.

GLT orthorectification (top-of-module diagram)

Geographic Grid (Output)          Sensor Grid (Raw Data)
┌─────────────────────┐           ┌─────────────────────┐
│ (0,0)               │           │ radiance array      │
│   ┌───┬───┬───┐     │   GLT     │ ┌───────────────┐   │
│   │ a │ b │ c │     │ ──────→   │ │ (5,2) (5,3)   │   │
│   ├───┼───┼───┤     │ lookup    │ │ (6,1) (6,2)   │   │
│   │ d │ e │ f │     │           │ │ ...           │   │
│   └───┴───┴───┘     │           │ └───────────────┘   │
│               (H,W) │           │                     │
└─────────────────────┘           └─────────────────────┘

For pixel (row=1, col=2) in geographic grid:
    glt_x[1,2] = 5  →  raw_col = 5
    glt_y[1,2] = 2  →  raw_row = 2
    value = radiance[2, 5, :]  (all bands)

GLT values of 0 indicate invalid/no-data pixels

This approach allows:

  1. Efficient storage (no wasted pixels from orthorectification padding).

  2. Preservation of original radiometric values (no resampling).

  3. Flexible reprojection to any target CRS.

Class-level diagram (preserved verbatim)

GLT Orthorectification:
┌────────────────────────────┐      ┌──────────────────────────┐
│    Geographic Grid         │      │   Sensor Grid (raw)      │
│  (orthorectified space)    │      │  (pushbroom scan)        │
│  ┌───┬───┬───┬───┐        │      │  ┌───┬───┬───┬───┐      │
│  │ · │ a │ b │ · │        │  GLT │  │ e │ a │ b │ · │      │
│  ├───┼───┼───┼───┤        │  ──→ │  ├───┼───┼───┼───┤      │
│  │ c │ d │ e │ f │        │      │  │ f │ c │ d │ · │      │
│  └───┴───┴───┴───┘        │      │  └───┴───┴───┴───┘      │
│  (pixels with data)        │      │  (original acquistion)   │
└────────────────────────────┘      └──────────────────────────┘

· = no data (GLT value = 0)

For geographic pixel (row, col):
    raw_x = glt_x[row, col]  
    raw_y = glt_y[row, col]
    value = radiance[raw_y, raw_x, :]

Radiometric units & spectral

Interface

EMITImage(path, ...) — main class. Methods include load_radiance(), load_reflectance(...), load_wavelengths([w1, w2, ...]). Helpers download_product(), get_radiance_link() use NASA Earthdata credentials from ~/.georeader/auth_emit.json.


3. PRISMA — ASI’s interpolation-required spectrometer

Data format

PRISMA HDF5 File Structure:
┌─────────────────────────────────────────────────────────┐
│  /HDFEOS/SWATHS/PRS_L1_HCO/                             │
│  ├── Data Fields/                                        │
│  │   ├── VNIR_Cube: (bands, crosstrack, downtrack)      │
│  │   │   └── 400-1010 nm, ~66 bands                     │
│  │   └── SWIR_Cube: (bands, crosstrack, downtrack)      │
│  │       └── 920-2500 nm, ~173 bands                    │
│  ├── Geolocation Fields/                                 │
│  │   ├── Latitude_SWIR, Longitude_SWIR                  │
│  │   └── Latitude_VNIR, Longitude_VNIR                  │
│  └── Attributes (solar/view angles, timing, etc.)       │
│                                                          │
│  /KDP_AUX/                                               │
│  ├── Cw_Vnir_Matrix, Cw_Swir_Matrix (wavelengths)       │
│  └── Fwhm_Vnir_Matrix, Fwhm_Swir_Matrix                 │
└─────────────────────────────────────────────────────────┘

Unlike EMIT, PRISMA L1 data is NOT orthorectified. Instead, it ships per-pixel Latitude_* / Longitude_* arrays. Producing a regular grid from this requires interpolation — the slow path in Chapter 7, using read_to_crs(data, lons, lats, ...) under the hood.

Sensor grid → geographic grid

Sensor Grid (raw)                  Geographic Grid (output)
┌─────────────────────┐            ┌─────────────────────┐
│ pushbroom scan      │            │ regular grid        │
│ ┌───┬───┬───┬───┐  │  gridding  │ ┌───┬───┬───┬───┐  │
│ │ a │ b │ c │ d │  │  ───────→  │ │ a'│ b'│ c'│ d'│  │
│ ├───┼───┼───┼───┤  │            │ ├───┼───┼───┼───┤  │
│ │ e │ f │ g │ h │  │            │ │ e'│ f'│ g'│ h'│  │
│ └───┴───┴───┴───┘  │            │ └───┴───┴───┴───┘  │
│ + lat/lon per pixel│            │ + affine transform  │
└─────────────────────┘            └─────────────────────┘

The raw=True / raw=False flag on PRISMA’s load methods is the eject button: raw=True returns sensor coordinates (no interpolation, fast), raw=False runs griddata (slow, but produces a GeoTensor ready for downstream operators). For most ML workflows you want raw=False; for matched-filter retrievals that need exact radiometry, raw=True and handle gridding yourself if needed.

Dual-sensor configuration

VNIR Sensor                          SWIR Sensor
┌────────────────────┐               ┌────────────────────┐
│ 400 - 1010 nm      │               │ 920 - 2500 nm      │
│ ~66 bands          │               │ ~173 bands         │
│ ~10 nm sampling    │               │ ~10 nm sampling    │
│                    │               │                    │
│ Shared 30m GSD     │               │ Shared 30m GSD     │
└────────────────────┘               └────────────────────┘
          │                                    │
          └──────────── Overlap ───────────────┘
                     920-1010 nm

PRISMA uses two separate sensors. The reader takes care of selecting the right one when you ask for a wavelength — prisma.load_wavelengths([850, 1600]) gives you 850 nm from VNIR and 1600 nm from SWIR. The 920–1010 nm overlap is mostly used for cross-calibration; for analysis pick one source per wavelength.

Wavelength range diagram

Wavelength Range:
├──────────────────────────────────────────────────────────────┤
400nm              1000nm                                 2500nm
├───────── VNIR ──────────┤
                  ├────────────────── SWIR ───────────────────┤
                  └─ overlap ─┘
                  920-1010nm

Radiometric units

Interface

PRISMA(path) — main class. Methods load_wavelengths([w1, w2, ...], as_reflectance=False, raw=False), load_rgb(as_reflectance=False, raw=False). The wavelength-list interface handles VNIR/SWIR routing transparently.


4. EnMAP — DLR’s already-orthorectified spectrometer

Data format

EnMAP Product Structure:
┌─────────────────────────────────────────────────────────────────────┐
│  ENMAP01-____L1B-DT0000000000_20220501T101523Z_001_V010110_...     │
│  ├── *-METADATA.XML           ← Main metadata file (input)         │
│  ├── *-SPECTRAL_IMAGE_VNIR.TIF   420-1000 nm, ~88 bands            │
│  ├── *-SPECTRAL_IMAGE_SWIR.TIF   900-2450 nm, ~136 bands           │
│  ├── *-QL_QUALITY_CLOUD.TIF      Cloud mask                        │
│  ├── *-QL_QUALITY_CIRRUS.TIF     Cirrus mask                       │
│  ├── *-QL_QUALITY_SNOW.TIF       Snow mask                         │
│  ├── *-QL_QUALITY_HAZE.TIF       Haze mask                         │
│  └── *-QL_PIXELMASK_*.TIF        Per-sensor pixel masks            │
└─────────────────────────────────────────────────────────────────────┘

EnMAP differs from both: ships as separate GeoTIFF files with an XML metadata manifest. Importantly, EnMAP L1B is already orthorectified (map-projected) — no GLT, no per-pixel coords. Plus a set of RPCs (Rational Polynomial Coefficients) for refined geolocation if you need it.

Class diagram

File Structure:
┌────────────────────────────────────────────────────┐
│  METADATA.XML  ──→  wavelengths, FWHM, angles,    │
│                      gain/offset, RPCs             │
│                                                    │
│  SPECTRAL_IMAGE_VNIR.TIF  ──→  (88, H, W) bands   │
│  SPECTRAL_IMAGE_SWIR.TIF  ──→  (136, H, W) bands  │
│                                                    │
│  QL_QUALITY_*.TIF  ──→  quality masks              │
└────────────────────────────────────────────────────┘

The XML metadata file is the input — pass that path to the reader. The reader walks the sibling files for the actual band data and quality masks.

Dual-sensor + radiometric calibration

VNIR Detector                        SWIR Detector
┌────────────────────┐               ┌────────────────────┐
│ 420 - 1000 nm      │               │ 900 - 2450 nm      │
│ ~88 bands          │               │ ~136 bands         │
│ 6.5 nm sampling    │               │ 10 nm sampling     │
│ Si CCD             │               │ HgCdTe             │
└────────────────────┘               └────────────────────┘
          │                                    │
          └──────────── Overlap ───────────────┘
                     900-1000 nm
Wavelength: 420nm ──── 1000nm ──── 2450nm
            ├── VNIR ────┤
                      ├──── SWIR ──────────┤
                      └ overlap┘
                      900-1000nm

VNIR has finer spectral sampling (6.5 nm vs 10 nm) than the other two sensors — useful for narrow-feature spectroscopy. The DN-to-radiance formula is the most peculiar:

L_λ = (GAIN × DN + OFFSET) × 1000   [mW/(m²·sr·nm)]

Note: DLR gains are multiplicative (not divisive as in some sensors)

The × 1000 at the end is because DLR’s spec gives the result in W/(m²·sr·nm) and the reader converts to mW units to match PRISMA / Thuillier conventions. Don’t double-multiply — let the reader handle calibration.

RPCs

Pixel (col, row) ──→ RPC Transform ──→ Geographic (lon, lat)

RPCs model:
- Satellite orbit and attitude
- Sensor geometry  
- Terrain elevation effects (when height_off is set appropriately)

EnMAP includes Rational Polynomial Coefficients in metadata for fine geolocation. The reader can apply RPCs during loading for refined geolocation; default uses the simple affine transform from the GeoTIFF (which is already pretty good — RPCs add ~1 pixel of refinement).

Interface

EnMAPImage(metadata_xml_path, ...) — the main class, takes the XML manifest path. Plus quality-mask accessors that pair with the per-pixel masks shipped alongside.


5. Side-by-side comparison

EMITPRISMAEnMAP
File formatNetCDF (one)HDF5 (one, .he5)GeoTIFFs (many) + XML
GeoreferencingGLT lookup (fast)per-pixel lat/lon (slow interp)already map-projected (cheap)
Radiance unitsμW/(cm²·sr·nm)mW/(m²·sr·nm)W/(m²·sr·nm)mW after reader
Bands285239 (66 VNIR + 173 SWIR)224 (88 VNIR + 136 SWIR)
GSD~60 m30 m30 m
Sensor splitsingleVNIR + SWIRVNIR + SWIR
Module function usedgriddata.georreferencegriddata.read_to_crsregular RasterioReader
AuthNASA Earthdatamanual downloadmanual download
Reader cost (full scene)secondsminutes (cubic interp)seconds

The cost asymmetry is the operational reality: PRISMA scenes are ~60× slower to ortho-load than EMIT. For workflows that read scenes repeatedly (parameter sweeps, hyperparameter searches), load() once into a GeoTensor and reuse — don’t ortho-on-demand.


6. Common workflow patterns

A — methane plume detection on EMIT

from georeader.readers.emit import EMITImage
from georeader import reflectance, hyperspectral_index  # hypothetical

emit = EMITImage("EMIT_L1B_RAD_*.nc")
radiance = emit.load_radiance()         # uses GLT — fast
# (1, B=285, H, W) GeoTensor in μW/(cm²·sr·nm)
# Pass to a matched filter targeting ch4 absorption features...

B — RGB visualization across sensors

# EMIT
emit_rgb = emit.load_wavelengths([640, 550, 460], as_reflectance=True)

# PRISMA — raw=False forces gridding to a regular UTM grid
prisma_rgb = prisma.load_rgb(as_reflectance=True, raw=False)

# EnMAP
enmap_rgb = enmap.load_rgb(as_reflectance=True)

All three return a 3-band GeoTensor ready for plot.show. The interfaces deliberately converge despite the substrates diverging.

C — Spectral binning to S2 bands

(Common to all three; used by presets.enmap.ENMAP_TO_S2_BANDS from the geotoolz plan):

from georeader.readers.S2_SAFE_reader import read_srf
from georeader.reflectance import transform_to_srf, integrated_irradiance

s2_srf = read_srf(mission="S2A")                                  # S2 SRF DataFrame
s2_equiv = transform_to_srf(enmap_radiance, s2_srf, wavelengths=enmap.wavelengths)
# s2_equiv is now a 12-band cube with S2-shaped band responses

This recipe works identically for PRISMA and EMIT — the readers expose wavelengths and fwhm arrays as attributes.


7. Sharp edges

EMIT

PRISMA

EnMAP

Cross-sensor


8. Connection to geotoolz

The hyperspectral operators in geotoolz.md consume GeoTensors produced by these readers:

The split-object pattern in geotoolz (Ch §4 of geotoolz.md) is especially relevant here: compute the scene mean / covariance / endmember spectrum once (slow), then apply per-band detectors fast. The hyperspectral cube is the canonical case where state-as-artifact pays off.

Next chapter: Earth Engine — Google Earth Engine integration (export tile splitting and parallel download).