Skip to content

Run matched filters in EMIT product

Author: Gonzalo Mateo-GarcΓ­a

Example from work: > V. Růžička, G. Mateo-Garcia, L. Gómez-Chova, A. Vaughan, L. Guanter, and A. Markham, Semantic segmentation of methane plumes with hyperspectral machine learning models. Scientific Reports 13, 19999 (2023). DOI: 10.1038/s41598-023-44918-6.

pip install "marshsi[mag1c]"

Step 1: download EMIT image

In order to download and process the EMIT image we will use the emit reader in the georeader package. See this tutorial for an example of how to load and plot the data.

EMIT product versions

NASA's LP DAAC ended forward processing of the EMIT v001 products on 2026-08-31 (EMIT Version 1 and Existing Version 2 Forward Processing Ended). New acquisitions are published as L1B radiance v002, with the L2A mask as a separate v003 product (EMIT Version 2 Level 1B and Level 2A, and Version 3 Level 2A Data Products Released). Older acquisitions are being reprocessed to v002.

v002 product names drop the orbit and scene numbers (EMIT_L1B_RAD_002_20220827T060753 instead of EMIT_L1B_RAD_001_20220827T060753_2223904_013), and reading them requires georeader 2.4 or later. The marshsi retrievals work on both versions. This notebook runs on v002 by default (EMIT_VERSION below), and the last section repeats the demo on v001 of the same acquisition and compares the two.

import os
import warnings
import numpy as np
import matplotlib.pyplot as plt

# mag1c and georeader call np.divide/np.log with where= and no out=; the result is masked afterwards.
warnings.filterwarnings("ignore", message="'where' used without 'out'")

EMIT_FILENAMES = {
    "001": "../tests/data/EMIT_L1B_RAD_001_20220827T060753_2223904_013.nc",
    "002": "../tests/data/EMIT_L1B_RAD_002_20220827T060753.nc",
}
EMIT_VERSION = "002"
EMIT_FILENAME = EMIT_FILENAMES[EMIT_VERSION]

NEED_DOWNLOAD = not os.path.exists(EMIT_FILENAME)
print(f"File '{EMIT_FILENAME}' exists: {not NEED_DOWNLOAD}")
File '../tests/data/EMIT_L1B_RAD_002_20220827T060753.nc' exists: True

import georeader
from georeader.readers import emit, download_utils
from georeader import plot

# NASA's data archive requires creating an account for downloading EMIT files directly.
# Create a user and a token at the NASA Earthdata portal (https://urs.earthdata.nasa.gov/profile)
# and set it as the EARTHDATA_TOKEN environment variable (or a GitHub Actions secret).
earthdata_nasa_account = True

if earthdata_nasa_account:
    # Also used by EMITImage to download the OBS file next to the radiance file.
    emit.AUTH_METHOD = "token"
    emit.TOKEN = os.environ.get("EARTHDATA_TOKEN", "")  # NASA Earthdata bearer token


def download_emit(filename):
    if earthdata_nasa_account:
        link = emit.get_radiance_link(filename)
        headers = {"Authorization": f"Bearer {emit.TOKEN}"}
        download_utils.download_product(link, headers=headers, verify=True, filename=filename)
    elif filename == EMIT_FILENAMES["001"]:
        # alternatively for the sake of this demo, we also uploaded the v001 scene (RAD and OBS) on our gdrive
        import subprocess
        out_dir = os.path.dirname(filename) + "/"
        subprocess.run(["gdown", "1jyFejO80Q82qUZ5tRPaDTqsBHefuXYa0", "-O", out_dir], check=True)
        subprocess.run(["gdown", "1a9k2YZDXfa4g95KlqCcKfkYcaA-QegYU", "-O", out_dir], check=True)
    else:
        raise ValueError("Only the v001 scene is on gdrive; download v002 with a NASA Earthdata token")


if NEED_DOWNLOAD:
    download_emit(EMIT_FILENAME)
else:
    print(f"Skipping download, '{EMIT_FILENAME}' already exists")
Skipping download, '../tests/data/EMIT_L1B_RAD_002_20220827T060753.nc' already exists

