Skip to content

Commit

Permalink
Merge pull request #4 from lukasValentin/dev
Browse files Browse the repository at this point in the history
Improved Mapping API and Support for Planet-Scope and Sentinel-1
  • Loading branch information
lukasValentin authored Aug 25, 2022
2 parents 8e4a63f + 6aac98d commit 6823f90
Show file tree
Hide file tree
Showing 76 changed files with 3,825 additions and 483 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ Initial release (to be released).

- Added: Support for Microsoft Planetary Computer (using its STAC)
- Added: Guidelines for Contribution to E:earth_africa:dal
- Added: Sensor core class to work with Planet Scope Super Dove sensors including download capacities
- Added: Sensor core class to work with Sentinel-1 data (Ground Range Detected and Radiometrically Terrain Corrected)
- Added: Metadata and archive handling for Sentinel-1 products (GRD, SLC)
- Fixed: Various issues and bugs in the operational.mapper class (made more generic to allow easy integration of further sensors)
- Added: Fast visualization of time series data (imagery) as part of the mapping module in the operational sub-package
Binary file not shown.
1 change: 1 addition & 0 deletions eodal/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .settings import get_settings
from .sentinel2 import Sentinel2
from .stac_providers import STAC_Providers
13 changes: 12 additions & 1 deletion eodal/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from os.path import join
from pathlib import Path
from pydantic import BaseSettings
from typing import Any

from .stac_providers import STAC_Providers

Expand Down Expand Up @@ -61,6 +62,13 @@ class Settings(BaseSettings):
CREODIAS_USER: str = ""
CREODIAS_PASSWORD: str = ""

# define Planet-API token
PLANET_API_KEY: str = ""
# Planet API URLs
ORDERS_URL: str = 'https://api.planet.com/compute/ops/orders/v2'
DATA_URL: str = 'https://api.planet.com/data/v1'


# metadata base connection details
DB_USER: str = "postgres"
DB_PW: str = "P@ssW0rd!"
Expand All @@ -77,7 +85,10 @@ class Settings(BaseSettings):
LIMIT_ITEMS: int = 5

# change the value of this variable to use a different STAC service provider
STAC_BACKEND = STAC_Providers.MSPC
STAC_BACKEND: Any = STAC_Providers.MSPC # STAC_Providers.AWS

# subscription key for MS-PC (might be required for some data sets like Sentinel-1)
PC_SDK_SUBSCRIPTION_KEY: str = ''

# define logger
CURRENT_TIME: str = datetime.now().strftime("%Y%m%d-%H%M%S")
Expand Down
7 changes: 6 additions & 1 deletion eodal/config/stac_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ class MSPC:
URL: str = "https://planetarycomputer.microsoft.com/api/stac/v1"
S2L1C: str = "sentinel-2-l1c"
S2L2A: str = "sentinel-2-l2a"

S1RTC: str = "sentinel-1-rtc" # radiometric and terrain corrected using PlanetDEM (IW mode)
S1GRD: str = "sentinel-1-grd" # corrected to ground range using ellipsoid model WGS84

class Sentinel2:
product_uri: str = "id"
scene_id: str = "s2:granule_id"
Expand All @@ -58,3 +60,6 @@ class Sentinel2:
sensing_time_fmt: str = "%Y-%m-%dT%H:%M:%S.%fZ"
cloud_cover: str = "eo:cloud_cover"
epsg: str = "proj:epsg"
sun_zenith_angle = "s2:mean_solar_zenith"
sun_azimuth_angle = "s2:mean_solar_azimuth"

