Skip to content

Commit

Permalink
Add support for changing band combinations and colormaps interactively (
Browse files Browse the repository at this point in the history
#46)

* Add support for changing band combinations and colormaps interactively

* Fix DESIS bug

* Add demo to docs
  • Loading branch information
giswqs authored Jun 10, 2024
1 parent 7344462 commit c4ca96f
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 40 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@
- Interactive visualization and analysis of hyperspectral data, such as [AVIRIS](https://aviris.jpl.nasa.gov), [DESIS](https://www.earthdata.nasa.gov/s3fs-public/imported/DESIS_TCloud_Mar0421.pdf), [EMIT](https://earth.jpl.nasa.gov/emit), [PACE](https://pace.gsfc.nasa.gov), [NEON AOP](https://data.neonscience.org/data-products/DP3.30006.001)
- Interactive visualization of NASA [ECOSTRESS](https://ecostress.jpl.nasa.gov) data
- Interactive extraction and visualization of spectral signatures
- Changing band combinations and colormaps interactively
- Saving spectral signatures as CSV files

## Demos

- Changing band combinations and colormaps interactively

![colormap](https://i.imgur.com/jYItN4D.gif)

- Visualizing NASA [AVIRIS](https://aviris.jpl.nasa.gov) hyperspectral data interactively

![AVIRIS](https://i.imgur.com/RdegGqx.gif)
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/desis.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"source": [
"m = hypercoast.Map()\n",
"m.add_basemap(\"Hybrid\")\n",
"m.add_desis(filepath, bands=[200], vmin=0, vmax=5000, nodata=0, colormap=\"jet\")\n",
"m.add_desis(filepath, wavelengths=[1000], vmin=0, vmax=5000, nodata=0, colormap=\"jet\")\n",
"m.add_colormap(cmap=\"jet\", vmin=0, vmax=0.5, label=\"Reflectance\")\n",
"m"
]
Expand All @@ -114,7 +114,7 @@
"source": [
"m = hypercoast.Map()\n",
"m.add_basemap(\"Hybrid\")\n",
"m.add_desis(filepath, bands=[50, 100, 200], vmin=0, vmax=1000, nodata=0)\n",
"m.add_desis(filepath, wavelengths=[900, 600, 525], vmin=0, vmax=1000, nodata=0)\n",
"m.add(\"spectral\")\n",
"m"
]
Expand Down
1 change: 1 addition & 0 deletions docs/examples/ecostress.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
"m = hypercoast.Map()\n",
"m.add_basemap(\"HYBRID\")\n",
"m.add_raster(filepath, colormap=\"jet\", layer_name=\"LST\")\n",
"m.add(\"spectral\")\n",
"m"
]
},
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/emit.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"source": [
"m = hypercoast.Map()\n",
"m.add_basemap(\"SATELLITE\")\n",
"m.add_emit(dataset, wavelengths=[500, 600, 1000], indexes=[3, 2, 1], layer_name=\"EMIT\")\n",
"m.add_emit(dataset, wavelengths=[1000, 600, 500], vmin=0, vmax=0.3, layer_name=\"EMIT\")\n",
"m.add(\"spectral\")\n",
"m"
]
Expand Down
5 changes: 5 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@
- Interactive visualization and analysis of hyperspectral data, such as [AVIRIS](https://aviris.jpl.nasa.gov), [DESIS](https://www.earthdata.nasa.gov/s3fs-public/imported/DESIS_TCloud_Mar0421.pdf), [EMIT](https://earth.jpl.nasa.gov/emit), [PACE](https://pace.gsfc.nasa.gov), [NEON AOP](https://data.neonscience.org/data-products/DP3.30006.001)
- Interactive visualization of NASA [ECOSTRESS](https://ecostress.jpl.nasa.gov) data
- Interactive extraction and visualization of spectral signatures
- Changing band combinations and colormaps interactively
- Saving spectral signatures as CSV files

## Demos

- Changing band combinations and colormaps interactively

![colormap](https://i.imgur.com/jYItN4D.gif)

- Visualizing NASA [AVIRIS](https://aviris.jpl.nasa.gov) hyperspectral data interactively

![AVIRIS](https://i.imgur.com/RdegGqx.gif)
Expand Down
41 changes: 21 additions & 20 deletions hypercoast/desis.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
from .common import convert_coords


def read_desis(filepath, bands=None, method="nearest", **kwargs):
def read_desis(filepath, wavelengths=None, method="nearest", **kwargs):
"""
Reads DESIS data from a given file and returns an xarray Dataset.
Args:
filepath (str): Path to the file to read.
bands (array-like, optional): Specific bands to select. If None, all
bands are selected.
method (str, optional): Method to use for selection when bands is not
wavelengths (array-like, optional): Specific wavelengths to select. If
None, all wavelengths are selected.
method (str, optional): Method to use for selection when wavelengths is not
None. Defaults to "nearest".
**kwargs: Additional keyword arguments to pass to the `sel` method when
bands is not None.
Expand All @@ -26,30 +26,31 @@ def read_desis(filepath, bands=None, method="nearest", **kwargs):
xr.Dataset: An xarray Dataset containing the DESIS data.
"""

url = "https://github.com/opengeos/datasets/releases/download/hypercoast/desis_wavelengths.csv"
df = pd.read_csv(url)
dataset = xr.open_dataset(filepath)
dataset = dataset.rename(
{"band": "wavelength", "band_data": "reflectance"}
).transpose("y", "x", "wavelength")
dataset["wavelength"] = df["wavelength"].tolist()

if bands is not None:
dataset = dataset.sel(band=bands, method=method, **kwargs)
if wavelengths is not None:
dataset = dataset.sel(wavelength=wavelengths, method=method, **kwargs)

dataset = dataset.rename({"band_data": "reflectance"})
url = "https://github.com/opengeos/datasets/releases/download/hypercoast/desis_wavelengths.csv"
df = pd.read_csv(url)
wavelengths = df["wavelength"].tolist()
dataset.attrs["wavelengths"] = wavelengths
dataset.attrs["crs"] = dataset.rio.crs.to_string()

return dataset


def desis_to_image(dataset, bands=None, method="nearest", output=None, **kwargs):
def desis_to_image(dataset, wavelengths=None, method="nearest", output=None, **kwargs):
"""
Converts an DESIS dataset to an image.
Args:
dataset (xarray.Dataset or str): The dataset containing the DESIS data
or the file path to the dataset.
bands (array-like, optional): The specific bands to select. If None, all
bands are selected. Defaults to None.
wavelengths (array-like, 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
Expand All @@ -67,10 +68,12 @@ def desis_to_image(dataset, bands=None, method="nearest", output=None, **kwargs)
if isinstance(dataset, str):
dataset = read_desis(dataset, method=method)

if bands is not None:
dataset = dataset.sel(band=bands, method=method)
if wavelengths is not None:
dataset = dataset.sel(wavelength=wavelengths, method=method)

return array_to_image(dataset["reflectance"], output=output, **kwargs)
return array_to_image(
dataset["reflectance"], output=output, transpose=False, **kwargs
)


def extract_desis(ds, lat, lon):
Expand All @@ -93,7 +96,7 @@ def extract_desis(ds, lat, lon):
values = ds.sel(x=x, y=y, method="nearest")["reflectance"].values / 10000

da = xr.DataArray(
values, dims=["wavelength"], coords={"wavelength": ds.attrs["wavelengths"]}
values, dims=["wavelength"], coords={"wavelength": ds.coords["wavelength"]}
)

return da
Expand Down Expand Up @@ -143,8 +146,6 @@ def filter_desis(dataset, lat, lon, return_plot=False, **kwargs):
print(x_min, y_min, x_max, y_max)
da = dataset.sel(x=slice(x_min, x_max), y=slice(y_min, y_max))["reflectance"]

wavelengths = dataset.attrs["wavelengths"]

if return_plot:
rrs_stack = da.stack(
{"pixel": ["latitude", "longitude"]},
Expand Down
7 changes: 5 additions & 2 deletions hypercoast/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def read_emit(filepath, ortho=True, wavelengths=None, method="nearest", **kwargs
if wavelengths is not None:
ds = ds.sel(wavelengths=wavelengths, method=method)

ds = ds.rename({"wavelengths": "wavelength"})
return ds


Expand Down Expand Up @@ -187,7 +188,7 @@ def viz_emit(

if isinstance(ds, str):
ds = read_emit(ds, ortho=ortho)
example = ds.sel(wavelengths=wavelengths, method=method)
example = ds.sel(wavelength=wavelengths, method=method)

if title is None:
title = f"Reflectance at {example.wavelengths.values:.3f} {example.wavelengths.units}"
Expand Down Expand Up @@ -248,7 +249,7 @@ def emit_to_image(data, wavelengths=None, method="nearest", output=None, **kwarg
ds = data["reflectance"]

if wavelengths is not None:
ds = ds.sel(wavelengths=wavelengths, method=method)
ds = ds.sel(wavelength=wavelengths, method=method)
return array_to_image(ds, transpose=False, output=output, **kwargs)


Expand Down Expand Up @@ -354,6 +355,8 @@ def emit_xarray(

if wavelengths is not None:
out_xr = out_xr.sel(wavelengths=wavelengths, method=method)

out_xr = out_xr.rename({"wavelengths": "wavelength"})
return out_xr


Expand Down
88 changes: 81 additions & 7 deletions hypercoast/hypercoast.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ def add_emit(

self.cog_layer_dict[layer_name]["xds"] = xds
self.cog_layer_dict[layer_name]["hyper"] = "EMIT"
self._update_band_names(layer_name, wavelengths)

def add_pace(
self,
Expand Down Expand Up @@ -320,11 +321,12 @@ def add_pace(

self.cog_layer_dict[layer_name]["xds"] = source
self.cog_layer_dict[layer_name]["hyper"] = "PACE"
self._update_band_names(layer_name, wavelengths)

def add_desis(
self,
source,
bands=[50, 100, 200],
wavelengths=[900, 650, 525],
indexes=None,
colormap="jet",
vmin=None,
Expand Down Expand Up @@ -378,19 +380,19 @@ def add_desis(

source = read_desis(source)

image = desis_to_image(source, bands=bands, method=method)
image = desis_to_image(source, wavelengths=wavelengths, method=method)

if isinstance(bands, list) and len(bands) > 1:
if isinstance(wavelengths, list) and len(wavelengths) > 1:
colormap = None

if isinstance(bands, int):
bands = [bands]
if isinstance(wavelengths, int):
wavelengths = [wavelengths]

if indexes is None:
if isinstance(bands, list) and len(bands) == 1:
if isinstance(wavelengths, list) and len(wavelengths) == 1:
indexes = [1]
else:
indexes = [3, 2, 1]
indexes = [1, 2, 3]

self.add_raster(
image,
Expand All @@ -409,6 +411,7 @@ def add_desis(

self.cog_layer_dict[layer_name]["xds"] = source
self.cog_layer_dict[layer_name]["hyper"] = "DESIS"
self._update_band_names(layer_name, wavelengths)

def add_neon(
self,
Expand Down Expand Up @@ -491,6 +494,7 @@ def add_neon(

self.cog_layer_dict[layer_name]["xds"] = xds
self.cog_layer_dict[layer_name]["hyper"] = "NEON"
self._update_band_names(layer_name, wavelengths)

def add_aviris(
self,
Expand Down Expand Up @@ -574,6 +578,34 @@ def add_aviris(
xds.attrs["bounds"] = self.cog_layer_dict[layer_name]["bounds"]
self.cog_layer_dict[layer_name]["xds"] = xds
self.cog_layer_dict[layer_name]["hyper"] = "AVIRIS"
self._update_band_names(layer_name, wavelengths)

def add_hyper(self, xds, type, wvl_indexes=None, **kwargs):
"""Add a hyperspectral dataset to the map.
Args:
xds (str): The Xarray dataset containing the hyperspectral data.
type (str): The type of the hyperspectral dataset. Can be one of
"EMIT", "PACE", "DESIS", "NEON", "AVIRIS".
**kwargs: Additional keyword arguments to pass to the corresponding
add function.
"""

if wvl_indexes is not None:
kwargs["wavelengths"] = (
xds.isel(wavelength=wvl_indexes).coords["wavelength"].values.tolist()
)

if type == "EMIT":
self.add_emit(xds, **kwargs)
elif type == "PACE":
self.add_pace(xds, **kwargs)
elif type == "DESIS":
self.add_desis(xds, **kwargs)
elif type == "NEON":
self.add_neon(xds, **kwargs)
elif type == "AVIRIS":
self.add_aviris(xds, **kwargs)

def set_plot_options(
self,
Expand Down Expand Up @@ -653,3 +685,45 @@ def spectral_to_csv(self, filename, index=True, **kwargs):
df = self.spectral_to_df()
df = df.rename_axis("band")
df.to_csv(filename, index=index, **kwargs)

def _update_band_names(self, layer_name, wavelengths):

# Function to find the nearest indices
def find_nearest_indices(
dataarray, selected_wavelengths, dim_name="wavelength"
):
indices = []
for wavelength in selected_wavelengths:
if dim_name == "band":
nearest_wavelength = dataarray.sel(
band=wavelength, method="nearest"
)
else:
nearest_wavelength = dataarray.sel(
wavelength=wavelength, method="nearest"
)
nearest_wavelength_index = nearest_wavelength[dim_name].item()
nearest_index = (
dataarray[dim_name].values.tolist().index(nearest_wavelength_index)
)
indices.append(nearest_index + 1)
return indices

if "xds" in self.cog_layer_dict[layer_name]:
xds = self.cog_layer_dict[layer_name]["xds"]
dim_name = "wavelength"

if "band" in xds:
dim_name = "band"

band_count = xds.dims[dim_name]
band_names = ["b" + str(band) for band in range(1, band_count + 1)]
self.cog_layer_dict[layer_name]["band_names"] = band_names

try:
indexes = find_nearest_indices(xds, wavelengths, dim_name=dim_name)
vis_bands = ["b" + str(index) for index in indexes]
self.cog_layer_dict[layer_name]["indexes"] = indexes
self.cog_layer_dict[layer_name]["vis_bands"] = vis_bands
except Exception as e:
print(e)
Loading

0 comments on commit c4ca96f

Please sign in to comment.