From c4ca96f51409bd1be964fdfd0f3d075f66418afa Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Mon, 10 Jun 2024 14:17:43 -0400 Subject: [PATCH] Add support for changing band combinations and colormaps interactively (#46) * Add support for changing band combinations and colormaps interactively * Fix DESIS bug * Add demo to docs --- README.md | 5 ++ docs/examples/desis.ipynb | 4 +- docs/examples/ecostress.ipynb | 1 + docs/examples/emit.ipynb | 2 +- docs/index.md | 5 ++ hypercoast/desis.py | 41 ++++++++-------- hypercoast/emit.py | 7 ++- hypercoast/hypercoast.py | 88 ++++++++++++++++++++++++++++++++--- hypercoast/ui.py | 34 ++++++++++---- 9 files changed, 147 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 1d723be4..32c18645 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/examples/desis.ipynb b/docs/examples/desis.ipynb index 9868bbbd..03edda16 100644 --- a/docs/examples/desis.ipynb +++ b/docs/examples/desis.ipynb @@ -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" ] @@ -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" ] diff --git a/docs/examples/ecostress.ipynb b/docs/examples/ecostress.ipynb index f9d59065..1fc50dac 100644 --- a/docs/examples/ecostress.ipynb +++ b/docs/examples/ecostress.ipynb @@ -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" ] }, diff --git a/docs/examples/emit.ipynb b/docs/examples/emit.ipynb index a38f6d70..2b4f1a85 100644 --- a/docs/examples/emit.ipynb +++ b/docs/examples/emit.ipynb @@ -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" ] diff --git a/docs/index.md b/docs/index.md index 0e7ec044..11f3d107 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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) diff --git a/hypercoast/desis.py b/hypercoast/desis.py index fe82f73f..c53c8867 100644 --- a/hypercoast/desis.py +++ b/hypercoast/desis.py @@ -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. @@ -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 @@ -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): @@ -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 @@ -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"]}, diff --git a/hypercoast/emit.py b/hypercoast/emit.py index 22bde848..d12225e7 100644 --- a/hypercoast/emit.py +++ b/hypercoast/emit.py @@ -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 @@ -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}" @@ -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) @@ -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 diff --git a/hypercoast/hypercoast.py b/hypercoast/hypercoast.py index 84d051b7..286d171a 100644 --- a/hypercoast/hypercoast.py +++ b/hypercoast/hypercoast.py @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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) diff --git a/hypercoast/ui.py b/hypercoast/ui.py index 16e27107..d46be8b3 100644 --- a/hypercoast/ui.py +++ b/hypercoast/ui.py @@ -53,6 +53,10 @@ def __init__(self, host_map, stack=True, position="topright"): self._fig = fig self._host_map._fig = fig + layer_names = list(host_map.cog_layer_dict.keys()) + layers_widget = widgets.Dropdown(options=layer_names) + layers_widget.layout.width = "18ex" + close_btn = widgets.Button( icon="times", tooltip="Close the widget", @@ -67,6 +71,13 @@ def __init__(self, host_map, stack=True, position="topright"): layout=widgets.Layout(width="32px"), ) + settings_btn = widgets.Button( + icon="gear", + tooltip="Change layer settings", + button_style="primary", + layout=widgets.Layout(width="32px"), + ) + stack_btn = widgets.ToggleButton( value=stack, icon="area-chart", @@ -74,6 +85,14 @@ def __init__(self, host_map, stack=True, position="topright"): layout=widgets.Layout(width="32px"), ) + def settings_btn_click(_): + self._host_map._add_layer_editor( + position="topright", + layer_dict=self._host_map.cog_layer_dict[layers_widget.value], + ) + + settings_btn.on_click(settings_btn_click) + def reset_btn_click(_): if hasattr(self._host_map, "_plot_marker_cluster"): self._host_map._plot_marker_cluster.markers = [] @@ -134,10 +153,9 @@ def close_widget(_): close_btn.on_click(close_widget) - layer_names = list(host_map.cog_layer_dict.keys()) - layers_widget = widgets.Dropdown(options=layer_names) - layers_widget.layout.width = "18ex" - super().__init__([layers_widget, stack_btn, reset_btn, save_btn, close_btn]) + super().__init__( + [layers_widget, settings_btn, stack_btn, reset_btn, save_btn, close_btn] + ) output = widgets.Output() output_control = ipyleaflet.WidgetControl(widget=output, position="bottomright") @@ -153,7 +171,7 @@ def handle_interaction(**kwargs): latlon = kwargs.get("coordinates") lat = latlon[0] lon = latlon[1] - if kwargs.get("type") == "click": + if kwargs.get("type") == "click" and self._host_map._layer_editor is None: layer_name = layers_widget.value if not hasattr(self._host_map, "_plot_markers"): @@ -170,9 +188,9 @@ def handle_interaction(**kwargs): "reflectance" ] - if "wavelengths" not in self._host_map._spectral_data: - self._host_map._spectral_data["wavelengths"] = ds[ - "wavelengths" + if "wavelength" not in self._host_map._spectral_data: + self._host_map._spectral_data["wavelength"] = ds[ + "wavelength" ].values elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "PACE": try: