diff --git a/scenes/core.py b/scenes/core.py index 29335bbda58638c80408b9dd7fd2da77015ad24f..dde5c63b5b6f5191a0f9303435d30c04322e2045 100644 --- a/scenes/core.py +++ b/scenes/core.py @@ -78,7 +78,7 @@ class Source(pyotb.Output): """ for new_app in args: self._app_stack.append(new_app) - return Source(root_imagery=self.root_imagery, out=self._app_stack[-1], parent=self) + return self.__class__(root_imagery=self.root_imagery, out=self._app_stack[-1], parent=self) def drilled(self, msk_vec_file, inside=True, nodata=0): """ diff --git a/scenes/dates.py b/scenes/dates.py index 22cdf07e93ba5957638ab740f0d45fff765ea18b..7c63b28c77791d17b839d90198906b69d2602ab7 100644 --- a/scenes/dates.py +++ b/scenes/dates.py @@ -1,6 +1,34 @@ # -*- coding: utf-8 -*- """ -This module aims to deal with dates +This module aims to deal with dates. + +--- + +The `datetime.datetime` class is used as internal date type. +``` +#!python +dt = datetime.datetime(year=2020, month=12, day=2) +``` +Is equivalent to: +``` +dt = str2datetime("02-12-2020") +dt = str2datetime("2020-12-02") +dt = str2datetime("02/12/2020") +``` + +The `any2datetime` method returns a `datetime.datetime` instance, whatever the input is (`str` or +`datetime.datetime`). +``` +dt1 = datetime.datetime(year=2020, month=12, day=2) +dt2 = str2datetime("02-12-2020") +dt1_2 = any2datetime(dt1) # same +dt2_2 = any2datetime("02-12-2020") # same +``` + +The `get_timestamp` method converts a `datetime.datetime` instance into a number of seconds (int). +``` +ts = get_timestamp(dt) # 1606780800.0 +``` """ import datetime diff --git a/scenes/deepnets.py b/scenes/deepnets.py index c0e063871da5ae71ebb26b82dc80a589dc77c00b..a69cc2aa430461a62518885408b5326667fba410 100644 --- a/scenes/deepnets.py +++ b/scenes/deepnets.py @@ -1,7 +1,24 @@ """ This module provides tools to easily interact with deep learning models. -It is based on OTBTF. +OTBTF is needed to use this module. + +--- + +# Super-resolution + +The SR4RS model can be applied over any `scenes.core.Source` instance. +We recall that this model is intended to be used over Sentinel-2 optical images. +For example, here is how we perform the super-resolution of a Theia S2 product. +``` +#!python +import scenes +archive = "SENTINEL2B_..._T31TEJ_D_V1-8.zip" +s2_scene = scenes.sentinel.Sentinel22AScene(archive) +s2_image = s2_scene.get_imagery().get_10m_bands() +sr = scenes.deepnets.sr4rs(s2_image) # pyotb.core.otbObject +sr.write("sr.tif") +``` """ import os import zipfile diff --git a/scenes/download.py b/scenes/download.py index 7dc533129eeb4da94761a7dbdde31c3840b1b25e..d2b95f6055e88eff8a19168e9fd05999e1e010ee 100644 --- a/scenes/download.py +++ b/scenes/download.py @@ -1,6 +1,65 @@ # -*- coding: utf-8 -*- """ This module handles the download of Sentinel-2 images from Theia (L2A or L3A). + +The `scenes.download.TheiaDownloader` uses Theia credentials. +Those must be stored in a file looking like this: + +``` +serveur = https://theia.cnes.fr/atdistrib +resto = resto2 +token_type = text +login_theia = remi.cresson@irstea.fr +password_theia = thisisnotmyrealpassword +``` + +To instantiate the `scenes.download.TheiaDownloader`: +``` +#!python +import scenes +cfg_file = "config.txt" # Theia config file +theia = scenes.download.TheiaDownloader(cfg_file) +``` + +# Bounding box + temporal range + +The following example shows how to use the `scenes.download.TheiaDownloader` to search +or download all Sentinel-2 images in a bounding box within a temporal range. + +## Search + +When the `download_dir` is not set, the download is not performed, and only the search results +are returned from the function. +``` +bbox = scenes.spatial.BoundingBox(43.706, 43.708, 4.317, 4.420) +trange = ("01/01/2020", "O1/O1/2022") +results = theia.download_in_range(bbox, trange, level="LEVEL2A") +print(results) +``` + +## Download + +To download the files, the `download_dir` must be specified: +``` +theia.download_in_range(bbox, trange, "/tmp/download/", "LEVEL2A") +``` +When files already exist, the md5sum is computed and compared with the one in the catalog, +in order to determine if it has to be downloaded again. If the file is already downloaded and +is complete according to the md5sum, its download is skipped. + +# Bounding box + single date + +In the same manner, the downloader can search or download the closest images from a specific date. +The returned dict from the function is updated with a "delta" key storing the value of the number of +days from the specific date. +``` +# For searching only +results = theia.download_in_range(bbox, trange, "LEVEL2A") + +# For downloading +theia.download_in_range(bbox, trange, "/tmp/download/", "LEVEL2A") +``` + """ import datetime import hashlib diff --git a/scenes/indexation.py b/scenes/indexation.py index 9e65553e34606507b7223fd9cb4a8e8d77419544..e8919e531ed67ce3705cfb75e77076b00e9b6eb1 100644 --- a/scenes/indexation.py +++ b/scenes/indexation.py @@ -1,5 +1,47 @@ """ -This module contains stuff for the spatio-temporal indexation of scenes +This module contains stuff for the spatio-temporal indexation of scenes. + +The `scenes.indexation.Index` uses an R-Tree to perform the indexation of objects in the (x, y, t) domain. + +--- + +Example: spatio-temporal indexation of multiple `scenes.core.Scene` instances. + +# Build a spatio temporal index + +``` +#!python +import scenes +scenes_list = [...] # a list of various `scenes.core.Scene` instances. +index = scenes.indexation.Index(scenes_list) # build the index +``` + +# Search from a `BoundingBox` + +The `find_indices()` method returns the indices of the indexed `scenes.core.Scene` instances matching +the query. +The `find()` method returns the indexed `scenes.core.Scene` instances matching the query. +``` +bbox = BoundingBox(43.706, 43.708, 4.317, 4.420) +indices_results = index.find_indices(bbox) # spatial query returning scenes indices +scenes_results = index.find(bbox) # spatial query returning scenes instances +``` + +# Spatio-temporal search + +A date min and/or a date max can be added in the query. +``` +scenes_results = index.find(bbox, "01/01/2020" "01/01/2022") +``` + +# Search from a vector data file + +The search can also be performed with a vector data file. +``` +vec = "/path/to/vector.gpkg" +scenes_results = index.find(vec, "01/01/2020" "01/01/2022") +``` + """ import datetime import rtree @@ -27,7 +69,8 @@ def _bbox(bbox_wgs84, date_min, date_max): class Index: """ - Stores an indexation structures for a list of Scenes""" + Stores an indexation structure for a list of Scenes + """ def __init__(self, scenes_list): """ Args: diff --git a/scenes/sentinel.py b/scenes/sentinel.py index f2b617209eb1def18fae1e1e36e3fc66543534a2..462a60c93024347d74e08070c9a1fb861dd91e23 100644 --- a/scenes/sentinel.py +++ b/scenes/sentinel.py @@ -2,6 +2,97 @@ This module contains Sentinel-2 classes (sources, imagery, and scenes). Right now everything is ready to use THEIA L2A and L3A products. + +# Scene + +The `Sentinel22AScene` and `Sentinel23AScene` classes carry metadata and imagery +respectively for Sentinel-2 Level 2A and Level 3A products. They both inherit from +the generic abstract `Sentinel2SceneBase` class. + +## Instantiation + +`Sentinel22AScene` and `Sentinel23AScene` are instantiated from the archive (.zip file) +or the product folder. +``` +#!python +import scenes +sc_2a = scenes.sentinel.Sentinel22AScene("SENTINEL2B_..._T31TEJ_D_V1-8.zip") +sc_3a = scenes.sentinel.Sentinel23AScene("SENTINEL2X_...L3A_T31TEJ_D_V1-0.zip") +``` + +## Metadata + +The scene metadata can be accessed with the `get_metadata()` method, like any +`scenes.core.Scene` instance. +``` +md_dict = sc_2a.get_metadata() +for key, value in md_dict.items(): + print(f"{key}: {value}") +``` + +## Imagery + +The imagery can be accessed with the `get_imagery()` method, which returns a +`Sentinel22AImagery` or a `Sentinel23AImagery` instance. +There is a single kind of imagery in `Sentinel22AScene` and `Sentinel23AScene`. +``` +imagery_2a = sc_2a.get_imagery() +imagery_3a = sc_3a.get_imagery() +``` + +## Sources + +The `Sentinel22AImagery` (and `Sentinel23AImagery`) both deliver two kind of `Sentinel22ASource` + (and `Sentinel23ASource`) instances: + +- The 10m spacing channels, in the following order: 4, 3, 2, 8 +- The 20m spacing channels, in the following order: 5, 6, 7, 8a, 11, 12 + +``` +bands_10m_2a = imagery_2a.get_10m_bands() +bands_20m_2a = imagery_2a.get_20m_bands() +bands_10m_3a = imagery_3a.get_10m_bands() +bands_20m_3a = imagery_3a.get_20m_bands() +``` + +The `Sentinel22ASource` implements the `Sentinel22ASource.cld_msk_drilled` method, that +enable to mask the cloud masks over the root source, with the specified +no-data value (default is 0). +The following example show how to derive a child source replacing the +pixels that are in the clouds with pixels at -10000 (which is the no-data +value in Theia products): +``` +drilled = bands_10m_2a.cld_msk_drilled() +``` + +The `Sentinel23ASource` implements the `Sentinel23ASource.flg_msk_drilled` method, that +enable to mask the pixels on a selection of labels of the quality mask. +The following example shows how to mask pixels of anything other that land with -10000: +``` +drilled = bands_10m_3a.flg_msk_drilled(keep_flags_values=(4,)) +``` + +`Sentinel22ASource` and `Sentinel23ASource` inherit from `scenes.core.Source`, +hence implemented sources transformations (e.g. `scenes.core.Source.masked`, +`scenes.core.Source.clip_over_img`, `scenes.core.Source.resample_over`, +`scenes.core.Source.reproject`, etc. +``` +clipped = drilled.clip_over_img(roi) +reprojected = clipped.reproject(epsg=4328) +``` +Note that the resulting transformed `Sentinel22ASource` and `Sentinel23ASource` are still +instances of `Sentinel22ASource` and `Sentinel23ASource` even after generic operations +implemented in `scenes.core.Source`. + +# Usage with pyotb + +As `scenes.core.Source`, it also can be used like any `pyotb.core.otbObject`. +The following example show how to use an OTB application with a source at input. +``` +rgb_nice = pyotb.DynamicConvert(reprojected) +rgb_nice.write("image.tif", pixel_type="uint8") +``` + """ import datetime from abc import abstractmethod @@ -60,12 +151,13 @@ class Sentinel23ASource(Sentinel2Source): Args: keep_flags_values: flags values to keep (Default value = (3, 4)). Can be: - 0 = No data - 1 = Cloud - 2 = Snow - 3 = Water - 4 = Land - (source: https://labo.obs-mip.fr/multitemp/theias-l3a-product-format/) + + - 0 = No data + - 1 = Cloud + - 2 = Snow + - 3 = Water + - 4 = Land + (source: https://labo.obs-mip.fr/multitemp/theias-l3a-product-format/) nodata: nodata value inside holes (Default value = -10000) Returns: diff --git a/scenes/spatial.py b/scenes/spatial.py index 98facf352f820d5852b73eafa5e7523c39cfd342..607ff9884ab0388e236ddec218bb6cc885d6a22b 100644 --- a/scenes/spatial.py +++ b/scenes/spatial.py @@ -1,5 +1,6 @@ """ -This module provides classes and functions to help with light geospatial objects (projections, bounding boxes, etc) +This module provides classes and functions to help with light geospatial objects +(projections, bounding boxes, etc). """ import math from osgeo import osr, ogr diff --git a/scenes/spot.py b/scenes/spot.py index 84bd623db348252f3c81ffdd49ae59cc4b29671a..9765172a89ef00087df0380948e235c4042e5676 100644 --- a/scenes/spot.py +++ b/scenes/spot.py @@ -1,5 +1,88 @@ """ -This module contains Spot 6/7 classes. It implements sources, imagery and scene. +This module contains classes to work with Spot 6/7 products. +In particular, it specializes `scenes.core.Source`, `scenes.core.Imagery` +and `scenes.core.Scene` for Spot 6/7 products. + +--- + +# Scene + +The `Spot67Scene` class carries metadata and imagery for Spot-6/7 sensors. + +## Instantiation + +A `Spot67Scene` is instantiated from the .XML DIMAP files of PAN and XS: +``` +#!python +import scenes +sc = scenes.spot.Spot67Scene(dimap_file_xs="DIM_SPOT6_MS..._1.XML", + dimap_file_pan="DIM_SPOT6_P..._1.XML") +``` + +## Metadata + +The scene metadata can be accessed with the `get_metadata()` method, like any +`scenes.core.Scene` instance. +``` +md_dict = sc.get_metadata() +for key, value in md_dict.items(): + print(f"{key}: {value}") +``` + +## Imagery + +The imagery can be accessed with the `get_imagery()` method, which returns a +`Spot67Imagery` instance. +Two kind of imagery exist: + +- DN (Digital Number): raw sensor values +- TOA (Top Of Atmosphere): calibrated reflectance +``` +imagery_dn = sc.get_imagery(reflectance="dn") # uncalibrated +imagery_toa = sc.get_imagery(reflectance="toa") # TOA reflectance +``` + +## Sources + +The `Spot67Imagery` delivers three kind of `Spot67Source` instances: + +- The multispectral image +- The Panchromatic image +- The pansharpenend image +``` +xs = imagery.get_xs() # Multispectral image Source +pan = imagery.get_pan() # Panchromatic image Source +pxs = imagery.get_pxs(method="bayes") # Pansharpened Source +``` + +The `Spot67Source` implements the `Spot67Source.cld_msk_drilled` method, that +enable to mask the cloud masks over the root source, with the specified +no-data value (default is 0). +The following example show how to derive a child source replacing the +pixels that are in the clouds with zero-valued pixels: +``` +pxs_drilled = pxs.cld_msk_drilled() +``` + +The `Spot67Source` inherits from `scenes.core.Source`, hence implemented +sources transformations (e.g. `scenes.core.Source.masked()`, `scenes.core.Source.clip_over_img()`, +`scenes.core.Source.resample_over()`, `scenes.core.Source.reproject()`, etc. +``` +clipped = pxs_drilled.clip_over_img(roi) +reprojected = clipped.reproject(epsg=4328) +``` +Note that the resulting transformed `Spot67Source` is still an instance of +`Spot67Source` even after generic operations implemented in `scenes.core.Source`. + +# Usage with pyotb + +As `scenes.core.Source`, it also can be used like any `pyotb.core.otbObject`. +The following example show how to use an OTB application with a source at input. +``` +rgb_nice = pyotb.DynamicConvert(reprojected) +rgb_nice.write("image.tif", pixel_type="uint8") +``` + """ import datetime import xml.etree.ElementTree as ET @@ -13,13 +96,13 @@ from scenes.spatial import extent_overlap def find_all_dimaps(pth): """ - Return the list of DIMAPS that are inside all subdirectories of the root directory + Return the list of DIMAPS XML files that are inside all subdirectories of the root directory. Args: pth: root directory Returns: - list of DIMAPS + list of DIMAPS XML files """ return utils.find_files_in_all_subdirs(pth=pth, pattern="DIM_*.XML") @@ -27,7 +110,8 @@ def find_all_dimaps(pth): def get_spot67_scenes(root_dir): """ - Return the list of pairs of PAN/XS DIMAPS + Return the list of scenes that can be instantiated from a root directory containing + a "PAN" and a "MS" subdirectories. Args: root_dir: directory containing "MS" and "PAN" subdirectories @@ -100,7 +184,11 @@ class Spot67Imagery(Imagery): """ Spot 6/7 imagery class. - This class carry the base image source, which can be radiometrically or geometrically corrected. + A `Spot67Imagery` instance carries the following `Spot67Source` instances: + + - xs: the multispectral channels (Red, Gree, Blue, Near infrared @ 6.0 meters spacing) + - pan: the panchromatic channel (Visible domain @ 1.5 meters spacing) + - pxs: the pansharpened channels (Red, Gree, Blue, Near infrared @ 1.5 meters spacing) """ @@ -126,20 +214,34 @@ class Spot67Imagery(Imagery): self.pan = _toa(self.pan) def get_xs(self): - """Returns XS source""" + """ + Returns the MS source + + Returns: + A Spot67Source instance for the MS image + + """ return Spot67Source(self, self.xs) def get_pan(self): - """Returns PAN source""" + """ + Returns PAN source + + Returns: + A Spot67Source instance for the PAN image + + """ return Spot67Source(self, self.pan) def get_pxs(self, method="bayes"): """ + Returns the pansharpened source + Args: method: one of rcs, lmvm, bayes (Default value = "bayes") Returns: - Pansharpened XS source + A Spot67Source instance for the pansharpened image """ xs = self.get_xs() @@ -153,7 +255,8 @@ class Spot67Scene(Scene): """ Spot 6/7 root_scene class. - The class carries all the metadata from the root_scene, and can be used to retrieve its imagery. + The Spot67Scene class carries all metadata and imagery from the scene. + A Spot67Scene object can be instantiated from the XS and PAN DIMAPS (.XML) file. """ PXS_OVERLAP_THRESH = 0.995 @@ -161,8 +264,9 @@ class Spot67Scene(Scene): def __init__(self, dimap_file_xs, dimap_file_pan): """ Args: - dimap_file_xs: DIMAP file for XS - dimap_file_pan: DIMAP file for PAN + dimap_file_xs: XML DIMAP file for the XS product + dimap_file_pan: XML DIMAP file for the PAN product + """ # DIMAP files @@ -298,7 +402,9 @@ class Spot67Scene(Scene): def get_metadata(self): """ - Get metadata + Get metadata. + + The metadata is stored in a dict(). Returns: metadata: dict