Skip to content

Run matched filters in PRISMA product

Author: Gonzalo Mateo-GarcĂ­a

See example of using PRISMA reader in georeader.

The Window-Wide Matched Filter is proposed in: > J. Roger, L. Guanter, J. Gorroño, and I. Irakulis-Loitxate, “Exploiting the entire near-infrared spectral range to improve the detection of methane plumes with high-resolution imaging spectrometers,” Atmospheric Measurement Techniques, vol. 17, no. 4, pp. 1333–1346, Feb. 2024, doi: 10.5194/amt-17-1333-2024.

pip install marshsi adlfs

Download PRISMA image from UNEP IMEO Cloud Storage

import os

SAS_TOKEN = ""
AZURE_STORAGE_ACCOUNT=""
CONTAINER = ""

folder_dest = "../tests/data/PRISMA"
tile = "PRS_L1_STD_OFFL_20250316071846_20250316071850_0001"
path_raster_dst = os.path.join(folder_dest, f"{tile}.he5")

NEED_DOWNLOAD = not os.path.exists(path_raster_dst)
print(f"File '{path_raster_dst}' exists: {not NEED_DOWNLOAD}")
File '../tests/data/PRISMA/PRS_L1_STD_OFFL_20250316071846_20250316071850_0001.he5' exists: True

if NEED_DOWNLOAD:
    import adlfs
    fs = adlfs.AzureBlobFileSystem(account_name=AZURE_STORAGE_ACCOUNT, sas_token=SAS_TOKEN, container=CONTAINER)
if NEED_DOWNLOAD:
    os.makedirs(folder_dest, exist_ok=True)

    path_raster = f"az://{CONTAINER}/PRISMA/raster/{tile}.he5"
    if not fs.exists(path_raster):
        raise FileNotFoundError(f"File {path_raster} does not exist")

    print(f"Downloading {path_raster}...")
    fs.get(path_raster, path_raster_dst)
else:
    print(f"Skipping download, '{path_raster_dst}' already exists")
Skipping download, '../tests/data/PRISMA/PRS_L1_STD_OFFL_20250316071846_20250316071850_0001.he5' already exists

!tree {folder_dest}
/bin/bash: line 1: tree: command not found

Load image

from georeader.readers import prisma

rst = prisma.PRISMA(path_raster_dst)
rst

        File: ../tests/data/PRISMA/PRS_L1_STD_OFFL_20250316071846_20250316071850_0001.he5
        Bounds: (53.92854309082031, 38.101470947265625, 54.339393615722656, 38.42490768432617)
        Time: 2025-03-16 07:18:46.598999+00:00
        VNIR Range: (np.float32(406.9934), np.float32(977.3654)) 63 bands
        SWIR Range: (np.float32(943.3579), np.float32(2497.1155)) 171 bands
        

Run WW-MF retrieval on PRISMA product

Run wide matched filter retrieval based on the work of Roger et al. 2024.

from marshsi.prismaenmap import retrieval_upv_prisma_enmap

cube_crs = retrieval_upv_prisma_enmap.MF_sunglint_combo_prisma(rst)
cube_crs
INFO:marshsi.prismaenmap.retrieval_upv_prisma_enmap:Running WMF in wavelength range: 978.68798828125 - 2462.7236328125
INFO:marshsi.prismaenmap.retrieval_upv_prisma_enmap:Running MF2100 in wavelength range: 2102.565673828125 - 2462.7236328125

 
         Transform: | 30.00, 0.00, 230898.88|
| 0.00,-30.00, 4257208.59|
| 0.00, 0.00, 1.00|
         Shape: (7, 1221, 1223)
         Resolution: (30.0, 30.0)
         Bounds: (230898.877003546, 4220578.5899621155, 267588.877003546, 4257208.5899621155)
         CRS: EPSG:32640
         fill_value_default: -1
        
rgb_local = rst.load_rgb(as_reflectance=True, raw=False)
rgb_local
/anaconda/envs/marsmlpy312_dev/lib/python3.12/site-packages/georeader/reflectance.py:219: UserWarning: 'where' used without 'out', expect unitialized memory in output. If this is intentional, use out=None.
  response = np.divide(response, response.sum(axis=0), where=response.sum(axis=0) > 0)# (N, K)

 
         Transform: | 30.00, 0.00, 230898.88|
| 0.00,-30.00, 4257208.59|
| 0.00, 0.00, 1.00|
         Shape: (3, 1221, 1223)
         Resolution: (30.0, 30.0)
         Bounds: (230898.877003546, 4220578.5899621155, 267588.877003546, 4257208.5899621155)
         CRS: EPSG:32640
         fill_value_default: -1
        

Plot results

from georeader import plot
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1,3, figsize=(15,5), sharey=True)
plot.show(rgb_local, ax=ax[0], mask=True, title="RGB")