23 changes: 19 additions & 4 deletions eodal/core/band.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,6 @@ class Band(object):
`rasterio` compatible representation of essential image metadata
:attrib transform:
`Affine` transform representation of the image geo-localisation
"""

def __init__(
Expand All @@ -378,6 +377,7 @@ def __init__(
nodata: Optional[Union[int, float]] = None,
is_tiled: Optional[Union[int, bool]] = 0,
area_or_point: Optional[str] = "Area",
alias: Optional[str] = ""
):
"""
Constructor to instantiate a new band object.
Expand Down Expand Up @@ -424,6 +424,8 @@ def __init__(
`Point`. When `Area` pixel coordinates refer to the upper left corner of the
pixel, whereas `Point` indicates that pixel coordinates are from the center
of the pixel.
:param alias:
band alias name (optional).
"""

# make sure the passed values are 2-dimensional
Expand Down Expand Up @@ -619,6 +621,7 @@ def from_rasterio(
band_name_dst: Optional[str] = "B1",
vector_features: Optional[Union[Path, gpd.GeoDataFrame]] = None,
full_bounding_box_only: Optional[bool] = False,
epsg_code: Optional[int] = None,
**kwargs,
):
"""
Expand Down Expand Up @@ -657,6 +660,9 @@ def from_rasterio(
if False (default) pixels not covered by the vector features are masked
out using ``maskedArray`` in the back. If True, does not mask pixels
within the spatial bounding box of the `vector_features`.
:param epsg_code:
custom EPSG code of the raster dataset in case the raster has no
internally-described EPSG code or no EPSG code at all.
:param kwargs:
further key-word arguments to pass to `~eodal.core.band.Band`.
:returns:
Expand Down Expand Up @@ -687,7 +693,10 @@ def from_rasterio(
# parse image attributes
attrs = get_raster_attributes(riods=src)
transform = src.meta["transform"]
epsg = src.meta["crs"].to_epsg()
if epsg_code is None:
epsg = src.meta["crs"].to_epsg()
else:
epsg = epsg_code
# check for area or point pixel coordinate definition
if "area_or_point" not in kwargs.keys():
area_or_point = src.tags().get("AREA_OR_POINT", "Area")
Expand Down Expand Up @@ -1348,9 +1357,15 @@ def plot(
# clip data for displaying to central 96% percentile
# TODO: here seems to be a bug with nans in the data ...
if vmin is None:
vmin = np.nanquantile(self.values, 0.02)
try:
vmin = np.nanquantile(self.values, 0.02)
except ValueError:
vmin = self.values.min()
if vmax is None:
vmax = np.nanquantile(self.values, 0.98)
try:
vmax = np.nanquantile(self.values, 0.98)
except ValueError:
vmax = self.values.max()

# actual displaying of the band data
img = ax.imshow(
Expand Down
6 changes: 4 additions & 2 deletions eodal/core/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,8 @@ def _bands_from_selection(
if len(band_names) == 0:
band_names_src = [f"B{idx+1}" for idx in range(band_count)]
else:
band_names_src = band_names
if band_names_src is None:
band_names_src = band_names
# is a selection of bands provided? If no use all available bands
# otherwise check the band indices
if band_names_src is None:
Expand Down Expand Up @@ -998,7 +999,8 @@ def reproject(
band = self.get_band(band_name)
collection.add_band(band_constructor=band.reproject, **kwargs)

return collection
if not inplace:
return collection

@check_band_names
def resample(
Expand Down
151 changes: 135 additions & 16 deletions eodal/core/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"""

import datetime
import numpy as np

from numbers import Number
from typing import Optional

from eodal.utils.constants import ProcessingLevels

Expand All @@ -38,15 +42,18 @@ class SceneProperties(object):
known and applicable)
:attribute product_uri:
unique product (scene) identifier
:attribute mode:
imaging mode of SAR sensors
"""

def __init__(
self,
acquisition_time: datetime.datetime = datetime.datetime(2999, 1, 1),
platform: str = "",
sensor: str = "",
processing_level: ProcessingLevels = ProcessingLevels.UNKNOWN,
product_uri: str = "",
acquisition_time: Optional[datetime.datetime] = datetime.datetime(2999, 1, 1),
platform: Optional[str] = "",
sensor: Optional[str] = "",
processing_level: Optional[ProcessingLevels] = ProcessingLevels.UNKNOWN,
product_uri: Optional[str] = "",
mode: Optional[str] = ""
):
"""
Class constructor
Expand All @@ -62,24 +69,16 @@ def __init__(
known and applicable)
:param product_uri:
unique product (scene) identifier
:attribute mode:
imaging mode of SAR sensors
"""
# type checking first
if not isinstance(acquisition_time, datetime.datetime):
raise TypeError(
f"A datetime.datetime object is required: {acquisition_time}"
)
if not isinstance(platform, str):
raise TypeError(f"A str object is required: {platform}")
if not isinstance(sensor, str):
raise TypeError(f"A str object is required: {sensor}")
if not isinstance(product_uri, str):
raise TypeError(f"A str object is required: {product_uri}")

self.acquisition_time = acquisition_time
self.platform = platform
self.sensor = sensor
self.processing_level = processing_level
self.product_uri = product_uri
self.mode = mode

def __repr__(self) -> str:
return str(self.__dict__)
Expand Down Expand Up @@ -141,3 +140,123 @@ def product_uri(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError("Expected a str object")
self._product_uri = value

@property
def mode(self) -> str:
"""imaging mode of SAR sensors"""
return self._mode

@mode.setter
def mode(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError("Expected a str object")
self._mode = value

# class Sentinel2SceneProperties(SceneProperties):
# """
# Sentinel-2 specific scene properties
#
# :attribute sun_zenith_angle:
# scene-wide sun zenith angle [deg]
# :attribute sun_azimuth_angle:
# scene-wide sun azimuth angle [deg]
# :attribute sensor_zenith_angle:
# scene-wide sensor zenith angle [deg]
# :attribute sensor_azimuth_angle:
# scene-wide sensor azimuth angle [deg]
# """
#
# def __init__(
# self,
# sun_zenith_angle: Optional[float] = np.nan,
# sun_azimuth_angle: Optional[float] = np.nan,
# sensor_zenith_angle: Optional[float] = np.nan,
# sensor_azimuth_angle: Optional[float] = np.nan,
# *args,
# **kwargs
# ):
# """
# Class constructor
#
# :param sun_zenith_angle:
# scene-wide sun zenith angle [deg]
# :param sun_azimuth_angle:
# scene-wide sun azimuth angle [deg]
# :param sensor_zenith_angle:
# scene-wide sensor zenith angle [deg]
# :param sensor_azimuth_angle:
# scene-wide sensor azimuth angle [deg]
# :param args:
# positional arguments to pass to the constructor of the
# super-class
# :param kwargs:
# key-word arguments to pass to the constructor of the
# super-class
# """
# # call constructor of super class
# super().__init__(*args, **kwargs)
#
# self.sun_zenith_angle = sun_zenith_angle
# self.sun_azimuth_angle = sun_azimuth_angle
# self.sensor_zenith_angle = sensor_zenith_angle
# self.sensor_azimuth_angle = sensor_azimuth_angle
#
# @property
# def sun_zenith_angle(self) -> float:
# """sun zenith angle [deg]"""
# return self._sun_zenith_angle
#
# @sun_zenith_angle.setter
# def sun_zenith_angle(self, val: float) -> None:
# """sun zenith angle [deg]"""
# if not isinstance(val, Number):
# raise TypeError('Expected integer of float')
# # plausibility check
# if not 0 <= val <= 90:
# raise ValueError('The sun zenith angle ranges from 0 to 90 degrees')
# self._sun_zenith_angle = val
#
# @property
# def sun_azimuth_angle(self) -> float:
# """sun azimuth angle [deg]"""
# return self._sun_zenith_angle
#
# @sun_azimuth_angle.setter
# def sun_azimuth_angle(self, val: float) -> None:
# """sun azimuth angle [deg]"""
# if not isinstance(val, Number):
# raise TypeError('Expected integer of float')
# # plausibility check
# if not 0 <= val <= 180:
# raise ValueError('The sun azimuth angle ranges from 0 to 180 degrees')
# self._sun_zenith_angle = val
#
# @property
# def sensor_zenith_angle(self) -> float:
# """sensor zenith angle [deg]"""
# return self._sensor_zenith_angle
#
# @sensor_zenith_angle.setter
# def sensor_zenith_angle(self, val: float) -> None:
# """sensor zenith angle [deg]"""
# if not isinstance(val, Number):
# raise TypeError('Expected integer of float')
# # plausibility check
# if not 0 <= val <= 90:
# raise ValueError('The sensor zenith angle ranges from 0 to 90 degrees')
# self._sensor_zenith_angle = val
#
# @property
# def sensor_azimuth_angle(self) -> float:
# """sun azimuth angle [deg]"""
# return self._sensor_zenith_angle
#
# @sensor_azimuth_angle.setter
# def sensor_azimuth_angle(self, val: float) -> None:
# """sun azimuth angle [deg]"""
# if not isinstance(val, Number):
# raise TypeError('Expected integer of float')
# # plausibility check
# if not 0 <= val <= 180:
# raise ValueError('The sensor azimuth angle ranges from 0 to 180 degrees')
# self._sun_zenith_angle = val
2 changes: 2 additions & 0 deletions eodal/core/sensors/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .sentinel1 import Sentinel1
from .sentinel2 import Sentinel2
from .planet_scope import PlanetScope, SuperDove
Loading

0 comments on commit 6823f90

Please sign in to comment.