# cache_radiance=True keeps the full-spectrum windowed radiance in RAM after
# the first load_raw() call. The four downstream reads (mag1c, AT_MF_total_EMIT,
# load_rgb after to_crs, and plume_vetting.compute_emit) then all serve from
# the in-memory array instead of re-decompressing the ~1.85 GB file each time.
# The cache propagates through to_crs / read_from_bands clones by reference,
# so emit_image_utm built below shares it with rst.
rst = emit.EMITImage(EMIT_FILENAME, cache_radiance=True)
rst
 
         File: ../tests/data/EMIT_L1B_RAD_002_20220827T060753.nc
         Transform: | 0.00,-0.00, 61.16|
|-0.00,-0.00, 36.83|
| 0.00, 0.00, 1.00|
         Shape: (285, 2007, 2239)
         Resolution: (0.0005422325202530942, 0.0005422325202530942)
         Bounds: (np.float64(61.1592142222353), np.float64(35.74201728362127), np.float64(62.3732728350893), np.float64(36.8302779517758))
         CRS: EPSG:4326
         units: uW/cm^2/SR/nm
        

Run mag1c retrieval on the EMIT product

Run mag1c filter retrieval based on the work of Foote et al. 2020.

from marshsi.emit import mag1c_emit

mfoutput, albedo = mag1c_emit.mag1c_emit(rst, column_step=4, georeferenced=False)
mfoutput

    Running mag1c filter by columns:   0%|          | 0/311 [00:00<?, ?it/s]

    Running mag1c filter by columns:   1%|          | 3/311 [00:00<00:13, 22.30it/s]

    Running mag1c filter by columns:   2%|▏         | 6/311 [00:00<00:12, 24.59it/s]

    Running mag1c filter by columns:   3%|β–Ž         | 9/311 [00:00<00:11, 25.70it/s]

    Running mag1c filter by columns:   4%|▍         | 12/311 [00:00<00:13, 22.28it/s]

    Running mag1c filter by columns:   5%|▍         | 15/311 [00:00<00:12, 23.06it/s]

    Running mag1c filter by columns:   6%|β–Œ         | 18/311 [00:00<00:12, 23.14it/s]

    Running mag1c filter by columns:   7%|β–‹         | 21/311 [00:00<00:12, 23.18it/s]

    Running mag1c filter by columns:   8%|β–Š         | 24/311 [00:01<00:11, 24.06it/s]

    Running mag1c filter by columns:   9%|β–Š         | 27/311 [00:01<00:11, 24.14it/s]

    Running mag1c filter by columns:  10%|β–‰         | 30/311 [00:01<00:11, 24.78it/s]

    Running mag1c filter by columns:  11%|β–ˆ         | 33/311 [00:01<00:11, 24.16it/s]

    Running mag1c filter by columns:  12%|β–ˆβ–        | 36/311 [00:01<00:11, 23.37it/s]

    Running mag1c filter by columns:  13%|β–ˆβ–Ž        | 39/311 [00:01<00:11, 23.42it/s]

    Running mag1c filter by columns:  14%|β–ˆβ–Ž        | 42/311 [00:01<00:11, 22.60it/s]

    Running mag1c filter by columns:  14%|β–ˆβ–        | 45/311 [00:01<00:11, 24.09it/s]

    Running mag1c filter by columns:  15%|β–ˆβ–Œ        | 48/311 [00:02<00:10, 24.44it/s]

    Running mag1c filter by columns:  16%|β–ˆβ–‹        | 51/311 [00:02<00:10, 24.38it/s]

    Running mag1c filter by columns:  17%|β–ˆβ–‹        | 54/311 [00:02<00:10, 24.50it/s]

    Running mag1c filter by columns:  18%|β–ˆβ–Š        | 57/311 [00:02<00:10, 24.72it/s]

    Running mag1c filter by columns:  19%|β–ˆβ–‰        | 60/311 [00:02<00:10, 23.87it/s]

    Running mag1c filter by columns:  20%|β–ˆβ–ˆ        | 63/311 [00:02<00:10, 23.62it/s]

    Running mag1c filter by columns:  21%|β–ˆβ–ˆ        | 66/311 [00:02<00:10, 23.71it/s]

    Running mag1c filter by columns:  22%|β–ˆβ–ˆβ–       | 69/311 [00:02<00:10, 23.23it/s]

    Running mag1c filter by columns:  23%|β–ˆβ–ˆβ–Ž       | 72/311 [00:03<00:10, 23.80it/s]

    Running mag1c filter by columns:  24%|β–ˆβ–ˆβ–       | 75/311 [00:03<00:09, 24.38it/s]

    Running mag1c filter by columns:  25%|β–ˆβ–ˆβ–Œ       | 78/311 [00:03<00:09, 24.81it/s]

    Running mag1c filter by columns:  26%|β–ˆβ–ˆβ–Œ       | 81/311 [00:03<00:09, 25.37it/s]

    Running mag1c filter by columns:  27%|β–ˆβ–ˆβ–‹       | 84/311 [00:03<00:08, 25.82it/s]

    Running mag1c filter by columns:  28%|β–ˆβ–ˆβ–Š       | 87/311 [00:03<00:09, 24.40it/s]

    Running mag1c filter by columns:  29%|β–ˆβ–ˆβ–‰       | 90/311 [00:03<00:10, 22.08it/s]

    Running mag1c filter by columns:  30%|β–ˆβ–ˆβ–‰       | 93/311 [00:03<00:09, 21.95it/s]

    Running mag1c filter by columns:  31%|β–ˆβ–ˆβ–ˆ       | 96/311 [00:04<00:09, 23.55it/s]

    Running mag1c filter by columns:  32%|β–ˆβ–ˆβ–ˆβ–      | 99/311 [00:04<00:08, 23.81it/s]

    Running mag1c filter by columns:  33%|β–ˆβ–ˆβ–ˆβ–Ž      | 102/311 [00:04<00:08, 23.96it/s]

    Running mag1c filter by columns:  34%|β–ˆβ–ˆβ–ˆβ–      | 105/311 [00:04<00:08, 24.19it/s]

    Running mag1c filter by columns:  35%|β–ˆβ–ˆβ–ˆβ–      | 108/311 [00:04<00:08, 24.26it/s]

    Running mag1c filter by columns:  36%|β–ˆβ–ˆβ–ˆβ–Œ      | 111/311 [00:04<00:08, 23.04it/s]

    Running mag1c filter by columns:  37%|β–ˆβ–ˆβ–ˆβ–‹      | 114/311 [00:04<00:08, 23.27it/s]

    Running mag1c filter by columns:  38%|β–ˆβ–ˆβ–ˆβ–Š      | 117/311 [00:04<00:08, 22.70it/s]

    Running mag1c filter by columns:  39%|β–ˆβ–ˆβ–ˆβ–Š      | 120/311 [00:05<00:08, 23.39it/s]

    Running mag1c filter by columns:  40%|β–ˆβ–ˆβ–ˆβ–‰      | 123/311 [00:05<00:07, 24.49it/s]

    Running mag1c filter by columns:  41%|β–ˆβ–ˆβ–ˆβ–ˆ      | 126/311 [00:05<00:07, 24.80it/s]

    Running mag1c filter by columns:  41%|β–ˆβ–ˆβ–ˆβ–ˆβ–     | 129/311 [00:05<00:07, 24.57it/s]

    Running mag1c filter by columns:  42%|β–ˆβ–ˆβ–ˆβ–ˆβ–     | 132/311 [00:05<00:07, 24.59it/s]

    Running mag1c filter by columns:  43%|β–ˆβ–ˆβ–ˆβ–ˆβ–Ž     | 135/311 [00:05<00:06, 25.79it/s]

    Running mag1c filter by columns:  44%|β–ˆβ–ˆβ–ˆβ–ˆβ–     | 138/311 [00:05<00:06, 25.31it/s]

    Running mag1c filter by columns:  45%|β–ˆβ–ˆβ–ˆβ–ˆβ–Œ     | 141/311 [00:05<00:07, 24.12it/s]

    Running mag1c filter by columns:  46%|β–ˆβ–ˆβ–ˆβ–ˆβ–‹     | 144/311 [00:06<00:06, 24.51it/s]

    Running mag1c filter by columns:  47%|β–ˆβ–ˆβ–ˆβ–ˆβ–‹     | 147/311 [00:06<00:06, 25.32it/s]

    Running mag1c filter by columns:  48%|β–ˆβ–ˆβ–ˆβ–ˆβ–Š     | 150/311 [00:06<00:06, 25.79it/s]

    Running mag1c filter by columns:  49%|β–ˆβ–ˆβ–ˆβ–ˆβ–‰     | 153/311 [00:06<00:06, 25.81it/s]

    Running mag1c filter by columns:  50%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ     | 156/311 [00:06<00:05, 26.31it/s]

    Running mag1c filter by columns:  51%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ     | 159/311 [00:06<00:06, 25.20it/s]

    Running mag1c filter by columns:  52%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–    | 162/311 [00:06<00:06, 24.12it/s]

    Running mag1c filter by columns:  53%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž    | 165/311 [00:06<00:06, 23.82it/s]

    Running mag1c filter by columns:  54%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–    | 168/311 [00:07<00:06, 22.28it/s]

    Running mag1c filter by columns:  55%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–    | 171/311 [00:07<00:06, 23.03it/s]

    Running mag1c filter by columns:  56%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Œ    | 174/311 [00:07<00:05, 23.94it/s]

    Running mag1c filter by columns:  57%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‹    | 177/311 [00:07<00:05, 24.75it/s]

    Running mag1c filter by columns:  58%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š    | 180/311 [00:07<00:05, 25.55it/s]

    Running mag1c filter by columns:  59%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰    | 183/311 [00:07<00:05, 24.21it/s]

    Running mag1c filter by columns:  60%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰    | 186/311 [00:07<00:05, 24.75it/s]

    Running mag1c filter by columns:  61%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    | 189/311 [00:07<00:04, 25.27it/s]

    Running mag1c filter by columns:  62%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–   | 192/311 [00:07<00:04, 25.33it/s]

    Running mag1c filter by columns:  63%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž   | 195/311 [00:08<00:04, 24.87it/s]

    Running mag1c filter by columns:  64%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž   | 198/311 [00:08<00:04, 24.93it/s]

    Running mag1c filter by columns:  65%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–   | 201/311 [00:08<00:04, 24.27it/s]

    Running mag1c filter by columns:  66%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Œ   | 204/311 [00:08<00:04, 23.60it/s]

    Running mag1c filter by columns:  67%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‹   | 207/311 [00:08<00:04, 23.67it/s]

    Running mag1c filter by columns:  68%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š   | 210/311 [00:08<00:04, 20.96it/s]

    Running mag1c filter by columns:  68%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š   | 213/311 [00:09<00:05, 16.55it/s]

    Running mag1c filter by columns:  69%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰   | 215/311 [00:09<00:05, 16.84it/s]

    Running mag1c filter by columns:  70%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰   | 217/311 [00:09<00:05, 17.10it/s]

    Running mag1c filter by columns:  70%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ   | 219/311 [00:09<00:05, 17.50it/s]

    Running mag1c filter by columns:  71%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–  | 222/311 [00:09<00:04, 18.41it/s]

    Running mag1c filter by columns:  72%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–  | 224/311 [00:09<00:04, 17.77it/s]

    Running mag1c filter by columns:  73%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž  | 226/311 [00:09<00:04, 18.26it/s]

    Running mag1c filter by columns:  73%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž  | 228/311 [00:09<00:04, 17.88it/s]

    Running mag1c filter by columns:  74%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–  | 230/311 [00:09<00:04, 16.89it/s]

    Running mag1c filter by columns:  75%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–  | 232/311 [00:10<00:04, 16.56it/s]

    Running mag1c filter by columns:  75%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Œ  | 234/311 [00:10<00:05, 15.03it/s]

    Running mag1c filter by columns:  76%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Œ  | 236/311 [00:10<00:04, 15.22it/s]

    Running mag1c filter by columns:  77%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‹  | 238/311 [00:10<00:04, 15.15it/s]

    Running mag1c filter by columns:  77%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‹  | 240/311 [00:10<00:05, 14.01it/s]

    Running mag1c filter by columns:  78%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š  | 242/311 [00:10<00:04, 14.64it/s]

    Running mag1c filter by columns:  78%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š  | 244/311 [00:10<00:04, 15.90it/s]

    Running mag1c filter by columns:  79%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰  | 247/311 [00:11<00:03, 18.99it/s]

    Running mag1c filter by columns:  80%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  | 250/311 [00:11<00:02, 21.37it/s]

    Running mag1c filter by columns:  81%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ– | 253/311 [00:11<00:02, 22.91it/s]

    Running mag1c filter by columns:  82%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ– | 256/311 [00:11<00:02, 23.25it/s]

    Running mag1c filter by columns:  83%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž | 259/311 [00:11<00:02, 24.51it/s]

    Running mag1c filter by columns:  84%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ– | 262/311 [00:11<00:01, 25.27it/s]

    Running mag1c filter by columns:  85%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Œ | 265/311 [00:11<00:01, 25.74it/s]

    Running mag1c filter by columns:  86%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Œ | 268/311 [00:11<00:01, 25.93it/s]

    Running mag1c filter by columns:  87%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‹ | 271/311 [00:11<00:01, 25.10it/s]

    Running mag1c filter by columns:  88%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š | 274/311 [00:12<00:01, 21.91it/s]

    Running mag1c filter by columns:  89%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰ | 277/311 [00:12<00:01, 22.29it/s]

    Running mag1c filter by columns:  90%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ | 280/311 [00:12<00:01, 22.86it/s]

    Running mag1c filter by columns:  91%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ | 283/311 [00:12<00:01, 22.53it/s]

    Running mag1c filter by columns:  92%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–| 286/311 [00:12<00:01, 22.92it/s]

    Running mag1c filter by columns:  93%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž| 289/311 [00:12<00:00, 23.06it/s]

    Running mag1c filter by columns:  94%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–| 292/311 [00:12<00:00, 21.98it/s]

    Running mag1c filter by columns:  95%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–| 295/311 [00:13<00:00, 21.64it/s]

    Running mag1c filter by columns:  96%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Œ| 298/311 [00:13<00:00, 23.27it/s]

    Running mag1c filter by columns:  97%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‹| 301/311 [00:13<00:00, 24.00it/s]

    Running mag1c filter by columns:  98%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š| 304/311 [00:13<00:00, 23.53it/s]

    Running mag1c filter by columns:  99%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š| 307/311 [00:13<00:00, 22.37it/s]

    Running mag1c filter by columns: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰| 310/311 [00:13<00:00, 21.36it/s]

    Running mag1c filter by columns: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 311/311 [00:13<00:00, 22.58it/s]


array([[  0.     ,   0.     ,   0.     , ...,   0.     ,   0.     ,
          0.     ],
       [331.95007,   0.     ,   0.     , ...,   0.     ,   0.     ,
          0.     ],
       [544.54987,   0.     ,   0.     , ...,   0.     ,   0.     ,
          0.     ],
       ...,
       [  0.     ,   0.     ,   0.     , ...,   0.     ,   0.     ,
        268.0124 ],
       [  0.     ,   0.     ,   0.     , ...,   0.     ,   0.     ,
          0.     ],
       [  0.     ,   0.     ,   0.     , ...,   0.     ,   0.     ,
          0.     ]], shape=(1280, 1242), dtype=float32)

Plot results on satellite coordinates:

fig, ax = plt.subplots(1,2, figsize=(16,8), tight_layout=True, sharey=True)
im = ax[0].imshow(mfoutput, vmin=0,vmax=1750)
ax[0].set_title(r"$\Delta$CH$_4$ [ppm x m]")
plot.colorbar_next_to(im, ax[0])

im = ax[1].imshow(albedo, vmin=0)
ax[1].set_title("Albedo")
plot.colorbar_next_to(im, ax[1])
<matplotlib.colorbar.Colorbar at 0x794ec06e30e0>
No description has been provided for this image