mf = cube_crs.isel({"band": 1})
plot.show(mf, ax=ax[1], vmin=0, vmax=.3, title= "MF $\Delta X$CH$_4$ [ppm]", mask=True, add_colorbar_next_to=True)
wmf = cube_crs.isel({"band": 2})
plot.show(wmf, ax=ax[2], vmin=0, vmax=.3, title= "WWMF $\Delta X$CH$_4$ [ppm]", mask=True,add_colorbar_next_to=True)
<>:8: SyntaxWarning: invalid escape sequence '\D'
<>:10: SyntaxWarning: invalid escape sequence '\D'
<>:8: SyntaxWarning: invalid escape sequence '\D'
<>:10: SyntaxWarning: invalid escape sequence '\D'
/tmp/ipykernel_71130/2047996446.py:8: SyntaxWarning: invalid escape sequence '\D'
  plot.show(mf, ax=ax[1], vmin=0, vmax=.3, title= "MF $\Delta X$CH$_4$ [ppm]", mask=True, add_colorbar_next_to=True)
/tmp/ipykernel_71130/2047996446.py:10: SyntaxWarning: invalid escape sequence '\D'
  plot.show(wmf, ax=ax[2], vmin=0, vmax=.3, title= "WWMF $\Delta X$CH$_4$ [ppm]", mask=True,add_colorbar_next_to=True)
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-1.0..1.0].

<Axes: title={'center': 'WWMF $\\Delta X$CH$_4$ [ppm]'}>
No description has been provided for this image

Plume vetting

Plume vetting is a spectral validation step that checks whether the pixels inside each detected plume actually show the CHâ‚„ absorption signature relative to nearby background pixels.

The method is described in:

> C. Xiang, D. R. Thompson, R. O. Green, J. E. Fahlen, A. K. Thorpe, P. G. Brodrick, > R. W. Coleman, A. M. Lopez, and C. D. Elder, Identification of false methane plumes > for orbital imaging spectrometers: A case study with EMIT, > Remote Sensing of Environment, 2025. >

import geopandas as gpd
import pandas as pd
import numpy as np
from georeader.geotensor import GeoTensor
from marshsi.quantification import ATMOSPHERE_HEIGHT_METHANE
from marshsi.plume_vetting import compute_prisma, plot_vetting

PLUMES_FILE = "../tests/data/plume_vetting/plumes_prisma.gpkg"
plumes_gdf = gpd.read_file(PLUMES_FILE)
polygons = [geom.geoms[0] for geom in plumes_gdf.geometry]

# Convert WMF from ppm → ppm·m (multiply by effective column height in metres)
wmf_values = wmf.values
if wmf_values.ndim == 3:
    wmf_values = wmf_values[0]
wmf_arr = np.where(
    wmf_values == wmf.fill_value_default,
    wmf.fill_value_default,
    wmf_values * ATMOSPHERE_HEIGHT_METHANE,
)
wmf_arr = np.where(~np.isfinite(wmf_arr), wmf.fill_value_default, wmf_arr)

wmf_ppmxm = GeoTensor(wmf_arr, transform=wmf.transform, crs=wmf.crs,
                      fill_value_default=wmf.fill_value_default)

results = compute_prisma(
    pi=rst,
    cmf=wmf_ppmxm,
    polygons=polygons,
    min_polygon_size=20,
    random_seed=42,
)

summary = (
    pd.DataFrame.from_dict(results, orient="index")
    .reindex(columns=["D_norm", "alpha_con_len"])
    .rename_axis("polygon")
    .sort_index()
)
summary
D_norm alpha_con_len
polygon
0 0.673661 5.274283e+02
1 1.000043 1.760993e-07
2 0.596059 1.509566e+03
3 0.199070 4.046536e+03
4 1.000055 9.480123e-04
5 1.000015 1.876138e-07
6 1.000042 6.650366e-07
7 0.864644 3.911720e+02
8 0.886827 3.513862e+02
9 0.612697 1.482467e+03
10 0.171570 4.215749e+03
11 0.213296 9.358613e+02
12 0.397914 7.843298e+02
13 1.000020 1.049950e-07
14 0.919909 1.934655e+02
15 0.999982 7.672433e-10
16 0.996917 3.772516e+01
17 0.406087 7.723328e+02
18 1.000002 2.933925e-10
19 0.999924 2.565519e-09
20 1.000010 4.107636e-09
21 0.800146 3.794543e+02
22 0.961779 9.500819e+01
23 0.858099 2.370036e+02
24 1.000181 1.643085e-07
25 1.000017 1.013954e-06
if results:
    best_idx = min(results, key=lambda k: results[k]["D_norm"])
    print(f"Polygon {best_idx}: D_norm={results[best_idx]['D_norm']:.3f}")
    plot_vetting(results[best_idx], cmf_values=wmf_ppmxm.values)
Polygon 10: D_norm=0.172

No description has been provided for this image
No description has been provided for this image

The marshsi package and tutorials are released under a Creative Commons non-commercial Share-Alike licence. For using the codes in comercial pipelines written consent by the authors must be provided.

If you find this work useful please cite:

@Article{roger_2024,
    AUTHOR = {Roger, J. and Guanter, L. and Gorroño, J. and Irakulis-Loitxate, I.},
    TITLE = {Exploiting the entire near-infrared spectral range to improve the detection of methane plumes with high-resolution imaging spectrometers},
    JOURNAL = {Atmospheric Measurement Techniques},
    VOLUME = {17},
    YEAR = {2024},
    NUMBER = {4},
    PAGES = {1333--1346},
    URL = {https://amt.copernicus.org/articles/17/1333/2024/},
    DOI = {10.5194/amt-17-1333-2024}
}