diff --git a/eodal/core/band.py b/eodal/core/band.py index 83ce314..ab81e11 100644 --- a/eodal/core/band.py +++ b/eodal/core/band.py @@ -1370,11 +1370,13 @@ def get_meta(self, driver: Optional[str] = "gTiff", **kwargs) -> Dict[str, Any]: name of the ``rasterio`` driver. `gTiff` (GeoTiff) by default :param kwargs: additional keyword arguments to append to metadata dictionary + or to overwrite defaults such as the "compress" attribute. :returns: ``rasterio`` compatible metadata dictionary to be used for writing new raster datasets """ meta = {} + # set defaults meta["height"] = self.nrows meta["width"] = self.ncols meta["crs"] = self.crs @@ -1387,6 +1389,8 @@ def get_meta(self, driver: Optional[str] = "gTiff", **kwargs) -> Dict[str, Any]: # "compress" as suggested here: # https://github.com/rasterio/rasterio/discussions/2933#discussioncomment-7208578 meta["compress"] = "DEFLATE" + + # defaults can be overwritten using custom kwargs meta.update(kwargs) return meta @@ -1517,7 +1521,7 @@ def hist( def plot( self, - colormap: Optional[str] = "gray", + colormap: Optional[str] = "viridis", discrete_values: Optional[bool] = False, user_defined_colors: Optional[ListedColormap] = None, user_defined_ticks: Optional[List[Union[str, int, float]]] = None, @@ -1532,7 +1536,7 @@ def plot( :param colormap: String identifying one of matplotlib's colormaps. - The default will plot the band in gray values. + The default will plot the band using the viridis colormap. :param discrete_values: if True (Default) assumes that the band has continuous values (i.e., ordinary spectral data). If False assumes that the @@ -2201,21 +2205,28 @@ def reduce( def scale_data( self, inplace: Optional[bool] = False, - pixel_values_to_ignore: Optional[List[Union[int, float]]] = None, + pixel_values_to_ignore: Optional[List[int | float]] = [], ): """ Applies scale and offset factors to the data. + .. versionadded:: 0.2.3 + No-data values are ignored when applying scale and offset. + :param inplace: if False (default) returns a copy of the ``Band`` instance with the changes applied. If True overwrites the values in the current instance. :param pixel_values_to_ignore: - optional list of pixel values (e.g., nodata values) to ignore, - i.e., where scaling has no effect + optional list of pixel values to ignore, i.e., where scaling + has no effect. From version 0.2.3 onwards, no-data values + are *always* ignored. :returns: ``Band`` instance if `inplace` is False, None instead. """ + # add no-data to the `pixel_values_to_ignore` list + pixel_values_to_ignore.append(self.nodata) + scale, offset = self.scale, self.offset if self.is_masked_array: if pixel_values_to_ignore is None: diff --git a/eodal/core/raster.py b/eodal/core/raster.py index a7b7023..17254f8 100644 --- a/eodal/core/raster.py +++ b/eodal/core/raster.py @@ -660,7 +660,7 @@ def _bands_from_selection( Selects bands in a multi-band raster dataset based on a custom selection of band indices or band names. - .. versionadd:: 0.2.0 + .. versionadded:: 0.2.0 works also with a dictionary of hrefs returned from a STAC query diff --git a/eodal/core/sensors/landsat.py b/eodal/core/sensors/landsat.py index 3ff321e..515d417 100644 --- a/eodal/core/sensors/landsat.py +++ b/eodal/core/sensors/landsat.py @@ -386,6 +386,7 @@ def from_usgs( RasterCollection containing the Landsat bands. """ # check band selection and determine the platform and sensor + _band_selection = deepcopy(band_selection) band_df = cls._preprocess_band_selection( cls, in_dir=in_dir, diff --git a/eodal/core/sensors/sentinel2.py b/eodal/core/sensors/sentinel2.py index fdfe497..d8e3c07 100644 --- a/eodal/core/sensors/sentinel2.py +++ b/eodal/core/sensors/sentinel2.py @@ -31,6 +31,7 @@ import geopandas as gpd import rasterio as rio +from copy import deepcopy from matplotlib.pyplot import Figure from matplotlib import colors from numbers import Number @@ -157,7 +158,7 @@ def _get_band_files( if processing_level == ProcessingLevels.L1C: is_l2a = False - # check if SCL should e read (L2A) + # check if SCL should be read (L2A) if is_l2a and read_scl: scl_in_selection = "scl" in band_selection or "SCL" in band_selection if not scl_in_selection: @@ -209,6 +210,20 @@ def _process_band_selection( for band in bands_to_exclude: band_selection.remove(band) + # check if band or color names are passed + color_names = set(band_selection).issubset(s2_band_mapping.values()) + band_names = set(band_selection).issubset(s2_band_mapping.keys()) + + if not color_names and not band_names: + raise BandNotFoundError( + f'Invalid selection of bands: {band_selection}') + + # internally, we use band names only. So we have to map the color names + # back to their band names + if color_names: + band_selection = [ + k for k, v in s2_band_mapping.items() if v in band_selection] + # determine which spatial resolutions are selected and check processing level band_df_safe = self._get_band_files( in_dir=in_dir, band_selection=band_selection, read_scl=read_scl @@ -224,7 +239,7 @@ def _process_band_selection( if bands_not_found[0] == "SCL": return band_df_safe raise BandNotFoundError( - f"Couldnot find bands {bands_not_found} " "provided in selection" + f"Could not find bands {bands_not_found} " "provided in selection" ) return band_df_safe @@ -280,8 +295,9 @@ def from_safe( `Sentinel2` instance with S2 bands loaded """ # load 10 and 20 bands by default + _band_selection = deepcopy(band_selection) band_df_safe = cls._process_band_selection( - cls, in_dir=in_dir, band_selection=band_selection, read_scl=read_scl + cls, in_dir=in_dir, band_selection=_band_selection, read_scl=read_scl ) # check the clipping extent of the raster with the lowest (coarsest) spatial diff --git a/tests/core/test_sentinel2.py b/tests/core/test_sentinel2.py index 2f7b042..4ae59a2 100644 --- a/tests/core/test_sentinel2.py +++ b/tests/core/test_sentinel2.py @@ -12,7 +12,7 @@ from eodal.config import get_settings from eodal.core.sensors import Sentinel2 from eodal.core.band import Band -from eodal.utils.exceptions import BandNotFoundError, InputError +from eodal.utils.exceptions import BandNotFoundError settings = get_settings() settings.USE_STAC = False @@ -251,7 +251,7 @@ def test_ignore_scl(datadir, get_s2_safe_l2a, get_polygons_2): def test_band_selections(datadir, get_s2_safe_l2a, get_polygons_2): - """testing invalid band selections""" + """testing valid and invalid band selections""" in_dir = get_s2_safe_l2a() in_file_aoi = get_polygons_2() @@ -265,6 +265,20 @@ def test_band_selections(datadir, get_s2_safe_l2a, get_polygons_2): band_selection=['B02', 'B13'] ) + # test with color names instead of band names + band_selection = ['red', 'green', 'blue'] + ds = Sentinel2.from_safe( + in_dir=in_dir, + band_selection=band_selection, + read_scl=False, + apply_scaling=False + ) + assert ds.band_names == ['B02', 'B03', 'B04'] + assert set(ds.band_aliases) == set(band_selection) + assert (ds['B02'] == ds['blue']).values.all() + assert (ds['B03'] == ds['green']).values.all() + assert (ds['B04'] == ds['red']).values.all() + @pytest.mark.skip(reason='too heavy test for Github workflows') def test_read_from_safe_l2a(datadir, get_s2_safe_l2a):