aviris module¶
This module contains functions to read and process NASA AVIRIS hyperspectral data. More info about the data can be found at https://aviris.jpl.nasa.gov. A portion of the source code is adapted from the jjmcnelis/aviris-ng-notebooks repository available at https://bit.ly/4bRCgqs. It is licensed under the MIT License. Credit goes to the original author Jack McNelis.
SPDX-FileCopyrightText = [ "2024 Jack McNelis jjmcne@gmail.com", ] SPDX-License-Identifier = "MIT"
aviris_to_image(dataset, wavelengths=None, method='nearest', output=None, **kwargs)
¶
Converts an AVIRIS dataset to an image.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset |
Union[xr.Dataset, str] |
The dataset containing the AVIRIS data or the file path to the dataset. |
required |
wavelengths |
np.ndarray |
The specific wavelengths to select. If None, all wavelengths are selected. Defaults to None. |
None |
method |
str |
The method to use for data interpolation. Defaults to "nearest". |
'nearest' |
output |
str |
The file path where the image will be saved. If None, the image will be returned as a PIL Image object. Defaults to None. |
None |
**kwargs |
Any |
Additional keyword arguments to be passed to
|
{} |
Returns:
| Type | Description |
|---|---|
Optional[rasterio.Dataset] |
The image converted from the dataset. If
|
Source code in hypercoast/aviris.py
def aviris_to_image(
dataset: Union[xr.Dataset, str],
wavelengths: Optional[np.ndarray] = None,
method: str = "nearest",
output: Optional[str] = None,
**kwargs: Any,
):
"""
Converts an AVIRIS dataset to an image.
Args:
dataset (Union[xr.Dataset, str]): The dataset containing the AVIRIS data
or the file path to the dataset.
wavelengths (np.ndarray, optional): The specific wavelengths to select. If None, all
wavelengths are selected. Defaults to None.
method (str, optional): The method to use for data interpolation.
Defaults to "nearest".
output (str, optional): The file path where the image will be saved. If
None, the image will be returned as a PIL Image object. Defaults to None.
**kwargs (Any): Additional keyword arguments to be passed to
`leafmap.array_to_image`.
Returns:
Optional[rasterio.Dataset]: The image converted from the dataset. If
`output` is provided, the image will be saved to the specified file
and the function will return None.
"""
from leafmap import array_to_image
if isinstance(dataset, str):
dataset = read_aviris(dataset, method=method)
if wavelengths is not None:
dataset = dataset.sel(wavelength=wavelengths, method=method)
return array_to_image(
dataset["reflectance"],
output=output,
transpose=False,
dtype=np.float32,
**kwargs,
)
extract_aviris(dataset, lat, lon, offset=2.0, allow_remote=False)
¶
Extracts AVIRIS data from a given xarray Dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset |
xarray.Dataset |
The dataset containing the AVIRIS data. |
required |
lat |
float |
The latitude of the point to extract. |
required |
lon |
float |
The longitude of the point to extract. |
required |
offset |
float |
The offset from the point to extract. Defaults to 2.0. |
2.0 |
allow_remote |
bool |
Allow extraction from a remote NetCDF source. Defaults to False because point reads from AVIRIS NetCDF cubes can be very slow over HTTPS/S3. |
False |
Returns:
| Type | Description |
|---|---|
xarray.DataArray |
The extracted data. |
Source code in hypercoast/aviris.py
def extract_aviris(
dataset: xr.Dataset,
lat: float,
lon: float,
offset: float = 2.0,
allow_remote: bool = False,
) -> xr.DataArray:
"""
Extracts AVIRIS data from a given xarray Dataset.
Args:
dataset (xarray.Dataset): The dataset containing the AVIRIS data.
lat (float): The latitude of the point to extract.
lon (float): The longitude of the point to extract.
offset (float, optional): The offset from the point to extract. Defaults to 2.0.
allow_remote (bool, optional): Allow extraction from a remote NetCDF
source. Defaults to False because point reads from AVIRIS NetCDF
cubes can be very slow over HTTPS/S3.
Returns:
xarray.DataArray: The extracted data.
"""
if dataset.attrs.get("_source_is_remote") and not allow_remote:
source = dataset.attrs.get("_source", "remote AVIRIS NetCDF")
raise RuntimeError(
"Interactive spectral extraction from a remote AVIRIS NetCDF can be "
"very slow because the reflectance cube is chunked in large spatial "
f"blocks. Download the reflectance NetCDF locally and reopen it with "
f"read_aviris() before using the spectral widget. Source: {source}"
)
crs = _dataset_crs(dataset)
x, y = convert_coords([[lat, lon]], "epsg:4326", crs)[0]
da = dataset["reflectance"]
if "xc" in da.coords and "yc" in da.coords:
x_con = (da["xc"] > x - offset) & (da["xc"] < x + offset)
y_con = (da["yc"] > y - offset) & (da["yc"] < y + offset)
try:
data = da.where(x_con & y_con, drop=True)
data = data.mean(dim=["x", "y"])
except ValueError:
data = np.nan * np.ones(da.sizes["wavelength"])
else:
try:
x_con = (da["x"] > x - offset) & (da["x"] < x + offset)
y_con = (da["y"] > y - offset) & (da["y"] < y + offset)
data = da.where(x_con & y_con, drop=True)
if data.sizes["x"] == 0 or data.sizes["y"] == 0:
data = da.sel(x=x, y=y, method="nearest")
else:
data = data.mean(dim=["x", "y"])
except (KeyError, ValueError):
data = np.nan * np.ones(da.sizes["wavelength"])
da = xr.DataArray(
data, dims=["wavelength"], coords={"wavelength": dataset.coords["wavelength"]}
)
return da
get_aviris_asset_url(granule, asset='RFL_ORT', prefer_s3=False)
¶
Get an AVIRIS asset URL from a CMR/earthaccess granule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
granule |
Any |
A CMR granule dictionary, an earthaccess granule, or a STAC-like item dictionary. |
required |
asset |
str |
Asset alias or filename fragment. Common aliases
include |
'RFL_ORT' |
prefer_s3 |
bool |
Prefer |
False |
Returns:
| Type | Description |
|---|---|
str |
The matching asset URL. |
Source code in hypercoast/aviris.py
def get_aviris_asset_url(
granule: Any,
asset: str = "RFL_ORT",
prefer_s3: bool = False,
) -> str:
"""Get an AVIRIS asset URL from a CMR/earthaccess granule.
Args:
granule (Any): A CMR granule dictionary, an earthaccess granule, or a
STAC-like item dictionary.
asset (str, optional): Asset alias or filename fragment. Common aliases
include ``"RFL_ORT"``, ``"UNC_ORT"``, ``"RFL_ORT_QL"``, and
``"BROWSE"``. Defaults to ``"RFL_ORT"``.
prefer_s3 (bool, optional): Prefer ``s3://`` links when available.
Defaults to False.
Returns:
str: The matching asset URL.
"""
asset_key = asset.lower()
fragments = AVIRIS_ASSET_ALIASES.get(asset_key, (asset,))
links = _granule_links(granule)
if not links:
raise ValueError("No links found in the AVIRIS granule.")
def score(link: dict) -> int:
href = str(link.get("href", ""))
is_s3 = href.startswith("s3://")
if prefer_s3 and is_s3:
return 0
if not prefer_s3 and not is_s3:
return 0
return 1
for link in sorted(links, key=score):
href = str(link.get("href", ""))
title = str(link.get("title", ""))
haystack = f"{href} {title}".lower()
if any(fragment.lower() in haystack for fragment in fragments):
return href
raise ValueError(f"Could not find AVIRIS asset matching {asset!r}.")
get_aviris_bounds(granule)
¶
Get AVIRIS granule bounds as [min_lon, min_lat, max_lon, max_lat].
Source code in hypercoast/aviris.py
def get_aviris_bounds(granule: Any) -> Optional[List[float]]:
"""Get AVIRIS granule bounds as [min_lon, min_lat, max_lon, max_lat]."""
if not isinstance(granule, dict):
return None
spatial = None
umm = granule.get("umm")
if isinstance(umm, dict):
spatial = umm.get("SpatialExtent")
if spatial is None:
spatial = granule.get("SpatialExtent")
try:
polygons = spatial["HorizontalSpatialDomain"]["Geometry"]["GPolygons"]
except (TypeError, KeyError):
return None
lons = []
lats = []
for polygon in polygons:
points = polygon.get("Boundary", {}).get("Points", [])
for point in points:
if "Longitude" in point and "Latitude" in point:
lons.append(float(point["Longitude"]))
lats.append(float(point["Latitude"]))
if not lons or not lats:
return None
return [min(lons), min(lats), max(lons), max(lats)]
get_aviris_collection_concept_id(granule)
¶
Get the CMR collection concept ID from an AVIRIS granule-like object.
Source code in hypercoast/aviris.py
def get_aviris_collection_concept_id(granule: Any) -> Optional[str]:
"""Get the CMR collection concept ID from an AVIRIS granule-like object."""
if isinstance(granule, dict):
meta = granule.get("meta")
if isinstance(meta, dict) and meta.get("collection-concept-id"):
return meta["collection-concept-id"]
for key in ("collection-concept-id", "collection_concept_id"):
if granule.get(key):
return granule[key]
return None
get_aviris_granule_ur(granule)
¶
Get the CMR GranuleUR from an AVIRIS granule-like object.
Source code in hypercoast/aviris.py
def get_aviris_granule_ur(granule: Any) -> Optional[str]:
"""Get the CMR GranuleUR from an AVIRIS granule-like object."""
if isinstance(granule, dict):
umm = granule.get("umm")
if isinstance(umm, dict) and umm.get("GranuleUR"):
return umm["GranuleUR"]
for key in ("GranuleUR", "producer_granule_id", "title"):
if granule.get(key):
return granule[key]
return None
read_aviris(filepath, wavelengths=None, method='nearest', chunks='auto', engine=None, variable='reflectance', storage_options=None, group='reflectance', **kwargs)
¶
Reads NASA AVIRIS hyperspectral data and returns an xarray dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filepath |
str |
The path or URL to the AVIRIS data. |
required |
wavelengths |
List[float] |
The wavelengths to select. If None, all wavelengths are selected. Defaults to None. |
None |
method |
str |
The method to use for selection. Defaults to "nearest". |
'nearest' |
chunks |
Any |
Chunking passed to |
'auto' |
engine |
str |
Xarray backend engine for NetCDF inputs. Remote
NetCDF sources default to |
None |
variable |
str |
Data variable to read from NetCDF inputs.
Defaults to |
'reflectance' |
storage_options |
dict |
Options passed to |
None |
group |
str |
NetCDF group containing the reflectance variable.
Defaults to |
'reflectance' |
**kwargs |
Any |
Additional arguments to pass to the selection method. |
{} |
Returns:
| Type | Description |
|---|---|
xr.Dataset |
The dataset containing the reflectance data. |
Source code in hypercoast/aviris.py
def read_aviris(
filepath: str,
wavelengths: Optional[List[float]] = None,
method: str = "nearest",
chunks: Any = "auto",
engine: Optional[str] = None,
variable: str = "reflectance",
storage_options: Optional[dict] = None,
group: Optional[str] = "reflectance",
**kwargs: Any,
) -> xr.Dataset:
"""
Reads NASA AVIRIS hyperspectral data and returns an xarray dataset.
Args:
filepath (str): The path or URL to the AVIRIS data.
wavelengths (List[float], optional): The wavelengths to select. If None,
all wavelengths are selected. Defaults to None.
method (str, optional): The method to use for selection. Defaults to
"nearest".
chunks (Any, optional): Chunking passed to ``xarray.open_dataset`` for
NetCDF inputs. Defaults to ``"auto"``.
engine (str, optional): Xarray backend engine for NetCDF inputs. Remote
NetCDF sources default to ``"h5netcdf"``.
variable (str, optional): Data variable to read from NetCDF inputs.
Defaults to ``"reflectance"``.
storage_options (dict, optional): Options passed to ``fsspec.open`` for
remote NetCDF inputs, such as S3 or HTTP credentials.
group (str, optional): NetCDF group containing the reflectance variable.
Defaults to ``"reflectance"`` for AVIRIS-3/5 products. If the group
is not present, the root dataset is opened.
**kwargs (Any): Additional arguments to pass to the selection method.
Returns:
xr.Dataset: The dataset containing the reflectance data.
"""
if not isinstance(filepath, (str, os.PathLike)) and not hasattr(filepath, "read"):
filepath = get_aviris_asset_url(filepath, asset="RFL_ORT")
if isinstance(filepath, os.PathLike):
filepath = os.fspath(filepath)
file_name = _source_name(filepath)
if _is_netcdf_path(filepath) or _is_netcdf_path(file_name):
ds = _open_aviris_netcdf(
filepath,
chunks=chunks,
engine=engine,
storage_options=storage_options,
group=group,
)
ds = _normalize_aviris_netcdf(
ds, wavelengths=wavelengths, method=method, variable=variable, **kwargs
)
if file_name:
ds.attrs["_source"] = file_name
ds.attrs["_source_is_remote"] = _is_remote_path(file_name)
return ds
if filepath.endswith(".hdr"):
filepath = filepath.replace(".hdr", "")
ds = xr.open_dataset(filepath, engine="rasterio")
wavelength = ds["wavelength"].values.tolist()
wavelength = [round(num, 2) for num in wavelength]
cols = ds.x.size
rows = ds.y.size
rio_transform = ds.rio.transform()
geo_transform = list(rio_transform)[:6]
# get the raster geotransform as its component parts
xres, _, xmin, _, yres, ymax = geo_transform
# generate coordinate arrays
xarr = np.array([xmin + i * xres for i in range(0, cols)])
yarr = np.array([ymax + i * yres for i in range(0, rows)])
ds["y"] = xr.DataArray(
data=yarr,
dims=("y"),
name="y",
attrs=dict(
units="m",
standard_name="projection_y_coordinate",
long_name="y coordinate of projection",
),
)
ds["x"] = xr.DataArray(
data=xarr,
dims=("x"),
name="x",
attrs=dict(
units="m",
standard_name="projection_x_coordinate",
long_name="x coordinate of projection",
),
)
global_atts = ds.attrs
global_atts["Conventions"] = "CF-1.6"
ds.attrs = dict(
units="unitless",
_FillValue=-9999,
grid_mapping="crs",
standard_name="reflectance",
long_name="atmospherically corrected surface reflectance",
)
ds.attrs.update(global_atts)
ds = ds.transpose("y", "x", "band")
ds = ds.drop_vars(["wavelength"])
ds = ds.rename({"band": "wavelength", "band_data": "reflectance"})
ds.coords["wavelength"] = wavelength
ds.attrs["crs"] = ds.rio.crs.to_string()
ds.rio.write_transform(rio_transform)
if wavelengths is not None:
ds = ds.sel(wavelength=wavelengths, method=method, **kwargs)
return ds