Module eoreader.products.optical.optical_product

Super class for optical products

Expand source code
# -*- coding: utf-8 -*-
# Copyright 2021, SERTIT-ICube - France, https://sertit.unistra.fr/
# This file is part of eoreader project
#     https://github.com/sertit/eoreader
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Super class for optical products """

import logging
import os
from abc import abstractmethod
from typing import Union

import geopandas as gpd
import numpy as np
import rasterio
from rasterio import crs as riocrs
from rasterio.enums import Resampling

from eoreader.bands import index
from eoreader.bands.alias import (
    is_clouds,
    is_dem,
    is_index,
    is_optical_band,
    is_sar_band,
)
from eoreader.bands.bands import BandNames
from eoreader.bands.bands import OpticalBandNames as obn
from eoreader.bands.bands import OpticalBands
from eoreader.exceptions import InvalidBandError, InvalidIndexError
from eoreader.products.product import Product, SensorType
from eoreader.utils import EOREADER_NAME
from sertit import misc, rasters, strings
from sertit.rasters import XDS_TYPE
from sertit.snap import MAX_CORES

LOGGER = logging.getLogger(EOREADER_NAME)


class OpticalProduct(Product):
    """Super class for optical products"""

    def _post_init(self) -> None:
        """
        Function used to post_init the products
        (setting sensor type, band names and so on)
        """
        self.band_names = OpticalBands()
        self._set_product_type()
        self.sensor_type = SensorType.OPTICAL

    def get_default_band(self) -> BandNames:
        """
        Get default band: `GREEN` for optical data as every optical satellite has a GREEN band.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_default_band()
        <OpticalBandNames.GREEN: 'GREEN'>
        ```

        Returns:
            str: Default band
        """
        return obn.GREEN

    def get_default_band_path(self) -> str:
        """
        Get default band (`GREEN` for optical data) path.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_default_band_path()
        'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B03.jp2'
        ```

        Returns:
            str: Default band path
        """
        default_band = self.get_default_band()
        return self.get_band_paths([default_band])[default_band]

    def crs(self) -> riocrs.CRS:
        """
        Get UTM projection of the tile

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.utm_crs()
        CRS.from_epsg(32630)
        ```

        Returns:
            rasterio.crs.CRS: CRS object
        """
        band_path = self.get_default_band_path()
        with rasterio.open(band_path) as dst:
            utm = dst.crs

        return utm

    def extent(self) -> gpd.GeoDataFrame:
        """
        Get UTM extent of the tile

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.utm_extent()
                                                    geometry
        0  POLYGON ((309780.000 4390200.000, 309780.000 4...
        ```

        Returns:
            gpd.GeoDataFrame: Footprint in UTM
        """
        # Get extent
        return rasters.get_extent(self.get_default_band_path())

    def get_existing_bands(self) -> list:
        """
        Return the existing band paths.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_existing_bands()
        [<OpticalBandNames.CA: 'COASTAL_AEROSOL'>,
        <OpticalBandNames.BLUE: 'BLUE'>,
        <OpticalBandNames.GREEN: 'GREEN'>,
        <OpticalBandNames.RED: 'RED'>,
        <OpticalBandNames.VRE_1: 'VEGETATION_RED_EDGE_1'>,
        <OpticalBandNames.VRE_2: 'VEGETATION_RED_EDGE_2'>,
        <OpticalBandNames.VRE_3: 'VEGETATION_RED_EDGE_3'>,
        <OpticalBandNames.NIR: 'NIR'>,
        <OpticalBandNames.NNIR: 'NARROW_NIR'>,
        <OpticalBandNames.WV: 'WATER_VAPOUR'>,
        <OpticalBandNames.CIRRUS: 'CIRRUS'>,
        <OpticalBandNames.SWIR_1: 'SWIR_1'>,
        <OpticalBandNames.SWIR_2: 'SWIR_2'>]
        ```

        Returns:
            list: List of existing bands in the products
        """
        return [name for name, nb in self.band_names.items() if nb]

    def get_existing_band_paths(self) -> dict:
        """
        Return the existing band paths.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_existing_band_paths()
        {
            <OpticalBandNames.CA: 'COASTAL_AEROSOL'>: 'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B01.jp2',
            ...,
            <OpticalBandNames.SWIR_2: 'SWIR_2'>: 'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B12.jp2'
        }
        ```

        Returns:
            dict: Dictionary containing the path of each queried band
        """
        existing_bands = self.get_existing_bands()
        return self.get_band_paths(band_list=existing_bands)

    def _open_bands(
        self,
        band_paths: dict,
        resolution: float = None,
        size: Union[list, tuple] = None,
    ) -> dict:
        """
        Open bands from disk.

        Args:
            band_paths (dict): Band dict: {band_enum: band_path}
            resolution (float): Band resolution in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.

        Returns:
            dict: Dictionary {band_name, band_xarray}

        """
        # Open bands and get array (resampled if needed)
        band_arrays = {}
        for band_name, band_path in band_paths.items():
            # Read band
            band_arrays[band_name] = self._read_band(
                band_path, resolution=resolution, size=size
            )
            band_arrays[band_name] = self._manage_invalid_pixels(
                band_arrays[band_name], band_name, resolution=resolution, size=size
            )

        return band_arrays

    # pylint: disable=R0913
    # R0913: Too many arguments (6/5) (too-many-arguments)
    @abstractmethod
    def _manage_invalid_pixels(
        self,
        band_arr: XDS_TYPE,
        band: obn,
        resolution: float = None,
        size: Union[list, tuple] = None,
    ) -> XDS_TYPE:
        """
        Manage invalid pixels (Nodata, saturated, defective...)

        Args:
            band_arr (XDS_TYPE): Band array
            band (obn): Band name as an OpticalBandNames
            resolution (float): Band resolution in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.

        Returns:
            XDS_TYPE: Cleaned band array
        """
        raise NotImplementedError("This method should be implemented by a child class")

    def _set_nodata_mask(self, band_arr: XDS_TYPE, mask: np.ndarray) -> XDS_TYPE:
        """
        Create the correct xarray with well positioned nodata

        Args:
            band_arr (XDS_TYPE): Band array
            mask (np.ndarray): Mask array

        Returns:
            (XDS_TYPE): Corrected band array
        """
        # Binary mask
        if mask.dtype != np.uint8:
            mask = mask.astype(np.uint8)

        if len(mask.shape) < len(band_arr.shape):
            mask = np.expand_dims(mask, axis=0)

        # Set masked values to nodata
        return band_arr.where(mask == 0, np.nan)

    def _load(
        self, bands: list, resolution: float = None, size: Union[list, tuple] = None
    ) -> dict:
        """
        Core function loading optical data bands

        Args:
            bands (list): Band list
            resolution (float): Resolution of the band, in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.

        Returns:
            Dictionary {band_name, band_xarray}
        """
        band_list = []
        index_list = []
        dem_list = []
        clouds_list = []

        # Check if everything is valid
        for idx_or_band in bands:
            if is_index(idx_or_band):
                if self._has_index(idx_or_band):
                    index_list.append(idx_or_band)
                else:
                    raise InvalidIndexError(
                        f"{idx_or_band} cannot be computed from {self.condensed_name}."
                    )
            elif is_sar_band(idx_or_band):
                raise TypeError(
                    f"You should ask for Optical bands as {self.name} is an optical product."
                )
            elif is_optical_band(idx_or_band):
                if self.has_band(idx_or_band):
                    band_list.append(idx_or_band)
                else:
                    raise InvalidBandError(
                        f"{idx_or_band} cannot be retrieved from {self.condensed_name}."
                    )
            elif is_dem(idx_or_band):
                dem_list.append(idx_or_band)
            elif is_clouds(idx_or_band):
                clouds_list.append(idx_or_band)

        # Check if DEM is set and exists
        if dem_list:
            self._check_dem_path()

        # Get all bands to be open
        bands_to_load = band_list.copy()
        for idx in index_list:
            bands_to_load += index.NEEDED_BANDS[idx]

        # Load band arrays (only keep unique bands: open them only one time !)
        bands = self._load_bands(
            list(set(bands_to_load)), resolution=resolution, size=size
        )

        # Compute index (they conserve the nodata)
        bands_dict = {idx: idx(bands) for idx in index_list}

        # Add bands
        bands_dict.update({band: bands[band] for band in band_list})

        # Add DEM
        bands_dict.update(self._load_dem(dem_list, resolution=resolution, size=size))

        # Add Clouds
        bands_dict.update(
            self._load_clouds(clouds_list, resolution=resolution, size=size)
        )

        return bands_dict

    @abstractmethod
    def get_mean_sun_angles(self) -> (float, float):
        """
        Get Mean Sun angles (Azimuth and Zenith angles)

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_mean_sun_angles()
        (149.148155074489, 32.6627897525474)
        ```

        Returns:
            (float, float): Mean Azimuth and Zenith angle
        """
        raise NotImplementedError("This method should be implemented by a child class")

    def _compute_hillshade(
        self,
        dem_path: str = "",
        resolution: Union[float, tuple] = None,
        size: Union[list, tuple] = None,
        resampling: Resampling = Resampling.bilinear,
    ) -> str:
        """
        Compute Hillshade mask

        Args:
            dem_path (str): DEM path, using EUDEM/MERIT DEM if none
            resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution.
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.
            resampling (Resampling): Resampling method

        Returns:
            str: Hillshade mask path

        """
        # Warp DEM
        warped_dem_path = self._warp_dem(dem_path, resolution, size, resampling)

        # Get Hillshade path
        hillshade_dem = os.path.join(
            self.output, f"{self.condensed_name}_HILLSHADE.tif"
        )
        if os.path.isfile(hillshade_dem):
            LOGGER.debug(
                "Already existing hillshade DEM for %s. Skipping process.", self.name
            )
        else:
            LOGGER.debug("Computing hillshade DEM for %s", self.name)

            # Get angles
            mean_azimuth_angle, mean_zenith_angle = self.get_mean_sun_angles()
            zenith = 90.0 - mean_zenith_angle
            azimuth = mean_azimuth_angle

            # Run cmd
            cmd_hillshade = [
                "gdaldem",
                "--config",
                "NUM_THREADS",
                MAX_CORES,
                "hillshade",
                strings.to_cmd_string(warped_dem_path),
                "-compute_edges",
                "-z",
                "1",
                "-az",
                azimuth,
                "-alt",
                zenith,
                "-of",
                "GTiff",
                strings.to_cmd_string(hillshade_dem),
            ]
            # Run command
            misc.run_cli(cmd_hillshade)

        return hillshade_dem

    def _load_clouds(
        self, bands: list, resolution: float = None, size: Union[list, tuple] = None
    ) -> dict:
        """
        Load cloud files as numpy arrays with the same resolution (and same metadata).

        Args:
            bands (list): List of the wanted bands
            resolution (int): Band resolution in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.
        Returns:
            dict: Dictionary {band_name, band_xarray}
        """
        raise NotImplementedError("This method should be implemented by a child class")

    def _create_mask(
        self, xds: XDS_TYPE, cond: np.ndarray, nodata: np.ndarray
    ) -> XDS_TYPE:
        """
        Create a mask from a conditional array and a nodata mask.

        Args:
            xds (XDS_TYPE): xarray to retrieve attributes
            cond (np.ndarray): Conditional array
            nodata (np.ndarray): Nodata mask

        Returns:
            XDS_TYPE: Mask as xarray
        """
        mask = xds.copy(data=np.where(cond, self._mask_true, self._mask_false))
        mask = mask.where(nodata == 0, np.nan)

        return mask

Classes

class OpticalProduct (product_path, archive_path=None, output_path=None)

Super class for optical products

Expand source code
class OpticalProduct(Product):
    """Super class for optical products"""

    def _post_init(self) -> None:
        """
        Function used to post_init the products
        (setting sensor type, band names and so on)
        """
        self.band_names = OpticalBands()
        self._set_product_type()
        self.sensor_type = SensorType.OPTICAL

    def get_default_band(self) -> BandNames:
        """
        Get default band: `GREEN` for optical data as every optical satellite has a GREEN band.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_default_band()
        <OpticalBandNames.GREEN: 'GREEN'>
        ```

        Returns:
            str: Default band
        """
        return obn.GREEN

    def get_default_band_path(self) -> str:
        """
        Get default band (`GREEN` for optical data) path.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_default_band_path()
        'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B03.jp2'
        ```

        Returns:
            str: Default band path
        """
        default_band = self.get_default_band()
        return self.get_band_paths([default_band])[default_band]

    def crs(self) -> riocrs.CRS:
        """
        Get UTM projection of the tile

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.utm_crs()
        CRS.from_epsg(32630)
        ```

        Returns:
            rasterio.crs.CRS: CRS object
        """
        band_path = self.get_default_band_path()
        with rasterio.open(band_path) as dst:
            utm = dst.crs

        return utm

    def extent(self) -> gpd.GeoDataFrame:
        """
        Get UTM extent of the tile

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.utm_extent()
                                                    geometry
        0  POLYGON ((309780.000 4390200.000, 309780.000 4...
        ```

        Returns:
            gpd.GeoDataFrame: Footprint in UTM
        """
        # Get extent
        return rasters.get_extent(self.get_default_band_path())

    def get_existing_bands(self) -> list:
        """
        Return the existing band paths.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_existing_bands()
        [<OpticalBandNames.CA: 'COASTAL_AEROSOL'>,
        <OpticalBandNames.BLUE: 'BLUE'>,
        <OpticalBandNames.GREEN: 'GREEN'>,
        <OpticalBandNames.RED: 'RED'>,
        <OpticalBandNames.VRE_1: 'VEGETATION_RED_EDGE_1'>,
        <OpticalBandNames.VRE_2: 'VEGETATION_RED_EDGE_2'>,
        <OpticalBandNames.VRE_3: 'VEGETATION_RED_EDGE_3'>,
        <OpticalBandNames.NIR: 'NIR'>,
        <OpticalBandNames.NNIR: 'NARROW_NIR'>,
        <OpticalBandNames.WV: 'WATER_VAPOUR'>,
        <OpticalBandNames.CIRRUS: 'CIRRUS'>,
        <OpticalBandNames.SWIR_1: 'SWIR_1'>,
        <OpticalBandNames.SWIR_2: 'SWIR_2'>]
        ```

        Returns:
            list: List of existing bands in the products
        """
        return [name for name, nb in self.band_names.items() if nb]

    def get_existing_band_paths(self) -> dict:
        """
        Return the existing band paths.

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_existing_band_paths()
        {
            <OpticalBandNames.CA: 'COASTAL_AEROSOL'>: 'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B01.jp2',
            ...,
            <OpticalBandNames.SWIR_2: 'SWIR_2'>: 'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B12.jp2'
        }
        ```

        Returns:
            dict: Dictionary containing the path of each queried band
        """
        existing_bands = self.get_existing_bands()
        return self.get_band_paths(band_list=existing_bands)

    def _open_bands(
        self,
        band_paths: dict,
        resolution: float = None,
        size: Union[list, tuple] = None,
    ) -> dict:
        """
        Open bands from disk.

        Args:
            band_paths (dict): Band dict: {band_enum: band_path}
            resolution (float): Band resolution in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.

        Returns:
            dict: Dictionary {band_name, band_xarray}

        """
        # Open bands and get array (resampled if needed)
        band_arrays = {}
        for band_name, band_path in band_paths.items():
            # Read band
            band_arrays[band_name] = self._read_band(
                band_path, resolution=resolution, size=size
            )
            band_arrays[band_name] = self._manage_invalid_pixels(
                band_arrays[band_name], band_name, resolution=resolution, size=size
            )

        return band_arrays

    # pylint: disable=R0913
    # R0913: Too many arguments (6/5) (too-many-arguments)
    @abstractmethod
    def _manage_invalid_pixels(
        self,
        band_arr: XDS_TYPE,
        band: obn,
        resolution: float = None,
        size: Union[list, tuple] = None,
    ) -> XDS_TYPE:
        """
        Manage invalid pixels (Nodata, saturated, defective...)

        Args:
            band_arr (XDS_TYPE): Band array
            band (obn): Band name as an OpticalBandNames
            resolution (float): Band resolution in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.

        Returns:
            XDS_TYPE: Cleaned band array
        """
        raise NotImplementedError("This method should be implemented by a child class")

    def _set_nodata_mask(self, band_arr: XDS_TYPE, mask: np.ndarray) -> XDS_TYPE:
        """
        Create the correct xarray with well positioned nodata

        Args:
            band_arr (XDS_TYPE): Band array
            mask (np.ndarray): Mask array

        Returns:
            (XDS_TYPE): Corrected band array
        """
        # Binary mask
        if mask.dtype != np.uint8:
            mask = mask.astype(np.uint8)

        if len(mask.shape) < len(band_arr.shape):
            mask = np.expand_dims(mask, axis=0)

        # Set masked values to nodata
        return band_arr.where(mask == 0, np.nan)

    def _load(
        self, bands: list, resolution: float = None, size: Union[list, tuple] = None
    ) -> dict:
        """
        Core function loading optical data bands

        Args:
            bands (list): Band list
            resolution (float): Resolution of the band, in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.

        Returns:
            Dictionary {band_name, band_xarray}
        """
        band_list = []
        index_list = []
        dem_list = []
        clouds_list = []

        # Check if everything is valid
        for idx_or_band in bands:
            if is_index(idx_or_band):
                if self._has_index(idx_or_band):
                    index_list.append(idx_or_band)
                else:
                    raise InvalidIndexError(
                        f"{idx_or_band} cannot be computed from {self.condensed_name}."
                    )
            elif is_sar_band(idx_or_band):
                raise TypeError(
                    f"You should ask for Optical bands as {self.name} is an optical product."
                )
            elif is_optical_band(idx_or_band):
                if self.has_band(idx_or_band):
                    band_list.append(idx_or_band)
                else:
                    raise InvalidBandError(
                        f"{idx_or_band} cannot be retrieved from {self.condensed_name}."
                    )
            elif is_dem(idx_or_band):
                dem_list.append(idx_or_band)
            elif is_clouds(idx_or_band):
                clouds_list.append(idx_or_band)

        # Check if DEM is set and exists
        if dem_list:
            self._check_dem_path()

        # Get all bands to be open
        bands_to_load = band_list.copy()
        for idx in index_list:
            bands_to_load += index.NEEDED_BANDS[idx]

        # Load band arrays (only keep unique bands: open them only one time !)
        bands = self._load_bands(
            list(set(bands_to_load)), resolution=resolution, size=size
        )

        # Compute index (they conserve the nodata)
        bands_dict = {idx: idx(bands) for idx in index_list}

        # Add bands
        bands_dict.update({band: bands[band] for band in band_list})

        # Add DEM
        bands_dict.update(self._load_dem(dem_list, resolution=resolution, size=size))

        # Add Clouds
        bands_dict.update(
            self._load_clouds(clouds_list, resolution=resolution, size=size)
        )

        return bands_dict

    @abstractmethod
    def get_mean_sun_angles(self) -> (float, float):
        """
        Get Mean Sun angles (Azimuth and Zenith angles)

        ```python
        >>> from eoreader.reader import Reader
        >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
        >>> prod = Reader().open(path)
        >>> prod.get_mean_sun_angles()
        (149.148155074489, 32.6627897525474)
        ```

        Returns:
            (float, float): Mean Azimuth and Zenith angle
        """
        raise NotImplementedError("This method should be implemented by a child class")

    def _compute_hillshade(
        self,
        dem_path: str = "",
        resolution: Union[float, tuple] = None,
        size: Union[list, tuple] = None,
        resampling: Resampling = Resampling.bilinear,
    ) -> str:
        """
        Compute Hillshade mask

        Args:
            dem_path (str): DEM path, using EUDEM/MERIT DEM if none
            resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution.
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.
            resampling (Resampling): Resampling method

        Returns:
            str: Hillshade mask path

        """
        # Warp DEM
        warped_dem_path = self._warp_dem(dem_path, resolution, size, resampling)

        # Get Hillshade path
        hillshade_dem = os.path.join(
            self.output, f"{self.condensed_name}_HILLSHADE.tif"
        )
        if os.path.isfile(hillshade_dem):
            LOGGER.debug(
                "Already existing hillshade DEM for %s. Skipping process.", self.name
            )
        else:
            LOGGER.debug("Computing hillshade DEM for %s", self.name)

            # Get angles
            mean_azimuth_angle, mean_zenith_angle = self.get_mean_sun_angles()
            zenith = 90.0 - mean_zenith_angle
            azimuth = mean_azimuth_angle

            # Run cmd
            cmd_hillshade = [
                "gdaldem",
                "--config",
                "NUM_THREADS",
                MAX_CORES,
                "hillshade",
                strings.to_cmd_string(warped_dem_path),
                "-compute_edges",
                "-z",
                "1",
                "-az",
                azimuth,
                "-alt",
                zenith,
                "-of",
                "GTiff",
                strings.to_cmd_string(hillshade_dem),
            ]
            # Run command
            misc.run_cli(cmd_hillshade)

        return hillshade_dem

    def _load_clouds(
        self, bands: list, resolution: float = None, size: Union[list, tuple] = None
    ) -> dict:
        """
        Load cloud files as numpy arrays with the same resolution (and same metadata).

        Args:
            bands (list): List of the wanted bands
            resolution (int): Band resolution in meters
            size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.
        Returns:
            dict: Dictionary {band_name, band_xarray}
        """
        raise NotImplementedError("This method should be implemented by a child class")

    def _create_mask(
        self, xds: XDS_TYPE, cond: np.ndarray, nodata: np.ndarray
    ) -> XDS_TYPE:
        """
        Create a mask from a conditional array and a nodata mask.

        Args:
            xds (XDS_TYPE): xarray to retrieve attributes
            cond (np.ndarray): Conditional array
            nodata (np.ndarray): Nodata mask

        Returns:
            XDS_TYPE: Mask as xarray
        """
        mask = xds.copy(data=np.where(cond, self._mask_true, self._mask_false))
        mask = mask.where(nodata == 0, np.nan)

        return mask

Ancestors

Subclasses

Instance variables

var output

Inherited from: Product.output

Output directory of the product, to write orthorectified data for example.

var name

Inherited from: Product.name

Product name (its filename without any extension).

var split_name

Inherited from: Product.split_name

Split name, to retrieve every information from its filename (dates, tile, product type…).

var archive_path

Inherited from: Product.archive_path

Archive path, same as the product path if not specified. Useful when you want to know where both the extracted and archived version of your product …

var path

Inherited from: Product.path

Usable path to the product, either extracted or archived path, according to the satellite.

var is_archived

Inherited from: Product.is_archived

Is the archived product is processed (a products is considered as archived if its products path is a directory).

var needs_extraction

Inherited from: Product.needs_extraction

Does this products needs to be extracted to be processed ? (True by default).

var date

Inherited from: Product.date

Acquisition date.

var datetime

Inherited from: Product.datetime

Acquisition datetime.

var tile_name

Inherited from: Product.tile_name

Tile if possible (for data that can be piled, for example S2 and Landsats).

var sensor_type

Inherited from: Product.sensor_type

Sensor type, SAR or optical.

var product_type

Inherited from: Product.product_type

Product type, satellite-related field, such as L1C or L2A for Sentinel-2 data.

var band_names

Inherited from: Product.band_names

Band mapping between band wrapping names such as GREEN and band real number such as 03 for Sentinel-2.

var is_reference

Inherited from: Product.is_reference

If the product is a reference, used for algorithms that need pre and post data, such as fire detection.

var corresponding_ref

Inherited from: Product.corresponding_ref

The corresponding reference products to the current one (if the product is not a reference but has a reference data corresponding to it). A list …

var nodata

Inherited from: Product.nodata

Product nodata, set to 0 by default. Please do not touch this or all index will fail.

var platform

Inherited from: Product.platform

Product platform, such as Sentinel-2

var resolution

Inherited from: Product.resolution

Default resolution in meters of the current product. For SAR product, we use Ground Range resolution as we will automatically orthorectify the tiles.

var condensed_name

Inherited from: Product.condensed_name

Condensed name, the filename with only useful data to keep the name unique (ie. 20191215T110441_S2_30TXP_L2A_122756). Used to shorten names and paths.

var sat_id

Inherited from: Product.sat_id

Satellite ID, i.e. S2 for Sentinel-2

Methods

def get_default_band(

self)

Get default band: GREEN for optical data as every optical satellite has a GREEN band.

>>> from eoreader.reader import Reader
>>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
>>> prod = Reader().open(path)
>>> prod.get_default_band()
<OpticalBandNames.GREEN: 'GREEN'>

Returns

str
Default band
Expand source code
def get_default_band(self) -> BandNames:
    """
    Get default band: `GREEN` for optical data as every optical satellite has a GREEN band.

    ```python
    >>> from eoreader.reader import Reader
    >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
    >>> prod = Reader().open(path)
    >>> prod.get_default_band()
    <OpticalBandNames.GREEN: 'GREEN'>
    ```

    Returns:
        str: Default band
    """
    return obn.GREEN

def get_default_band_path(

self)

Get default band (GREEN for optical data) path.

>>> from eoreader.reader import Reader
>>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
>>> prod = Reader().open(path)
>>> prod.get_default_band_path()
'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B03.jp2'

Returns

str
Default band path
Expand source code
def get_default_band_path(self) -> str:
    """
    Get default band (`GREEN` for optical data) path.

    ```python
    >>> from eoreader.reader import Reader
    >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
    >>> prod = Reader().open(path)
    >>> prod.get_default_band_path()
    'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B03.jp2'
    ```

    Returns:
        str: Default band path
    """
    default_band = self.get_default_band()
    return self.get_band_paths([default_band])[default_band]

def crs(

self)

Get UTM projection of the tile

>>> from eoreader.reader import Reader
>>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
>>> prod = Reader().open(path)
>>> prod.utm_crs()
CRS.from_epsg(32630)

Returns

rasterio.crs.CRS
CRS object
Expand source code
def crs(self) -> riocrs.CRS:
    """
    Get UTM projection of the tile

    ```python
    >>> from eoreader.reader import Reader
    >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
    >>> prod = Reader().open(path)
    >>> prod.utm_crs()
    CRS.from_epsg(32630)
    ```

    Returns:
        rasterio.crs.CRS: CRS object
    """
    band_path = self.get_default_band_path()
    with rasterio.open(band_path) as dst:
        utm = dst.crs

    return utm

def extent(

self)

Inherited from: Product.extent

Get UTM extent of the tile …

Expand source code
def extent(self) -> gpd.GeoDataFrame:
    """
    Get UTM extent of the tile

    ```python
    >>> from eoreader.reader import Reader
    >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
    >>> prod = Reader().open(path)
    >>> prod.utm_extent()
                                                geometry
    0  POLYGON ((309780.000 4390200.000, 309780.000 4...
    ```

    Returns:
        gpd.GeoDataFrame: Footprint in UTM
    """
    # Get extent
    return rasters.get_extent(self.get_default_band_path())

def get_existing_bands(

self)

Return the existing band paths.

>>> from eoreader.reader import Reader
>>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
>>> prod = Reader().open(path)
>>> prod.get_existing_bands()
[<OpticalBandNames.CA: 'COASTAL_AEROSOL'>,
<OpticalBandNames.BLUE: 'BLUE'>,
<OpticalBandNames.GREEN: 'GREEN'>,
<OpticalBandNames.RED: 'RED'>,
<OpticalBandNames.VRE_1: 'VEGETATION_RED_EDGE_1'>,
<OpticalBandNames.VRE_2: 'VEGETATION_RED_EDGE_2'>,
<OpticalBandNames.VRE_3: 'VEGETATION_RED_EDGE_3'>,
<OpticalBandNames.NIR: 'NIR'>,
<OpticalBandNames.NNIR: 'NARROW_NIR'>,
<OpticalBandNames.WV: 'WATER_VAPOUR'>,
<OpticalBandNames.CIRRUS: 'CIRRUS'>,
<OpticalBandNames.SWIR_1: 'SWIR_1'>,
<OpticalBandNames.SWIR_2: 'SWIR_2'>]

Returns

list
List of existing bands in the products
Expand source code
def get_existing_bands(self) -> list:
    """
    Return the existing band paths.

    ```python
    >>> from eoreader.reader import Reader
    >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
    >>> prod = Reader().open(path)
    >>> prod.get_existing_bands()
    [<OpticalBandNames.CA: 'COASTAL_AEROSOL'>,
    <OpticalBandNames.BLUE: 'BLUE'>,
    <OpticalBandNames.GREEN: 'GREEN'>,
    <OpticalBandNames.RED: 'RED'>,
    <OpticalBandNames.VRE_1: 'VEGETATION_RED_EDGE_1'>,
    <OpticalBandNames.VRE_2: 'VEGETATION_RED_EDGE_2'>,
    <OpticalBandNames.VRE_3: 'VEGETATION_RED_EDGE_3'>,
    <OpticalBandNames.NIR: 'NIR'>,
    <OpticalBandNames.NNIR: 'NARROW_NIR'>,
    <OpticalBandNames.WV: 'WATER_VAPOUR'>,
    <OpticalBandNames.CIRRUS: 'CIRRUS'>,
    <OpticalBandNames.SWIR_1: 'SWIR_1'>,
    <OpticalBandNames.SWIR_2: 'SWIR_2'>]
    ```

    Returns:
        list: List of existing bands in the products
    """
    return [name for name, nb in self.band_names.items() if nb]

def get_existing_band_paths(

self)

Inherited from: Product.get_existing_band_paths

Return the existing band paths …

Expand source code
def get_existing_band_paths(self) -> dict:
    """
    Return the existing band paths.

    ```python
    >>> from eoreader.reader import Reader
    >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
    >>> prod = Reader().open(path)
    >>> prod.get_existing_band_paths()
    {
        <OpticalBandNames.CA: 'COASTAL_AEROSOL'>: 'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B01.jp2',
        ...,
        <OpticalBandNames.SWIR_2: 'SWIR_2'>: 'zip+file://S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip!/S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE/GRANULE/L1C_T30TTK_A027018_20200824T111345/IMG_DATA/T30TTK_20200824T110631_B12.jp2'
    }
    ```

    Returns:
        dict: Dictionary containing the path of each queried band
    """
    existing_bands = self.get_existing_bands()
    return self.get_band_paths(band_list=existing_bands)

def get_mean_sun_angles(

self)

Get Mean Sun angles (Azimuth and Zenith angles)

>>> from eoreader.reader import Reader
>>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
>>> prod = Reader().open(path)
>>> prod.get_mean_sun_angles()
(149.148155074489, 32.6627897525474)

Returns

(float, float): Mean Azimuth and Zenith angle

Expand source code
@abstractmethod
def get_mean_sun_angles(self) -> (float, float):
    """
    Get Mean Sun angles (Azimuth and Zenith angles)

    ```python
    >>> from eoreader.reader import Reader
    >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
    >>> prod = Reader().open(path)
    >>> prod.get_mean_sun_angles()
    (149.148155074489, 32.6627897525474)
    ```

    Returns:
        (float, float): Mean Azimuth and Zenith angle
    """
    raise NotImplementedError("This method should be implemented by a child class")

def footprint(

self)

Inherited from: Product.footprint

Get UTM footprint of the products (without nodata, in french == emprise utile) …

def get_datetime(

self,
as_datetime=False)

Inherited from: Product.get_datetime

Get the product's acquisition datetime, with format YYYYMMDDTHHMMSS <-> %Y%m%dT%H%M%S

def get_date(

self,
as_date=False)

Inherited from: Product.get_date

Get the product's acquisition date …

def get_band_paths(

self,
band_list,
resolution=None)

Inherited from: Product.get_band_paths

Return the paths of required bands …

def read_mtd(

self)

Inherited from: Product.read_mtd

Read metadata and outputs the metadata XML root and its namespace most of the time, except from L8-collection 1 data which outputs a pandas DataFrame …

def load(

self,
bands,
resolution=None,
size=None)

Inherited from: Product.load

Open the bands and compute the wanted index …

def has_band(

self,
band)

Inherited from: Product.has_band

Does this products has the specified band ? …

def stack(

self,
bands,
resolution=None,
stack_path=None,
save_as_int=False)

Inherited from: Product.stack

Stack bands and index of a products …