Run WMF retrieval on EMIT product

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

from marshsi.emit import retrieval_upv_emit


mf_classic, mf_extended, mf_combo, mf_extended_filtered, rad = retrieval_upv_emit.AT_MF_total_EMIT(rst, georeferenced=False) # georreferenced=False

Plot results on satellite coordinates:

fig, ax = plt.subplots(2,2, figsize=(20,20), tight_layout=True, sharey=True, sharex=True)

axi = ax[0,0]
im = axi.imshow(mf_classic, vmin=0,vmax=0.3)
axi.set_title(r"MF $\Delta$CH$_4$ [ppm]")
plot.colorbar_next_to(im, axi)

axi = ax[0,1]
im = axi.imshow(mf_extended, vmin=0,vmax=0.3)
axi.set_title(r"WMF $\Delta$CH$_4$ [ppm]")
plot.colorbar_next_to(im, axi)

axi = ax[1,0]
im = axi.imshow(mf_combo, vmin=0,vmax=0.3)
axi.set_title(r"MF Combo $\Delta$CH$_4$ [ppm]")
plot.colorbar_next_to(im, axi)

# rad
axi = ax[1,1]
im = axi.imshow(rad, vmin=0)
axi.set_title(f"Radiance at {retrieval_upv_emit.RAD_WAVELENGTH} in {rst.units}")
plot.colorbar_next_to(im, axi)
<matplotlib.colorbar.Colorbar at 0x794ec27f30e0>
No description has been provided for this image

