Module eoreader.products.sar.sar_product
Super class for SAR 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 SAR products """
import logging
import os
import re
import tempfile
import zipfile
from abc import abstractmethod
from enum import unique
from string import Formatter
from typing import Union
import geopandas as gpd
import numpy as np
from rasterio import crs
from rasterio.enums import Resampling
from eoreader import utils
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 SarBandNames as sbn
from eoreader.bands.bands import SarBands
from eoreader.env_vars import DSPK_GRAPH, PP_GRAPH, SAR_DEF_RES
from eoreader.exceptions import InvalidBandError, InvalidProductError, InvalidTypeError
from eoreader.products.product import Product, SensorType, path_or_dst
from eoreader.reader import Platform
from eoreader.utils import EOREADER_NAME
from sertit import files, misc, rasters, snap, strings, vectors
from sertit.misc import ListEnum
from sertit.rasters import XDS_TYPE
LOGGER = logging.getLogger(EOREADER_NAME)
@unique
class SarProductType(ListEnum):
"""
Generic products types, used to chose a SNAP graph.
"""
CPLX = "COMPLEX"
"""Single Look Complex"""
GDRG = "GROUND"
"""Ground Range"""
OTHER = "OTHER"
"""Other products types, no used in EOReader"""
# Add ortho products ?
class _ExtendedFormatter(Formatter):
"""An extended format string formatter
Formatter with extended conversion symbol
"""
def convert_field(self, value, conversion):
"""Extend conversion symbol
Following additional symbol has been added
* l: convert to string and low case
* u: convert to string and up case
default are:
* s: convert with str()
* r: convert with repr()
* a: convert with ascii()
"""
if conversion == "u":
cv_field = str(value).upper()
elif conversion == "l":
cv_field = str(value).lower()
else:
cv_field = super().convert_field(value, conversion)
return cv_field
class SarProduct(Product):
"""Super class for SAR Products"""
def __init__(
self, product_path: str, archive_path: str = None, output_path=None
) -> None:
self.sar_prod_type = None
"""SAR product type, either Single Look Complex or Ground Range"""
self.sensor_mode = None
"""Sensor Mode of the current product"""
self.pol_channels = None
"""Polarization Channels stored in the current product"""
# Private attributes
self._band_folder = None
self._snap_path = None
self._raw_band_regex = None
self._snap_no_data = 0
# Initialization from the super class
super().__init__(product_path, archive_path, output_path)
def _post_init(self) -> None:
"""
Function used to post_init the products
(setting product-type, band names and so on)
"""
self.tile_name = None
self.sensor_type = SensorType.SAR
self.band_names = SarBands()
self._set_sensor_mode()
self.pol_channels = self._get_raw_bands()
def footprint(self) -> gpd.GeoDataFrame:
"""
Get UTM footprint of the products (without nodata, *in french == emprise utile*)
```python
>>> from eoreader.reader import Reader
>>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
>>> prod = Reader().open(path)
>>> prod.footprint()
index geometry
0 0 POLYGON ((199980.000 4500000.000, 199980.000 4...
```
Returns:
gpd.GeoDataFrame: Footprint as a GeoDataFrame
"""
return rasters.get_footprint(
self.get_default_band_path()
) # Processed by SNAP: the nodata is set
def get_default_band(self) -> BandNames:
"""
Get default band:
The first existing one between `VV` and `HH` for SAR data.
```python
>>> from eoreader.reader import Reader
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.get_default_band()
<SarBandNames.VV: 'VV'>
```
Returns:
str: Default band
"""
existing_bands = self._get_raw_bands()
if not existing_bands:
raise InvalidProductError(f"No band exists for products: {self.name}")
# The order matters, as we almost always prefer VV and HH
if sbn.VV in existing_bands:
default_band = sbn.VV
elif sbn.HH in existing_bands:
default_band = sbn.HH
elif sbn.VH in existing_bands:
default_band = sbn.VH
elif sbn.HV in existing_bands:
default_band = sbn.HV
else:
raise InvalidTypeError(f"Invalid bands for products: {existing_bands}")
return default_band
# Parameters differ from overridden 'get_default_band_path' method (arguments-differ)
# pylint: disable=W0221
def get_default_band_path(self) -> str:
"""
Get default band path (the first existing one between `VV` and `HH` for SAR data), ready to use (orthorectified)
.. WARNING:: This functions orthorectifies SAR bands if not existing !
```python
>>> from eoreader.reader import Reader
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.get_default_band_path()
Executing processing graph
....10%....20%....30%....40%....50%....60%....70%....80%....90% done.
'20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif'
```
Returns:
str: Default band path
"""
default_band = self.get_default_band()
band_path = self.get_band_paths([default_band])
return band_path[default_band]
@abstractmethod
def wgs84_extent(self) -> gpd.GeoDataFrame:
"""
Get the WGS84 extent of the file before any reprojection.
This is useful when the SAR pre-process has not been done yet.
```python
>>> from eoreader.reader import Reader
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.wgs84_extent()
Name ... geometry
0 Sentinel-1 Image Overlay ... POLYGON ((0.85336 42.24660, -2.32032 42.65493,...
[1 rows x 12 columns]
```
Returns:
gpd.GeoDataFrame: WGS84 extent as a gpd.GeoDataFrame
"""
raise NotImplementedError("This method should be implemented by a child class")
def extent(self) -> gpd.GeoDataFrame:
"""
Get UTM extent of the tile
```python
>>> from eoreader.reader import Reader
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.utm_extent()
Name ... geometry
0 Sentinel-1 Image Overlay ... POLYGON ((817914.501 4684349.823, 555708.624 4...
[1 rows x 12 columns]
```
Returns:
gpd.GeoDataFrame: Footprint in UTM
"""
# Get WGS84 extent
extent_wgs84 = self.wgs84_extent()
# Get upper-left corner and deduce UTM proj from it
utm = vectors.corresponding_utm_projection(
extent_wgs84.bounds.minx, extent_wgs84.bounds.maxy
)
extent = extent_wgs84.to_crs(utm)
return extent
def crs(self) -> crs.CRS:
"""
Get UTM projection
```python
>>> from eoreader.reader import Reader
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.utm_crs()
CRS.from_epsg(32630)
```
Returns:
crs.CRS: CRS object
"""
# Get WGS84 extent
extent_wgs84 = self.wgs84_extent()
# Get upper-left corner and deduce UTM proj from it
crs_str = vectors.corresponding_utm_projection(
extent_wgs84.bounds.minx, extent_wgs84.bounds.maxy
)
return crs.CRS.from_string(crs_str)
def _get_sar_product_type(
self,
prod_type_pos: int,
gdrg_types: Union[ListEnum, list],
cplx_types: Union[ListEnum, list],
) -> None:
"""
Get products type, special function for SAR satellites.
Args:
prod_type_pos (int): Position of the products type in the file name
gdrg_types (Union[ListEnum, list]): Ground Range products types
cplx_types (Union[ListEnum, list]): Complex products types
"""
# Get and check products type class
if not isinstance(gdrg_types, list):
gdrg_types = [gdrg_types]
if not isinstance(cplx_types, list):
cplx_types = [cplx_types]
all_types = gdrg_types + cplx_types
prod_type_class = all_types[0].__class__
assert all(isinstance(prod_type, prod_type_class) for prod_type in all_types)
# Get products type
try:
# All products types can be found in the filename and are 3 characters long
self.product_type = prod_type_class.from_value(
self.split_name[prod_type_pos][:3]
)
except ValueError as ex:
raise InvalidTypeError(f"Invalid products type for {self.name}") from ex
# Link to SAR generic products types
if self.product_type in gdrg_types:
self.sar_prod_type = SarProductType.GDRG
elif self.product_type in cplx_types:
self.sar_prod_type = SarProductType.CPLX
else:
self.sar_prod_type = SarProductType.OTHER
# Discard invalid products types
if self.sar_prod_type == SarProductType.OTHER:
raise NotImplementedError(
f"{self.product_type.value} product type is not available ({self.name})"
)
@abstractmethod
def _set_sensor_mode(self) -> None:
"""
Set SAR sensor mode
"""
raise NotImplementedError("This method should be implemented by a child class")
def get_band_paths(self, band_list: list, resolution: float = None) -> dict:
"""
Return the paths of required bands.
.. WARNING:: This functions orthorectifies SAR bands if not existing !
```python
>>> from eoreader.reader import Reader
>>> from eoreader.bands.alias import *
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.get_band_paths([VV, HH])
{
<SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif' # HH doesn't exist
}
```
Args:
band_list (list): List of the wanted bands
resolution (float): Band resolution
Returns:
dict: Dictionary containing the path of each queried band
"""
band_paths = {}
for band in band_list:
bname = self.band_names[band]
if bname is None:
raise InvalidProductError(
f"Non existing band ({band.name}) for {self.name}"
)
try:
# Try to load orthorectified bands
band_paths[band] = files.get_file_in_dir(
self._get_band_folder(),
f"{self.condensed_name}_{bname}.tif",
exact_name=True,
)
except FileNotFoundError:
speckle_band = sbn.corresponding_speckle(band)
if speckle_band in self.pol_channels:
if sbn.is_despeckle(band):
# Despeckle the noisy band
band_paths[band] = self._despeckle_sar(speckle_band)
else:
all_band_paths = self._pre_process_sar(resolution)
band_paths = {
band: path
for band, path in all_band_paths.items()
if band in band_list
}
return band_paths
def _get_raw_band_paths(self) -> dict:
"""
Return the existing band paths (as they come with th archived products).
Returns:
dict: Dictionary containing the path of every band existing in the raw products
"""
extended_fmt = _ExtendedFormatter()
band_paths = {}
for band, band_name in self.band_names.items():
band_regex = extended_fmt.format(self._raw_band_regex, band_name)
if self.is_archived:
if self.path.endswith(".zip"):
# Open the zip file
with zipfile.ZipFile(self.path, "r") as zip_ds:
# Get the correct band path
regex = re.compile(band_regex.replace("*", ".*"))
try:
band_paths[band] = list(
filter(
regex.match, [f.filename for f in zip_ds.filelist]
)
)[0]
except IndexError:
continue
else:
raise InvalidProductError(
f"Only zipped products can be processed without extraction: {self.path}"
)
else:
try:
band_paths[band] = files.get_file_in_dir(
self._band_folder, band_regex, exact_name=True, get_list=True
)
except FileNotFoundError:
continue
return band_paths
def _get_raw_bands(self) -> list:
"""
Return the existing band paths (as they come with th archived products).
Returns:
list: List of existing bands in the raw products (vv, hh, vh, hv)
"""
band_paths = self._get_raw_band_paths()
return list(band_paths.keys())
def get_existing_band_paths(self) -> dict:
"""
Return the existing orthorectified band paths (including despeckle bands).
.. WARNING:: This functions orthorectifies SAR bands if not existing !
.. WARNING:: This functions despeckles SAR bands if not existing !
```python
>>> from eoreader.reader import Reader
>>> from eoreader.bands.alias import *
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.get_existing_band_paths()
Executing processing graph
....10%....20%....30%....40%....50%....60%....70%....80%....90% done.
Executing processing graph
....10%....20%....30%....40%....50%....60%....70%....80%....90% done.
{
<SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif',
<SarBandNames.VH: 'VH'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VH.tif',
<SarBandNames.VV_DSPK: 'VV_DSPK'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV_DSPK.tif',
<SarBandNames.VH_DSPK: 'VH_DSPK'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VH_DSPK.tif'
}
```
Returns:
dict: Dictionary containing the path of every orthorectified bands
"""
# Get raw bands (maximum number of bands)
raw_bands = self._get_raw_bands()
possible_bands = raw_bands + [
sbn.corresponding_despeckle(band) for band in raw_bands
]
return self.get_band_paths(possible_bands)
def get_existing_bands(self) -> list:
"""
Return the existing orthorectified bands (including despeckle bands).
.. WARNING:: This functions orthorectifies SAR bands if not existing !
.. WARNING:: This functions despeckles SAR bands if not existing !
```python
>>> from eoreader.reader import Reader
>>> from eoreader.bands.alias import *
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.get_existing_bands()
[<SarBandNames.VV: 'VV'>,
<SarBandNames.VH: 'VH'>,
<SarBandNames.VV_DSPK: 'VV_DSPK'>,
<SarBandNames.VH_DSPK: 'VH_DSPK'>]
```
Returns:
list: List of existing bands in the products
"""
band_paths = self.get_existing_band_paths()
return list(band_paths.keys())
# unused band_name (compatibility reasons)
# pylint: disable=W0613
@path_or_dst
def _read_band(
self,
path: str,
resolution: Union[tuple, list, float] = None,
size: Union[list, tuple] = None,
) -> XDS_TYPE:
"""
Read band from disk.
Args:
path (str): Band path
resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y)
size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.
Returns:
XDS_TYPE: Band xarray
"""
return rasters.read(
path, resolution=resolution, size=size, resampling=Resampling.bilinear
)
def _load_bands(
self,
bands: Union[list, BandNames],
resolution: float = None,
size: Union[list, tuple] = None,
) -> dict:
"""
Load bands as numpy arrays with the same resolution (and same metadata).
Args:
bands (list, BandNames): List of the wanted bands
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}
"""
# Return empty if no band are specified
if not bands:
return {}
# Get band paths
if not isinstance(bands, list):
bands = [bands]
band_paths = self.get_band_paths(bands, resolution)
# Open bands and get array (resampled if needed)
band_arrays = {}
for band_name, band_path in band_paths.items():
# Read CSK band
band_arrays[band_name] = self._read_band(
band_path, resolution=resolution, size=size
)
return band_arrays
def _load(
self, bands: list, resolution: float = None, size: Union[list, tuple] = None
) -> dict:
"""
Core function loading SAR 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 = []
dem_list = []
for band in bands:
if is_index(band):
raise NotImplementedError(
"For now, no index is implemented for SAR data."
)
elif is_optical_band(band):
raise TypeError(
f"You should ask for SAR bands as {self.name} is a SAR product."
)
elif is_sar_band(band):
if not self.has_band(band):
raise InvalidBandError(
f"{band} cannot be retrieved from {self.condensed_name}"
)
else:
band_list.append(band)
elif is_dem(band):
dem_list.append(band)
elif is_clouds(band):
raise NotImplementedError(
f"Clouds cannot be retrieved from SAR data ({self.condensed_name})."
)
else:
raise InvalidTypeError(f"{band} is neither a band nor an index !")
# Load bands
bands = self._load_bands(band_list, resolution=resolution, size=size)
# Add DEM
bands.update(self._load_dem(dem_list, resolution=resolution, size=size))
return bands
def _pre_process_sar(self, resolution: float = None) -> dict:
"""
Pre-process SAR data (geocoding...)
Args:
resolution (float): Resolution
Returns:
dict: Dictionary containing {band: path}
"""
out = {}
# Create target dir (tmp dir)
with tempfile.TemporaryDirectory() as tmp_dir:
# Set command as a list
target_file = os.path.join(tmp_dir, f"{self.condensed_name}")
# Use dimap for speed and security (ie. GeoTiff's broken georef)
pp_target = f"{target_file}"
pp_dim = pp_target + ".dim"
# Pre-process graph
if PP_GRAPH not in os.environ:
sat = "s1" if self.sat_id == Platform.S1.name else "sar"
spt = "grd" if self.sar_prod_type == SarProductType.GDRG else "cplx"
pp_graph = os.path.join(
utils.get_data_dir(), f"{spt}_{sat}_preprocess_default.xml"
)
else:
pp_graph = os.environ[PP_GRAPH]
if not os.path.isfile(pp_graph) or not pp_graph.endswith(".xml"):
FileNotFoundError(f"{pp_graph} cannot be found.")
# Command line
if not os.path.isfile(pp_dim):
def_res = float(os.environ.get(SAR_DEF_RES, self.resolution))
res_m = resolution if resolution else def_res
res_deg = res_m / 10.0 * 8.983152841195215e-5 # Approx
cmd_list = snap.get_gpt_cli(
pp_graph,
[
f"-Pfile={strings.to_cmd_string(self._snap_path)}",
f"-Pout={pp_dim}",
f"-Pcrs={self.crs()}",
f"-Pres_m={res_m}",
f"-Pres_deg={res_deg}",
],
display_snap_opt=LOGGER.level == logging.DEBUG,
)
# Pre-process SAR images according to the given graph
LOGGER.debug("Pre-process SAR image")
misc.run_cli(cmd_list)
# Convert DIMAP images to GeoTiff
for pol in self.pol_channels:
# Speckle image
out[sbn.from_value(pol)] = self._write_sar(pp_dim, pol.value)
return out
def _despeckle_sar(self, band: sbn) -> str:
"""
Pre-process SAR data (geocode...)
Args:
band (sbn): Band to despeckle
Returns:
str: Despeckled path
"""
# Create target dir (tmp dir)
with tempfile.TemporaryDirectory() as tmp_dir:
# Out files
target_file = os.path.join(tmp_dir, f"{self.condensed_name}")
dspk_dim = target_file + ".dim"
# Despeckle graph
if DSPK_GRAPH not in os.environ:
dspk_graph = os.path.join(
utils.get_data_dir(), "sar_despeckle_default.xml"
)
else:
dspk_graph = os.environ[DSPK_GRAPH]
if not os.path.isfile(dspk_graph) or not dspk_graph.endswith(".xml"):
FileNotFoundError(f"{dspk_graph} cannot be found.")
# Create command line and run it
if not os.path.isfile(dspk_dim):
path = self.get_band_paths([band])[band]
cmd_list = snap.get_gpt_cli(
dspk_graph,
[f"-Pfile={path}", f"-Pout={dspk_dim}"],
display_snap_opt=False,
)
# Pre-process SAR images according to the given graph
LOGGER.debug("Despeckle SAR image")
misc.run_cli(cmd_list)
# Convert DIMAP images to GeoTiff
out = self._write_sar(dspk_dim, band.value, dspk=True)
return out
def _write_sar(self, dim_path: str, pol_up: str, dspk=False):
"""
Write SAR image on disk.
Args:
dim_path (str): DIMAP path
pol_up (str): Polarization name
"""
pol_up = pol_up.upper() # To be sure
# Get .img file path (readable by rasterio)
try:
img = rasters.get_dim_img_path(dim_path, pol_up)
except FileNotFoundError:
img = rasters.get_dim_img_path(dim_path) # Maybe not the good name
# Open SAR image
arr = rasters.read(img)
arr = arr.where(arr != self._snap_no_data, np.nan)
# Save the file as the terrain-corrected image
file_path = os.path.join(
self.output,
f"{files.get_filename(dim_path)}_{pol_up}{'_DSPK' if dspk else ''}.tif",
)
# WARNING: Set nodata to 0 here as it is the value wanted by SNAP !
rasters.write(arr, file_path, dtype=np.float32, nodata=0)
return file_path
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.
resampling (Resampling): Resampling method
size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.
Returns:
str: Hillshade mask path
"""
raise InvalidProductError("Impossible to compute hillshade mask for SAR data.")
def _has_cloud_band(self, band: BandNames) -> bool:
"""
Does this products has the specified cloud band ?
```python
>>> from eoreader.reader import Reader
>>> from eoreader.bands.alias import *
>>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip"
>>> prod = Reader().open(path)
>>> prod.has_cloud_band(CLOUDS)
False
```
"""
return False
def _get_condensed_name(self) -> str:
"""
Get products condensed name ({acq_datetime}_S1_{sensor_mode}_{product_type}).
Returns:
str: Condensed S1 name
"""
return f"{self.get_datetime()}_{self.platform.name}_{self.sensor_mode.name}_{self.product_type.value}"
Classes
class SarProductType (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Generic products types, used to chose a SNAP graph.
Expand source code
class SarProductType(ListEnum): """ Generic products types, used to chose a SNAP graph. """ CPLX = "COMPLEX" """Single Look Complex""" GDRG = "GROUND" """Ground Range""" OTHER = "OTHER" """Other products types, no used in EOReader"""
Ancestors
- sertit.misc.ListEnum
- enum.Enum
Class variables
var CPLX
-
Single Look Complex
var GDRG
-
Ground Range
var OTHER
-
Other products types, no used in EOReader
class SarProduct (product_path, archive_path=None, output_path=None)
-
Super class for SAR Products
Expand source code
class SarProduct(Product): """Super class for SAR Products""" def __init__( self, product_path: str, archive_path: str = None, output_path=None ) -> None: self.sar_prod_type = None """SAR product type, either Single Look Complex or Ground Range""" self.sensor_mode = None """Sensor Mode of the current product""" self.pol_channels = None """Polarization Channels stored in the current product""" # Private attributes self._band_folder = None self._snap_path = None self._raw_band_regex = None self._snap_no_data = 0 # Initialization from the super class super().__init__(product_path, archive_path, output_path) def _post_init(self) -> None: """ Function used to post_init the products (setting product-type, band names and so on) """ self.tile_name = None self.sensor_type = SensorType.SAR self.band_names = SarBands() self._set_sensor_mode() self.pol_channels = self._get_raw_bands() def footprint(self) -> gpd.GeoDataFrame: """ Get UTM footprint of the products (without nodata, *in french == emprise utile*) ```python >>> from eoreader.reader import Reader >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip" >>> prod = Reader().open(path) >>> prod.footprint() index geometry 0 0 POLYGON ((199980.000 4500000.000, 199980.000 4... ``` Returns: gpd.GeoDataFrame: Footprint as a GeoDataFrame """ return rasters.get_footprint( self.get_default_band_path() ) # Processed by SNAP: the nodata is set def get_default_band(self) -> BandNames: """ Get default band: The first existing one between `VV` and `HH` for SAR data. ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_default_band() <SarBandNames.VV: 'VV'> ``` Returns: str: Default band """ existing_bands = self._get_raw_bands() if not existing_bands: raise InvalidProductError(f"No band exists for products: {self.name}") # The order matters, as we almost always prefer VV and HH if sbn.VV in existing_bands: default_band = sbn.VV elif sbn.HH in existing_bands: default_band = sbn.HH elif sbn.VH in existing_bands: default_band = sbn.VH elif sbn.HV in existing_bands: default_band = sbn.HV else: raise InvalidTypeError(f"Invalid bands for products: {existing_bands}") return default_band # Parameters differ from overridden 'get_default_band_path' method (arguments-differ) # pylint: disable=W0221 def get_default_band_path(self) -> str: """ Get default band path (the first existing one between `VV` and `HH` for SAR data), ready to use (orthorectified) .. WARNING:: This functions orthorectifies SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_default_band_path() Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif' ``` Returns: str: Default band path """ default_band = self.get_default_band() band_path = self.get_band_paths([default_band]) return band_path[default_band] @abstractmethod def wgs84_extent(self) -> gpd.GeoDataFrame: """ Get the WGS84 extent of the file before any reprojection. This is useful when the SAR pre-process has not been done yet. ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.wgs84_extent() Name ... geometry 0 Sentinel-1 Image Overlay ... POLYGON ((0.85336 42.24660, -2.32032 42.65493,... [1 rows x 12 columns] ``` Returns: gpd.GeoDataFrame: WGS84 extent as a gpd.GeoDataFrame """ raise NotImplementedError("This method should be implemented by a child class") def extent(self) -> gpd.GeoDataFrame: """ Get UTM extent of the tile ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.utm_extent() Name ... geometry 0 Sentinel-1 Image Overlay ... POLYGON ((817914.501 4684349.823, 555708.624 4... [1 rows x 12 columns] ``` Returns: gpd.GeoDataFrame: Footprint in UTM """ # Get WGS84 extent extent_wgs84 = self.wgs84_extent() # Get upper-left corner and deduce UTM proj from it utm = vectors.corresponding_utm_projection( extent_wgs84.bounds.minx, extent_wgs84.bounds.maxy ) extent = extent_wgs84.to_crs(utm) return extent def crs(self) -> crs.CRS: """ Get UTM projection ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.utm_crs() CRS.from_epsg(32630) ``` Returns: crs.CRS: CRS object """ # Get WGS84 extent extent_wgs84 = self.wgs84_extent() # Get upper-left corner and deduce UTM proj from it crs_str = vectors.corresponding_utm_projection( extent_wgs84.bounds.minx, extent_wgs84.bounds.maxy ) return crs.CRS.from_string(crs_str) def _get_sar_product_type( self, prod_type_pos: int, gdrg_types: Union[ListEnum, list], cplx_types: Union[ListEnum, list], ) -> None: """ Get products type, special function for SAR satellites. Args: prod_type_pos (int): Position of the products type in the file name gdrg_types (Union[ListEnum, list]): Ground Range products types cplx_types (Union[ListEnum, list]): Complex products types """ # Get and check products type class if not isinstance(gdrg_types, list): gdrg_types = [gdrg_types] if not isinstance(cplx_types, list): cplx_types = [cplx_types] all_types = gdrg_types + cplx_types prod_type_class = all_types[0].__class__ assert all(isinstance(prod_type, prod_type_class) for prod_type in all_types) # Get products type try: # All products types can be found in the filename and are 3 characters long self.product_type = prod_type_class.from_value( self.split_name[prod_type_pos][:3] ) except ValueError as ex: raise InvalidTypeError(f"Invalid products type for {self.name}") from ex # Link to SAR generic products types if self.product_type in gdrg_types: self.sar_prod_type = SarProductType.GDRG elif self.product_type in cplx_types: self.sar_prod_type = SarProductType.CPLX else: self.sar_prod_type = SarProductType.OTHER # Discard invalid products types if self.sar_prod_type == SarProductType.OTHER: raise NotImplementedError( f"{self.product_type.value} product type is not available ({self.name})" ) @abstractmethod def _set_sensor_mode(self) -> None: """ Set SAR sensor mode """ raise NotImplementedError("This method should be implemented by a child class") def get_band_paths(self, band_list: list, resolution: float = None) -> dict: """ Return the paths of required bands. .. WARNING:: This functions orthorectifies SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_band_paths([VV, HH]) { <SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif' # HH doesn't exist } ``` Args: band_list (list): List of the wanted bands resolution (float): Band resolution Returns: dict: Dictionary containing the path of each queried band """ band_paths = {} for band in band_list: bname = self.band_names[band] if bname is None: raise InvalidProductError( f"Non existing band ({band.name}) for {self.name}" ) try: # Try to load orthorectified bands band_paths[band] = files.get_file_in_dir( self._get_band_folder(), f"{self.condensed_name}_{bname}.tif", exact_name=True, ) except FileNotFoundError: speckle_band = sbn.corresponding_speckle(band) if speckle_band in self.pol_channels: if sbn.is_despeckle(band): # Despeckle the noisy band band_paths[band] = self._despeckle_sar(speckle_band) else: all_band_paths = self._pre_process_sar(resolution) band_paths = { band: path for band, path in all_band_paths.items() if band in band_list } return band_paths def _get_raw_band_paths(self) -> dict: """ Return the existing band paths (as they come with th archived products). Returns: dict: Dictionary containing the path of every band existing in the raw products """ extended_fmt = _ExtendedFormatter() band_paths = {} for band, band_name in self.band_names.items(): band_regex = extended_fmt.format(self._raw_band_regex, band_name) if self.is_archived: if self.path.endswith(".zip"): # Open the zip file with zipfile.ZipFile(self.path, "r") as zip_ds: # Get the correct band path regex = re.compile(band_regex.replace("*", ".*")) try: band_paths[band] = list( filter( regex.match, [f.filename for f in zip_ds.filelist] ) )[0] except IndexError: continue else: raise InvalidProductError( f"Only zipped products can be processed without extraction: {self.path}" ) else: try: band_paths[band] = files.get_file_in_dir( self._band_folder, band_regex, exact_name=True, get_list=True ) except FileNotFoundError: continue return band_paths def _get_raw_bands(self) -> list: """ Return the existing band paths (as they come with th archived products). Returns: list: List of existing bands in the raw products (vv, hh, vh, hv) """ band_paths = self._get_raw_band_paths() return list(band_paths.keys()) def get_existing_band_paths(self) -> dict: """ Return the existing orthorectified band paths (including despeckle bands). .. WARNING:: This functions orthorectifies SAR bands if not existing ! .. WARNING:: This functions despeckles SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_existing_band_paths() Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. { <SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif', <SarBandNames.VH: 'VH'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VH.tif', <SarBandNames.VV_DSPK: 'VV_DSPK'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV_DSPK.tif', <SarBandNames.VH_DSPK: 'VH_DSPK'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VH_DSPK.tif' } ``` Returns: dict: Dictionary containing the path of every orthorectified bands """ # Get raw bands (maximum number of bands) raw_bands = self._get_raw_bands() possible_bands = raw_bands + [ sbn.corresponding_despeckle(band) for band in raw_bands ] return self.get_band_paths(possible_bands) def get_existing_bands(self) -> list: """ Return the existing orthorectified bands (including despeckle bands). .. WARNING:: This functions orthorectifies SAR bands if not existing ! .. WARNING:: This functions despeckles SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_existing_bands() [<SarBandNames.VV: 'VV'>, <SarBandNames.VH: 'VH'>, <SarBandNames.VV_DSPK: 'VV_DSPK'>, <SarBandNames.VH_DSPK: 'VH_DSPK'>] ``` Returns: list: List of existing bands in the products """ band_paths = self.get_existing_band_paths() return list(band_paths.keys()) # unused band_name (compatibility reasons) # pylint: disable=W0613 @path_or_dst def _read_band( self, path: str, resolution: Union[tuple, list, float] = None, size: Union[list, tuple] = None, ) -> XDS_TYPE: """ Read band from disk. Args: path (str): Band path resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. Returns: XDS_TYPE: Band xarray """ return rasters.read( path, resolution=resolution, size=size, resampling=Resampling.bilinear ) def _load_bands( self, bands: Union[list, BandNames], resolution: float = None, size: Union[list, tuple] = None, ) -> dict: """ Load bands as numpy arrays with the same resolution (and same metadata). Args: bands (list, BandNames): List of the wanted bands 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} """ # Return empty if no band are specified if not bands: return {} # Get band paths if not isinstance(bands, list): bands = [bands] band_paths = self.get_band_paths(bands, resolution) # Open bands and get array (resampled if needed) band_arrays = {} for band_name, band_path in band_paths.items(): # Read CSK band band_arrays[band_name] = self._read_band( band_path, resolution=resolution, size=size ) return band_arrays def _load( self, bands: list, resolution: float = None, size: Union[list, tuple] = None ) -> dict: """ Core function loading SAR 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 = [] dem_list = [] for band in bands: if is_index(band): raise NotImplementedError( "For now, no index is implemented for SAR data." ) elif is_optical_band(band): raise TypeError( f"You should ask for SAR bands as {self.name} is a SAR product." ) elif is_sar_band(band): if not self.has_band(band): raise InvalidBandError( f"{band} cannot be retrieved from {self.condensed_name}" ) else: band_list.append(band) elif is_dem(band): dem_list.append(band) elif is_clouds(band): raise NotImplementedError( f"Clouds cannot be retrieved from SAR data ({self.condensed_name})." ) else: raise InvalidTypeError(f"{band} is neither a band nor an index !") # Load bands bands = self._load_bands(band_list, resolution=resolution, size=size) # Add DEM bands.update(self._load_dem(dem_list, resolution=resolution, size=size)) return bands def _pre_process_sar(self, resolution: float = None) -> dict: """ Pre-process SAR data (geocoding...) Args: resolution (float): Resolution Returns: dict: Dictionary containing {band: path} """ out = {} # Create target dir (tmp dir) with tempfile.TemporaryDirectory() as tmp_dir: # Set command as a list target_file = os.path.join(tmp_dir, f"{self.condensed_name}") # Use dimap for speed and security (ie. GeoTiff's broken georef) pp_target = f"{target_file}" pp_dim = pp_target + ".dim" # Pre-process graph if PP_GRAPH not in os.environ: sat = "s1" if self.sat_id == Platform.S1.name else "sar" spt = "grd" if self.sar_prod_type == SarProductType.GDRG else "cplx" pp_graph = os.path.join( utils.get_data_dir(), f"{spt}_{sat}_preprocess_default.xml" ) else: pp_graph = os.environ[PP_GRAPH] if not os.path.isfile(pp_graph) or not pp_graph.endswith(".xml"): FileNotFoundError(f"{pp_graph} cannot be found.") # Command line if not os.path.isfile(pp_dim): def_res = float(os.environ.get(SAR_DEF_RES, self.resolution)) res_m = resolution if resolution else def_res res_deg = res_m / 10.0 * 8.983152841195215e-5 # Approx cmd_list = snap.get_gpt_cli( pp_graph, [ f"-Pfile={strings.to_cmd_string(self._snap_path)}", f"-Pout={pp_dim}", f"-Pcrs={self.crs()}", f"-Pres_m={res_m}", f"-Pres_deg={res_deg}", ], display_snap_opt=LOGGER.level == logging.DEBUG, ) # Pre-process SAR images according to the given graph LOGGER.debug("Pre-process SAR image") misc.run_cli(cmd_list) # Convert DIMAP images to GeoTiff for pol in self.pol_channels: # Speckle image out[sbn.from_value(pol)] = self._write_sar(pp_dim, pol.value) return out def _despeckle_sar(self, band: sbn) -> str: """ Pre-process SAR data (geocode...) Args: band (sbn): Band to despeckle Returns: str: Despeckled path """ # Create target dir (tmp dir) with tempfile.TemporaryDirectory() as tmp_dir: # Out files target_file = os.path.join(tmp_dir, f"{self.condensed_name}") dspk_dim = target_file + ".dim" # Despeckle graph if DSPK_GRAPH not in os.environ: dspk_graph = os.path.join( utils.get_data_dir(), "sar_despeckle_default.xml" ) else: dspk_graph = os.environ[DSPK_GRAPH] if not os.path.isfile(dspk_graph) or not dspk_graph.endswith(".xml"): FileNotFoundError(f"{dspk_graph} cannot be found.") # Create command line and run it if not os.path.isfile(dspk_dim): path = self.get_band_paths([band])[band] cmd_list = snap.get_gpt_cli( dspk_graph, [f"-Pfile={path}", f"-Pout={dspk_dim}"], display_snap_opt=False, ) # Pre-process SAR images according to the given graph LOGGER.debug("Despeckle SAR image") misc.run_cli(cmd_list) # Convert DIMAP images to GeoTiff out = self._write_sar(dspk_dim, band.value, dspk=True) return out def _write_sar(self, dim_path: str, pol_up: str, dspk=False): """ Write SAR image on disk. Args: dim_path (str): DIMAP path pol_up (str): Polarization name """ pol_up = pol_up.upper() # To be sure # Get .img file path (readable by rasterio) try: img = rasters.get_dim_img_path(dim_path, pol_up) except FileNotFoundError: img = rasters.get_dim_img_path(dim_path) # Maybe not the good name # Open SAR image arr = rasters.read(img) arr = arr.where(arr != self._snap_no_data, np.nan) # Save the file as the terrain-corrected image file_path = os.path.join( self.output, f"{files.get_filename(dim_path)}_{pol_up}{'_DSPK' if dspk else ''}.tif", ) # WARNING: Set nodata to 0 here as it is the value wanted by SNAP ! rasters.write(arr, file_path, dtype=np.float32, nodata=0) return file_path 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. resampling (Resampling): Resampling method size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. Returns: str: Hillshade mask path """ raise InvalidProductError("Impossible to compute hillshade mask for SAR data.") def _has_cloud_band(self, band: BandNames) -> bool: """ Does this products has the specified cloud band ? ```python >>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.has_cloud_band(CLOUDS) False ``` """ return False def _get_condensed_name(self) -> str: """ Get products condensed name ({acq_datetime}_S1_{sensor_mode}_{product_type}). Returns: str: Condensed S1 name """ return f"{self.get_datetime()}_{self.platform.name}_{self.sensor_mode.name}_{self.product_type.value}"
Ancestors
Subclasses
Instance variables
var sar_prod_type
-
SAR product type, either Single Look Complex or Ground Range
var sensor_mode
-
Sensor Mode of the current product
var pol_channels
-
Polarization Channels stored in the current product
var output
-
Inherited from:
Product
.output
Output directory of the product, to write orthorectified data for example.
var 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
-
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
-
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 as03
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 footprint(
self)
-
Inherited from:
Product
.footprint
Get UTM footprint of the products (without nodata, in french == emprise utile) …
Expand source code
def footprint(self) -> gpd.GeoDataFrame: """ Get UTM footprint of the products (without nodata, *in french == emprise utile*) ```python >>> from eoreader.reader import Reader >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip" >>> prod = Reader().open(path) >>> prod.footprint() index geometry 0 0 POLYGON ((199980.000 4500000.000, 199980.000 4... ``` Returns: gpd.GeoDataFrame: Footprint as a GeoDataFrame """ return rasters.get_footprint( self.get_default_band_path() ) # Processed by SNAP: the nodata is set
def get_default_band(
self)
-
Get default band: The first existing one between
VV
andHH
for SAR data.>>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_default_band() <SarBandNames.VV: 'VV'>
Returns
str
- Default band
Expand source code
def get_default_band(self) -> BandNames: """ Get default band: The first existing one between `VV` and `HH` for SAR data. ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_default_band() <SarBandNames.VV: 'VV'> ``` Returns: str: Default band """ existing_bands = self._get_raw_bands() if not existing_bands: raise InvalidProductError(f"No band exists for products: {self.name}") # The order matters, as we almost always prefer VV and HH if sbn.VV in existing_bands: default_band = sbn.VV elif sbn.HH in existing_bands: default_band = sbn.HH elif sbn.VH in existing_bands: default_band = sbn.VH elif sbn.HV in existing_bands: default_band = sbn.HV else: raise InvalidTypeError(f"Invalid bands for products: {existing_bands}") return default_band
def get_default_band_path(
self)
-
Get default band path (the first existing one between
VV
andHH
for SAR data), ready to use (orthorectified)Warning: This functions orthorectifies SAR bands if not existing !
>>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_default_band_path() Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. '20191215T060906_S1_IW_GRD\20191215T060906_S1_IW_GRD_VV.tif'
Returns
str
- Default band path
Expand source code
def get_default_band_path(self) -> str: """ Get default band path (the first existing one between `VV` and `HH` for SAR data), ready to use (orthorectified) .. WARNING:: This functions orthorectifies SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_default_band_path() Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif' ``` Returns: str: Default band path """ default_band = self.get_default_band() band_path = self.get_band_paths([default_band]) return band_path[default_band]
def wgs84_extent(
self)
-
Get the WGS84 extent of the file before any reprojection. This is useful when the SAR pre-process has not been done yet.
>>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.wgs84_extent() Name ... geometry 0 Sentinel-1 Image Overlay ... POLYGON ((0.85336 42.24660, -2.32032 42.65493,... [1 rows x 12 columns]
Returns
gpd.GeoDataFrame
- WGS84 extent as a gpd.GeoDataFrame
Expand source code
@abstractmethod def wgs84_extent(self) -> gpd.GeoDataFrame: """ Get the WGS84 extent of the file before any reprojection. This is useful when the SAR pre-process has not been done yet. ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.wgs84_extent() Name ... geometry 0 Sentinel-1 Image Overlay ... POLYGON ((0.85336 42.24660, -2.32032 42.65493,... [1 rows x 12 columns] ``` Returns: gpd.GeoDataFrame: WGS84 extent as a gpd.GeoDataFrame """ raise NotImplementedError("This method should be implemented by a child class")
def extent(
self)
-
Get UTM extent of the tile
>>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.utm_extent() Name ... geometry 0 Sentinel-1 Image Overlay ... POLYGON ((817914.501 4684349.823, 555708.624 4... [1 rows x 12 columns]
Returns
gpd.GeoDataFrame
- Footprint in UTM
Expand source code
def extent(self) -> gpd.GeoDataFrame: """ Get UTM extent of the tile ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.utm_extent() Name ... geometry 0 Sentinel-1 Image Overlay ... POLYGON ((817914.501 4684349.823, 555708.624 4... [1 rows x 12 columns] ``` Returns: gpd.GeoDataFrame: Footprint in UTM """ # Get WGS84 extent extent_wgs84 = self.wgs84_extent() # Get upper-left corner and deduce UTM proj from it utm = vectors.corresponding_utm_projection( extent_wgs84.bounds.minx, extent_wgs84.bounds.maxy ) extent = extent_wgs84.to_crs(utm) return extent
def crs(
self)
-
Get UTM projection
>>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.utm_crs() CRS.from_epsg(32630)
Returns
crs.CRS
- CRS object
Expand source code
def crs(self) -> crs.CRS: """ Get UTM projection ```python >>> from eoreader.reader import Reader >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.utm_crs() CRS.from_epsg(32630) ``` Returns: crs.CRS: CRS object """ # Get WGS84 extent extent_wgs84 = self.wgs84_extent() # Get upper-left corner and deduce UTM proj from it crs_str = vectors.corresponding_utm_projection( extent_wgs84.bounds.minx, extent_wgs84.bounds.maxy ) return crs.CRS.from_string(crs_str)
def get_band_paths(
self,
band_list,
resolution=None)-
Return the paths of required bands.
Warning: This functions orthorectifies SAR bands if not existing !
>>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_band_paths([VV, HH]) { <SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\20191215T060906_S1_IW_GRD_VV.tif' # HH doesn't exist }
Args
band_list
:list
- List of the wanted bands
resolution
:float
- Band resolution
Returns
dict
- Dictionary containing the path of each queried band
Expand source code
def get_band_paths(self, band_list: list, resolution: float = None) -> dict: """ Return the paths of required bands. .. WARNING:: This functions orthorectifies SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_band_paths([VV, HH]) { <SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif' # HH doesn't exist } ``` Args: band_list (list): List of the wanted bands resolution (float): Band resolution Returns: dict: Dictionary containing the path of each queried band """ band_paths = {} for band in band_list: bname = self.band_names[band] if bname is None: raise InvalidProductError( f"Non existing band ({band.name}) for {self.name}" ) try: # Try to load orthorectified bands band_paths[band] = files.get_file_in_dir( self._get_band_folder(), f"{self.condensed_name}_{bname}.tif", exact_name=True, ) except FileNotFoundError: speckle_band = sbn.corresponding_speckle(band) if speckle_band in self.pol_channels: if sbn.is_despeckle(band): # Despeckle the noisy band band_paths[band] = self._despeckle_sar(speckle_band) else: all_band_paths = self._pre_process_sar(resolution) band_paths = { band: path for band, path in all_band_paths.items() if band in band_list } return band_paths
def get_existing_band_paths(
self)
-
Return the existing orthorectified band paths (including despeckle bands).
Warning: This functions orthorectifies SAR bands if not existing !
Warning: This functions despeckles SAR bands if not existing !
>>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_existing_band_paths() Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. { <SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\20191215T060906_S1_IW_GRD_VV.tif', <SarBandNames.VH: 'VH'>: '20191215T060906_S1_IW_GRD\20191215T060906_S1_IW_GRD_VH.tif', <SarBandNames.VV_DSPK: 'VV_DSPK'>: '20191215T060906_S1_IW_GRD\20191215T060906_S1_IW_GRD_VV_DSPK.tif', <SarBandNames.VH_DSPK: 'VH_DSPK'>: '20191215T060906_S1_IW_GRD\20191215T060906_S1_IW_GRD_VH_DSPK.tif' }
Returns
dict
- Dictionary containing the path of every orthorectified bands
Expand source code
def get_existing_band_paths(self) -> dict: """ Return the existing orthorectified band paths (including despeckle bands). .. WARNING:: This functions orthorectifies SAR bands if not existing ! .. WARNING:: This functions despeckles SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_existing_band_paths() Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. Executing processing graph ....10%....20%....30%....40%....50%....60%....70%....80%....90% done. { <SarBandNames.VV: 'VV'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV.tif', <SarBandNames.VH: 'VH'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VH.tif', <SarBandNames.VV_DSPK: 'VV_DSPK'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VV_DSPK.tif', <SarBandNames.VH_DSPK: 'VH_DSPK'>: '20191215T060906_S1_IW_GRD\\20191215T060906_S1_IW_GRD_VH_DSPK.tif' } ``` Returns: dict: Dictionary containing the path of every orthorectified bands """ # Get raw bands (maximum number of bands) raw_bands = self._get_raw_bands() possible_bands = raw_bands + [ sbn.corresponding_despeckle(band) for band in raw_bands ] return self.get_band_paths(possible_bands)
def get_existing_bands(
self)
-
Return the existing orthorectified bands (including despeckle bands).
Warning: This functions orthorectifies SAR bands if not existing !
Warning: This functions despeckles SAR bands if not existing !
>>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_existing_bands() [<SarBandNames.VV: 'VV'>, <SarBandNames.VH: 'VH'>, <SarBandNames.VV_DSPK: 'VV_DSPK'>, <SarBandNames.VH_DSPK: 'VH_DSPK'>]
Returns
list
- List of existing bands in the products
Expand source code
def get_existing_bands(self) -> list: """ Return the existing orthorectified bands (including despeckle bands). .. WARNING:: This functions orthorectifies SAR bands if not existing ! .. WARNING:: This functions despeckles SAR bands if not existing ! ```python >>> from eoreader.reader import Reader >>> from eoreader.bands.alias import * >>> path = r"S1A_IW_GRDH_1SDV_20191215T060906_20191215T060931_030355_0378F7_3696.zip" >>> prod = Reader().open(path) >>> prod.get_existing_bands() [<SarBandNames.VV: 'VV'>, <SarBandNames.VH: 'VH'>, <SarBandNames.VV_DSPK: 'VV_DSPK'>, <SarBandNames.VH_DSPK: 'VH_DSPK'>] ``` Returns: list: List of existing bands in the products """ band_paths = self.get_existing_band_paths() return list(band_paths.keys())
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 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)-
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)-
Stack bands and index of a products …