From 86cf95a737c24be2ee81416b8e63510fb30d3236 Mon Sep 17 00:00:00 2001 From: BRAUN REMI Date: Tue, 5 Dec 2023 15:02:06 +0100 Subject: [PATCH] ENH: Handling of Sentinel-1 MPC RTC products. #118 --- CHANGES.md | 2 +- docs/sar.md | 82 +++---- eoreader/products/__init__.py | 10 +- eoreader/products/optical/s2_e84_product.py | 2 +- eoreader/products/sar/s1_rtc_product.py | 255 +++++++++++++++++++- 5 files changed, 299 insertions(+), 52 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9f1f806c..5150948c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,7 +9,7 @@ - **ENH: Handle Python 3.12. ([#113](https://github.com/sertit/eoreader/issues/113))** - **ENH: Guard against S1 COG format, not yet handled by SNAP.** - **ENH: Calibration step for `Capella` products now exists in ESA SNAP. Add it in pre-processing.** -- **ENH: Handling of Sentinel-1 ASF RTC products. ([#112](https://github.com/sertit/eoreader/issues/112))** +- **ENH: Handling of Sentinel-1 [ASF](https://hyp3-docs.asf.alaska.edu/guides/rtc_product_guide/#readme-file) and [MPC](https://planetarycomputer.microsoft.com/dataset/sentinel-1-rtc) RTC products. ([#112](https://github.com/sertit/eoreader/issues/112), [#118](https://github.com/sertit/eoreader/issues/118))** - **ENH: Handling of Sentinel-1 SM products.** - FIX: Fix jpg, png... quicklooks management when plotting - FIX: Fix an `xarray` issue when trying to compute percentiles when stacking bands diff --git a/docs/sar.md b/docs/sar.md index 7e04bcdb..d644e575 100644 --- a/docs/sar.md +++ b/docs/sar.md @@ -6,18 +6,18 @@ You will find a SAR tutorial [here](https://eoreader.readthedocs.io/en/latest/no ![sar_sensors](_static/sar_sensors.png) -| Constellations | Class | Use archive | -|-------------------------------------|---------------------------------------------------------------|---------------------------------------------| -| `Capella` | {meth}`~eoreader.products.sar.capella_product.CapellaProduct` | ❌ | -| `COSMO-Skymed 1st Generation` | {meth}`~eoreader.products.sar.csk_product.CskProduct` | ❌ | -| `COSMO-Skymed 2nd Generation` | {meth}`~eoreader.products.sar.csg_product.CsgProduct` | ❌ | -| `ICEYE` | {meth}`~eoreader.products.sar.iceye_product.IceyeProduct` | ❌ | -| `RADARSAT Constellation Mission` | {meth}`~eoreader.products.sar.rcm_product.RcmProduct` | ❌ | +| Constellations | Class | Use archive | +|-------------------------------------|---------------------------------------------------------------|-------------------------------------------| +| `Capella` | {meth}`~eoreader.products.sar.capella_product.CapellaProduct` | ❌ | +| `COSMO-Skymed 1st Generation` | {meth}`~eoreader.products.sar.csk_product.CskProduct` | ❌ | +| `COSMO-Skymed 2nd Generation` | {meth}`~eoreader.products.sar.csg_product.CsgProduct` | ❌ | +| `ICEYE` | {meth}`~eoreader.products.sar.iceye_product.IceyeProduct` | ❌ | +| `RADARSAT Constellation Mission` | {meth}`~eoreader.products.sar.rcm_product.RcmProduct` | ❌ | | `RADARSAT-2` | {meth}`~eoreader.products.sar.rs2_product.Rs2Product` | ✅ for ground range data, ❌ for complex data | -| `Sentinel-1` | {meth}`~eoreader.products.sar.s1_product.S1Product` | ✅ | -| `Sentinel-1 RTC` | {meth}`~eoreader.products.sar.s1_product.S1RtcProduct` | ✅ | -| `SAOCOM-1` | {meth}`~eoreader.products.sar.saocom_product.SaocomProduct` | ❌ | -| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | {meth}`~eoreader.products.sar.tsx_product.TsxProduct` | ❌ | +| `Sentinel-1` | {meth}`~eoreader.products.sar.s1_product.S1Product` | ✅ | +| `Sentinel-1 RTC` | {meth}`~eoreader.products.sar.s1_product.S1RtcProduct` | ✅ for ASF | +| `SAOCOM-1` | {meth}`~eoreader.products.sar.saocom_product.SaocomProduct` | ❌ | +| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | {meth}`~eoreader.products.sar.tsx_product.TsxProduct` | ❌ | ```{warning} Satellites products that cannot be used as archived have to be extracted before use, @@ -26,36 +26,36 @@ mostly because SNAP doesn't handle them. ## Product type handling -| Constellations | Product Type | Handled | -|-------------------------------------|--------------------------------------|---------| -| `Capella` | SLC | ✅ | -| `Capella` | GEC | ✅ | -| `Capella` | GEO | ✅ | -| `Capella` | SICD, SIDD, CPHD | ❌ | -| `COSMO-Skymed` | SCS | ✅ | -| `COSMO-SkyMed` 1st Generation | DGM | ✅ | -| `COSMO-SkyMed` 2nd Generation | DGM | ⚠ | -| `COSMO-SkyMed` | GEC, GTC | ⚠ | -| `ICEYE` | SLC | ✅ | -| `ICEYE` | GRD | ✅ | -| `ICEYE` | ORTHO | 💤 | -| `RADARSAT Constellation Mission` | SLC | ⚠ | -| `RADARSAT Constellation Mission` | GRC, GCC, GCD | ⚠ | -| `RADARSAT Constellation Mission` | GRD | ✅ | -| `RADARSAT-2` | SLC | ✅ | -| `RADARSAT-2` | SGX, SCN, SCW,
SCF, SCS, SSG, SPG | ⚠ | -| `RADARSAT-2` | SGF | ✅ | -| `Sentinel-1` | SLC | ✅ | -| `Sentinel-1` | GRD | ✅ | -| `Sentinel-1` | RTC (ASF) | ✅ | -| `SAOCOM-1` | SLC | ✅ | -| `SAOCOM-1` | ID | ⚠ | -| `SAOCOM-1` | GEC | ✅ | -| `SAOCOM-1` | GTC | ✅ | -| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | SSC | ✅ | -| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | MGD | ✅ | -| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | GEC | ⚠ | -| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | EEC | ✅ | +| Constellations | Product Type | Handled | +|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| `Capella` | SLC | ✅ | +| `Capella` | GEC | ✅ | +| `Capella` | GEO | ✅ | +| `Capella` | SICD, SIDD, CPHD | ❌ | +| `COSMO-Skymed` | SCS | ✅ | +| `COSMO-SkyMed` 1st Generation | DGM | ✅ | +| `COSMO-SkyMed` 2nd Generation | DGM | ⚠ | +| `COSMO-SkyMed` | GEC, GTC | ⚠ | +| `ICEYE` | SLC | ✅ | +| `ICEYE` | GRD | ✅ | +| `ICEYE` | ORTHO | 💤 | +| `RADARSAT Constellation Mission` | SLC | ⚠ | +| `RADARSAT Constellation Mission` | GRC, GCC, GCD | ⚠ | +| `RADARSAT Constellation Mission` | GRD | ✅ | +| `RADARSAT-2` | SLC | ✅ | +| `RADARSAT-2` | SGX, SCN, SCW,
SCF, SCS, SSG, SPG | ⚠ | +| `RADARSAT-2` | SGF | ✅ | +| `Sentinel-1` | SLC | ✅ | +| `Sentinel-1` | GRD | ✅ | +| `Sentinel-1` | RTC ([ASF](https://hyp3-docs.asf.alaska.edu/guides/rtc_product_guide/#readme-file) and [MPC](https://planetarycomputer.microsoft.com/dataset/sentinel-1-rtc)) | ✅ | +| `SAOCOM-1` | SLC | ✅ | +| `SAOCOM-1` | ID | ⚠ | +| `SAOCOM-1` | GEC | ✅ | +| `SAOCOM-1` | GTC | ✅ | +| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | SSC | ✅ | +| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | MGD | ✅ | +| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | GEC | ⚠ | +| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | EEC | ✅ | ✅: Tested ⚠: Never tested, **use it at your own risk!** diff --git a/eoreader/products/__init__.py b/eoreader/products/__init__.py index b7f28752..cf9764ff 100644 --- a/eoreader/products/__init__.py +++ b/eoreader/products/__init__.py @@ -170,7 +170,7 @@ S2Jp2Masks, S2StacProduct, ) -from .optical.s2_e84_product import S2E84Product, S2E84StacProduct, S2MPCStacProduct +from .optical.s2_e84_product import S2E84Product, S2E84StacProduct, S2MpcStacProduct from .optical.s2_theia_product import S2TheiaProduct from .optical.s3_product import S3Product, S3ProductType, S3DataType, S3Instrument from .optical.s3_olci_product import S3OlciProduct @@ -205,9 +205,9 @@ "S1Product", "S1SensorMode", "S1ProductType", - "S1RTCProduct", - "S1RTCProductType", - "SaocomProduct", + "S1RtcProduct", + "S1RtcProductType", + "S1RtcMpcStacProduct" "SaocomProduct", "SaocomProductType", "SaocomPolarization", "SaocomSensorMode" "TsxProduct", @@ -227,7 +227,7 @@ from .sar.rcm_product import RcmProduct, RcmProductType, RcmSensorMode from .sar.rs2_product import Rs2Product, Rs2ProductType, Rs2SensorMode from .sar.s1_product import S1Product, S1SensorMode, S1ProductType -from .sar.s1_rtc_product import S1RtcProduct, S1RtcProductType +from .sar.s1_rtc_product import S1RtcAsfProduct, S1RtcProductType, S1RtcMpcStacProduct from .sar.saocom_product import ( SaocomProduct, SaocomProductType, diff --git a/eoreader/products/optical/s2_e84_product.py b/eoreader/products/optical/s2_e84_product.py index 85b4abe9..1bcc2c90 100644 --- a/eoreader/products/optical/s2_e84_product.py +++ b/eoreader/products/optical/s2_e84_product.py @@ -894,7 +894,7 @@ def get_quicklook_path(self) -> str: return self._get_path("thumbnail") -class S2MPCStacProduct(StacProduct, S2E84Product): +class S2MpcStacProduct(StacProduct, S2E84Product): def __init__( self, product_path: AnyPathStrType = None, diff --git a/eoreader/products/sar/s1_rtc_product.py b/eoreader/products/sar/s1_rtc_product.py index ceee3580..e23a4c5d 100644 --- a/eoreader/products/sar/s1_rtc_product.py +++ b/eoreader/products/sar/s1_rtc_product.py @@ -19,21 +19,26 @@ Take a look `here `_. """ +import asyncio import logging from datetime import datetime from enum import unique from typing import Union import geopandas as gpd +import planetary_computer from lxml import etree +from pystac import Item from rasterio import crs -from sertit import path, vectors +from sertit import AnyPath, path, vectors, xml from sertit.misc import ListEnum +from sertit.types import AnyPathStrType from shapely import box from eoreader import DATETIME_FMT, EOREADER_NAME, cache +from eoreader.bands import SarBandNames as sab from eoreader.exceptions import InvalidProductError -from eoreader.products import S1SensorMode, SarProduct +from eoreader.products import S1SensorMode, SarProduct, StacProduct from eoreader.products.product import OrbitDirection from eoreader.utils import simplify @@ -53,9 +58,9 @@ class S1RtcProductType(ListEnum): """ -class S1RtcProduct(SarProduct): +class S1RtcAsfProduct(SarProduct): """ - Class for S1 RTC + Class for S1 RTC from Asf Take a look `here `_. """ @@ -317,3 +322,245 @@ def get_orbit_direction(self) -> OrbitDirection: OrbitDirection: Orbit direction (ASCENDING/DESCENDING) """ return OrbitDirection.UNKNOWN + + +class S1RtcMpcStacProduct(StacProduct, SarProduct): + """ + Class for S1 RTC from MPC (via stac) + Take a look + `here `_. + """ + + def __init__( + self, + product_path: AnyPathStrType = None, + archive_path: AnyPathStrType = None, + output_path: AnyPathStrType = None, + remove_tmp: bool = False, + **kwargs, + ) -> None: + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + self.kwargs = kwargs + """Custom kwargs""" + + # Copy the kwargs + super_kwargs = kwargs.copy() + + # Get STAC Item + self.item = None + """ STAC Item of the product """ + self.item = super_kwargs.pop("item", None) + if self.item is None: + try: + self.item = Item.from_file(product_path) + except TypeError: + raise InvalidProductError( + "You should either fill 'product_path' or 'item'." + ) + + self.default_clients = [planetary_computer] + self.clients = super_kwargs.pop("client", self.default_clients) + + if product_path is None: + # Canonical link is always the second one + # TODO: check if ok + product_path = AnyPath(self.item.links[0].target).parent + + # Initialization from the super class + super().__init__(product_path, archive_path, output_path, remove_tmp, **kwargs) + + def _set_pixel_size(self) -> None: + """ + Set product default pixel size (in meters) + """ + # Use range, maybe azimuth one day + self.pixel_size = self.item.properties["sar:pixel_spacing_range"] + self.resolution = self.item.properties["sar:resolution_range"] + + def _set_instrument(self) -> None: + """ + Set instrument + + Sentinel-1: https://sentinels.copernicus.eu/web/sentinel/missions/sentinel-1/instrument-payload + """ + self.instrument = "SAR C-band" + + def _pre_init(self, **kwargs) -> None: + """ + Function used to pre_init the products + (setting needs_extraction and so on) + """ + self.stac_mtd = self.item.to_dict() + + self._band_folder = self.path + + # Already ORTHO, so OK + self.needs_extraction = False + + # Its original filename is its name + self._use_filename = True + + # Pre init done by the super class + super()._pre_init(**kwargs) + + def _post_init(self, **kwargs) -> None: + """ + Function used to post_init the products + (setting product-type, band names and so on) + """ + # Private attributes + self._raw_band_regex = "*_{!l}.rtc.tiff" + + # Post init done by the super class + super()._post_init(**kwargs) + + def get_raw_band_paths(self, **kwargs) -> dict: + """ + Return the existing band paths (as they come with the archived products). + + Args: + **kwargs: Additional arguments + + Returns: + dict: Dictionary containing the path of every band existing in the raw products + """ + band_paths = {} + for band in sab.speckle_list(): + try: + band_paths[band] = self.sign_url( + self.item.assets[band.name.lower()].href + ) + except KeyError: + continue + + return band_paths + + def _set_product_type(self) -> None: + """Set products type""" + self.product_type = S1RtcProductType.RTC + + def _set_sensor_mode(self) -> None: + """Get sensor mode""" + mode = self.split_name[1] + # Mono swath SM + if mode in ["S1", "S2", "S3", "S4", "S5", "S6"]: + mode = "SM" + + # Get sensor mode + self.sensor_mode = S1SensorMode.from_value(mode) + + if not self.sensor_mode: + raise InvalidProductError( + f"Invalid {self.constellation.value} name: {self.name}" + ) + + def get_datetime(self, as_datetime: bool = False) -> Union[str, datetime]: + """ + Get the product's acquisition datetime, with format :code:`YYYYMMDDTHHMMSS` <-> :code:`%Y%m%dT%H%M%S` + + .. code-block:: python + + >>> from eoreader.reader import Reader + >>> path = r"1011117-766193" + >>> prod = Reader().open(path) + >>> prod.get_datetime(as_datetime=True) + datetime.datetime(2020, 10, 28, 22, 46, 25) + >>> prod.get_datetime(as_datetime=False) + '20201028T224625' + + Args: + as_datetime (bool): Return the date as a datetime.datetime. If false, returns a string. + + Returns: + Union[str, datetime.datetime]: Its acquisition datetime + """ + if self.datetime is None: + # Sentinel-2 datetime (in the filename) is the datatake sensing time, not the granule sensing time ! + sensing_time = self.split_name[4] + + # Convert to datetime + date = datetime.strptime(sensing_time, "%Y%m%dT%H%M%S") + else: + date = self.datetime + + if not as_datetime: + date = date.strftime(DATETIME_FMT) + + return date + + def _get_name(self) -> str: + """ + Set product real name. + + Returns: + str: True name of the product (from metadata) + """ + return self.stac_mtd["id"] + + def _get_name_constellation_specific(self) -> str: + """ + Set product real name from metadata + + Returns: + str: True name of the product (from metadata) + """ + # No need here (_get_name reimplemented) + pass + + @cache + def _read_mtd(self) -> (etree._Element, dict): + """ + Read metadata and outputs the metadata XML root and its namespaces as a dict + + .. code-block:: python + + >>> from eoreader.reader import Reader + >>> path = r"1001513-735093" + >>> prod = Reader().open(path) + >>> prod.read_mtd() + (, {}) + + Returns: + (etree._Element, dict): Metadata XML root and its namespaces + """ + return xml.dict_to_xml(self.stac_mtd), {} + + def get_quicklook_path(self) -> str: + """ + Get quicklook path if existing. + + Returns: + str: Quicklook path + """ + quicklook_path = None + try: + if self.is_archived: + quicklook_path = self.path / path.get_archived_path( + self.path, file_regex=r".*preview\.png" + ) + else: + quicklook_path = next(self.path.glob("*preview.png")) + except (StopIteration, FileNotFoundError): + pass + + return str(quicklook_path) + + @cache + def get_orbit_direction(self) -> OrbitDirection: + """ + Get cloud cover as given in the metadata + + .. code-block:: python + + >>> from eoreader.reader import Reader + >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip" + >>> prod = Reader().open(path) + >>> prod.get_orbit_direction().value + "DESCENDING" + + Returns: + OrbitDirection: Orbit direction (ASCENDING/DESCENDING) + """ + return OrbitDirection.from_value( + self.item.properties["sat:orbit_state"].upper() + )