Georreference to UTM

crs_utm = georeader.get_utm_epsg(rst.footprint("EPSG:4326"))
emit_image_utm = rst.to_crs(crs_utm)

rgb = emit_image_utm.load_rgb(as_reflectance=True)

fig, ax = plt.subplots(1,3, figsize=(15,5), sharey=True)

mag1c_geo = emit_image_utm.georreference(mfoutput, fill_value_default=-1)
wmf_geo = emit_image_utm.georreference(mf_extended, fill_value_default=-1)

plot.show(rgb, ax=ax[0], title= "RGB",mask=True) # add_scalebar=True -&gt; pip install matplotlib_scalebar
plot.show(mag1c_geo, ax=ax[1], title= r"Mag1c $\Delta X$CH$_4$ [ppm x m]",mask=True,vmin=0, vmax=1750,
         add_colorbar_next_to=True)
plot.show(wmf_geo, ax=ax[2], title= r"WMF $\Delta X$CH$_4$ [ppm]", mask=True, vmin=0, vmax=1750/8_000, 
          add_colorbar_next_to=True)
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-9999.0..1.0].

<Axes: title={'center': 'WMF $\\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 import plume_vetting

from importlib import reload
reload(plume_vetting)

PLUMES_FILE = "../tests/data/plume_vetting/plumes_emit.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_geo.values
if wmf_values.ndim == 3:
    wmf_values = wmf_values[0]
wmf_arr = np.where(
    wmf_values == wmf_geo.fill_value_default,
    wmf_geo.fill_value_default,
    wmf_values * ATMOSPHERE_HEIGHT_METHANE,
)
wmf_ppmxm = GeoTensor(wmf_arr, transform=wmf_geo.transform, crs=wmf_geo.crs,
                      fill_value_default=wmf_geo.fill_value_default)

results = plume_vetting.compute_emit(
    emit_image=emit_image_utm,
    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.480943 328.448570
1 0.352480 449.576411
2 0.250121 754.082037
3 0.093108 2137.781336
4 0.784889 183.305691
5 0.214774 644.877861
6 0.090768 1897.233852
7 0.295351 497.309343
8 0.095737 4033.825615
9 0.929433 64.034608
10 0.329464 552.104013
11 0.100806 2327.242794
12 0.323175 438.336946
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}")
    plume_vetting.plot_vetting(results[best_idx], cmf_values=wmf_ppmxm.values)
Polygon 6: D_norm=0.091

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

Same demo on EMIT v001

The cells below repeat the retrievals and the plume vetting on the other product version of the same acquisition, and compare both versions. The two versions are georeferenced to the same UTM zone but not to the same grid, because the v002 geolocation differs slightly from v001.

OTHER_VERSION = "001" if EMIT_VERSION == "002" else "002"
other_filename = EMIT_FILENAMES[OTHER_VERSION]
if not os.path.exists(other_filename):
    download_emit(other_filename)

# Release the first version's radiance so only one cube is held in memory. The cache is shared by rst,
# its clones and IPython's output history, so clear it instead of only deleting names. rad is a view of
# the float64 cube used inside AT_MF_total_EMIT, so it keeps that cube alive too.
rst.clear_radiance_cache()
del rst, emit_image_utm, rad
rst_other = emit.EMITImage(other_filename, cache_radiance=True)
mfoutput_other, _ = mag1c_emit.mag1c_emit(rst_other, column_step=4, georeferenced=False, display_pbar=False)
mf_extended_other = retrieval_upv_emit.AT_MF_total_EMIT(rst_other, georeferenced=False)[1]

emit_image_utm_other = rst_other.to_crs(crs_utm)
rgb_other = emit_image_utm_other.load_rgb(as_reflectance=True)
mag1c_geo_other = emit_image_utm_other.georreference(mfoutput_other, fill_value_default=-1)
wmf_geo_other = emit_image_utm_other.georreference(mf_extended_other, fill_value_default=-1)
rst_other
 
         File: ../tests/data/EMIT_L1B_RAD_001_20220827T060753_2223904_013.nc
         Transform: | 0.00,-0.00, 61.16|
|-0.00,-0.00, 36.83|
| 0.00, 0.00, 1.00|
         Shape: (285, 2007, 2239)
         Resolution: (0.0005422325202530942, 0.0005422325202530942)
         Bounds: (np.float64(61.1592142222353), np.float64(35.74201728362127), np.float64(62.3732728350893), np.float64(36.8302779517758))
         CRS: EPSG:4326
         units: uW/cm^2/SR/nm
        
fig, ax = plt.subplots(2, 3, figsize=(15, 10), sharex=True, sharey=True, tight_layout=True)

versions = {EMIT_VERSION: (rgb, mag1c_geo, wmf_geo), OTHER_VERSION: (rgb_other, mag1c_geo_other, wmf_geo_other)}
for row, (version, (rgb_v, mag1c_v, wmf_v)) in enumerate(sorted(versions.items())):
    plot.show(rgb_v, ax=ax[row, 0], title=f"v{version} RGB", mask=True)
    plot.show(mag1c_v, ax=ax[row, 1], title=rf"v{version} Mag1c $\Delta X$CH$_4$ [ppm x m]", mask=True,
              vmin=0, vmax=1750, add_colorbar_next_to=True)
    plot.show(wmf_v, ax=ax[row, 2], title=rf"v{version} WMF $\Delta X$CH$_4$ [ppm]", mask=True,
              vmin=0, vmax=1750/8_000, add_colorbar_next_to=True)
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-9999.0..1.0].

Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-9999.0..1.0].

No description has been provided for this image
wmf_values_other = wmf_geo_other.values
if wmf_values_other.ndim == 3:
    wmf_values_other = wmf_values_other[0]
wmf_ppmxm_other = GeoTensor(
    np.where(wmf_values_other == wmf_geo_other.fill_value_default, wmf_geo_other.fill_value_default,
             wmf_values_other * ATMOSPHERE_HEIGHT_METHANE),
    transform=wmf_geo_other.transform, crs=wmf_geo_other.crs, fill_value_default=wmf_geo_other.fill_value_default,
)

results_other = plume_vetting.compute_emit(
    emit_image=emit_image_utm_other,
    cmf=wmf_ppmxm_other,
    polygons=polygons,
    min_polygon_size=20,
    random_seed=42,
)

summary_other = (
    pd.DataFrame.from_dict(results_other, orient="index")
    .reindex(columns=["D_norm", "alpha_con_len"])
    .rename_axis("polygon")
    .sort_index()
)
pd.concat({f"v{EMIT_VERSION}": summary, f"v{OTHER_VERSION}": summary_other}, axis=1)
v002 v001
D_norm alpha_con_len D_norm alpha_con_len
polygon
0 0.480943 328.448570 0.556507 305.693196
1 0.352480 449.576411 0.296048 599.550531
2 0.250121 754.082037 0.174494 785.158936
3 0.093108 2137.781336 0.106628 1933.989522
4 0.784889 183.305691 1.001629 10.228344
5 0.214774 644.877861 0.157723 833.134033
6 0.090768 1897.233852 0.121652 1665.302879
7 0.295351 497.309343 0.191946 532.584839
8 0.095737 4033.825615 0.094294 3841.045367
9 0.929433 64.034608 0.722786 140.879506
10 0.329464 552.104013 0.320229 629.945574
11 0.100806 2327.242794 0.108880 2086.863912
12 0.323175 438.336946 0.290267 448.001360

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{ruzicka_starcop_2023,
    title = {Semantic segmentation of methane plumes with hyperspectral machine learning models},
    volume = {13},
    issn = {2045-2322},
    url = {https://www.nature.com/articles/s41598-023-44918-6},
    doi = {10.1038/s41598-023-44918-6},
    number = {1},
    journal = {Scientific Reports},
    author = {Růžička, Vít and Mateo-Garcia, Gonzalo and Gómez-Chova, Luis and Vaughan, Anna, and Guanter, Luis and Markham, Andrew},
    month = nov,
    year = {2023},
    pages = {19999}
}