diff --git a/scenes/core.py b/scenes/core.py index 007a8dbaba365c0b2deb316fc2792e0efe0d317f..4630e047a68096b181ebeda1e48486e27e96144a 100644 --- a/scenes/core.py +++ b/scenes/core.py @@ -9,34 +9,43 @@ from scenes import utils def save_scenes(scenes_list, pickle_file): - """ - Use pickle to save scenes - :param scenes_list: a list of Scene instances - :param pickle_file: pickle file + """Use pickle to save scenes + + Args: + scenes_list: a list of Scene instances + pickle_file: pickle file + """ pickle.dump(scenes_list, open(pickle_file, "wb")) def load_scenes(pickle_file): - """ - Use pickle to save Spot-6/7 scenes - :param pickle_file: pickle file - :return: list of Scene instances + """Use pickle to save Spot-6/7 scenes + + Args: + pickle_file: pickle file + + Returns: + list of Scene instances + """ return pickle.load(open(pickle_file, "rb")) class Source(pyotb.Output): - """ - Source class. + """Source class. + Holds common operations on image sources (e.g. drill, resample, extract an ROI, etc.) + """ def __init__(self, root_imagery, out, parent=None): """ - :param root_imagery: root Imagery instance - :param out: image to deliver (can be an image filename (str), a pyotb.App, etc.) - :param parent: parent Source instance + Args: + root_imagery: root Imagery instance + out: image to deliver (can be an image filename (str), a pyotb.App, etc.) + parent: parent Source instance + """ assert isinstance(root_imagery, Imagery), "root_imagery type is {}".format(type(root_imagery)) self.root_imagery = root_imagery # root imagery @@ -50,24 +59,33 @@ class Source(pyotb.Output): self._app_stack = [] # list of otb applications or output to keep trace def new_source(self, *args): - """ - Return a new Source instance with new apps added at the end of the pipeline. - :param *args: list of pyotb.app instances to append to the existing pipeline - :return: new source + """Return a new Source instance with new apps added at the end of the pipeline. + + Args: + *args: list of pyotb.app instances to append to the existing pipeline + + Returns: + new source + """ 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) def drilled(self, msk_vec_file, inside=True, nodata=0): - """ - Return the source drilled from the input vector data. + """Return the source drilled from the input vector data. + The default behavior is that the hole is made inside the polygon. This can be changed setting the "inside" parameter to False. - :param msk_vec_file: input vector data filename - :param inside: whether the drill is happening inside the polygon or outside - :param nodata: nodata value inside holes - :return: drilled source + + Args: + msk_vec_file: input vector data filename + inside: whether the drill is happening inside the polygon or outside (Default value = True) + nodata: nodata value inside holes (Default value = 0) + + Returns: + drilled source + """ if utils.open_vector_layer(msk_vec_file): # Vector data not empty @@ -80,12 +98,17 @@ class Source(pyotb.Output): return self # Nothing but a soft copy of the source def masked(self, binary_mask, nodata=0): - """ - Return the source masked from an uint8 binary raster (0 or 1..255). + """Return the source masked from an uint8 binary raster (0 or 1..255). + Pixels are set to "nodata" where the mask values are 0. - :param binary_mask: input mono-band binary raster filename - :param nodata: nodata value for rejected values - :return: masked source + + Args: + binary_mask: input mono-band binary raster filename + nodata: nodata value for rejected values (Default value = 0) + + Returns: + masked source + """ manage_nodata = pyotb.ManageNoData({"in": self, "mode": "apply", @@ -94,12 +117,16 @@ class Source(pyotb.Output): return self.new_source(binary_mask, manage_nodata) def resample_over(self, ref_img, interpolator="bco", nodata=0): - """ - Return the source superimposed over the input image - :param ref_img: reference image - :param interpolator: interpolator - :param nodata: no data value - :return: resampled image source + """Return the source superimposed over the input image + + Args: + ref_img: reference image + interpolator: interpolator (Default value = "bco") + nodata: no data value (Default value = 0) + + Returns: + resampled image source + """ superimpose = pyotb.Superimpose({"inm": self, "inr": ref_img, @@ -108,10 +135,14 @@ class Source(pyotb.Output): return self.new_source(ref_img, superimpose) def clip_over_img(self, ref_img): - """ - Return the source clipped over the ROI specified by the input image extent - :param ref_img: reference image - :return: ROI clipped source + """Return the source clipped over the ROI specified by the input image extent + + Args: + ref_img: reference image + + Returns: + ROI clipped source + """ extract_roi = pyotb.ExtractROI({"in": self, "mode": "fit", @@ -119,21 +150,29 @@ class Source(pyotb.Output): return self.new_source(ref_img, extract_roi) def clip_over_vec(self, ref_vec): - """ - Return the source clipped over the ROI specified by the input vector extent - :param ref_vec: reference vector data - :return: ROI clipped source + """Return the source clipped over the ROI specified by the input vector extent + + Args: + ref_vec: reference vector data + + Returns: + ROI clipped source + """ return self.new_source(pyotb.ExtractROI({"in": self, "mode": "fit", "mode.fit.vec": ref_vec})) def reproject(self, epsg, interpolator="bco"): - """ - Reproject the source into the specified EPSG - :param epsg: EPSG (int) - :param interpolator: interpolator - :return: reprojected source + """Reproject the source into the specified EPSG + + Args: + epsg: EPSG (int) + interpolator: interpolator (Default value = "bco") + + Returns: + reprojected source + """ if self.root_imagery.root_scene.epsg != epsg: return self.new_source(pyotb.Orthorectification({"io.in": self, @@ -144,58 +183,69 @@ class Source(pyotb.Output): class Imagery(ABC): - """ - Imagery class. + """Imagery class. + This class carry the base image source, and additional generic stuff common to all sensors imagery. + """ def __init__(self, root_scene): """ - :param root_scene: The Scene of which the Imagery instance is attached + Args: + root_scene: The Scene of which the Imagery instance is attached + """ self.root_scene = root_scene class Scene(ABC): - """ - Scene class. + """Scene class. + The class carries all the metadata from the scene, and can be used to retrieve its imagery. The get_imagery() function is abstract and must be implemented in child classes. + """ def __init__(self, acquisition_date, bbox_wgs84, epsg): - """ - Constructor - :param acquisition_date: Acquisition date - :param bbox_wgs84: Bounding box, in WGS84 coordinates reference system - :param epsg: EPSG code - """ + """Constructor + + Args: + acquisition_date: Acquisition date + bbox_wgs84: Bounding box, in WGS84 coordinates reference system + epsg: EPSG code + """ assert isinstance(acquisition_date, datetime.datetime), "acquisition_date must be a datetime.datetime instance" self.acquisition_date = acquisition_date self.bbox_wgs84 = bbox_wgs84 assert isinstance(epsg, int), "epsg must be an int" self.epsg = epsg + self.metadata = self.get_metadata() @abstractmethod def get_imagery(self, **kwargs): - """ - Must be implemented in child classes. - Return the imagery. - :param **kwargs: Imagery options - :return: Imagery instance + """Must be implemented in child classes. + + Args: + **kwargs: Imagery options + + Returns: + Imagery instance + """ def get_metadata(self): - """ - Enable one instance to be used with print() - """ + """Enable one instance to be used with print()""" return { "Acquisition date": self.acquisition_date, "Bounding box (WGS84)": self.bbox_wgs84, "EPSG": self.epsg, } + def get_serializable_metadata(self): + """Enable one instance to be used with print()""" + return {k: str(v) for k, v in self.metadata.items()} + def __repr__(self): """ Enable one instance to be used with print() diff --git a/scenes/download.py b/scenes/download.py index 6fb3a047145446d608310377e1f0a4f5cb8f4b7c..eb55009a5f1ed502bea55cf77715cac157fbb81f 100644 --- a/scenes/download.py +++ b/scenes/download.py @@ -13,9 +13,11 @@ import tqdm def compute_md5(fname): - """ - Compute md5sum of a file - :param fname: file name + """Compute md5sum of a file + + Args: + fname: file name + """ hash_md5 = hashlib.md5() with open(fname, "rb") as f: @@ -25,8 +27,12 @@ def compute_md5(fname): def is_file_complete(filename, md5sum): - """ - Tell if a file is complete + """Tell if a file is complete + + Args: + filename: path of the file + md5sum: reference md5 + """ # Does the file exist? if not os.path.isfile(filename): @@ -37,14 +43,18 @@ def is_file_complete(filename, md5sum): def curl_url(url, postdata, verbose=False, fp=None, header=None): - """ - Use PyCurl to make some requests - :param url: url - :param postdata: POST data - :param verbose: verbose (True or False) - :param fp: file handle - :param header: header. If None is kept, ['Accept:application/json'] is used - :return: decoded contents + """Use PyCurl to make some requests + + Args: + url: url + postdata: POST data + verbose: boolean (Default value = False) + fp: file handle (Default value = None) + header: header. If None is kept, ['Accept:application/json'] is used (Default value = None) + + Returns: + decoded contents + """ if not header: header = ['Accept:application/json'] @@ -67,12 +77,14 @@ def curl_url(url, postdata, verbose=False, fp=None, header=None): print("Downloading", flush=True) def _status(download_t, download_d, *_): - """ - callback function for c.XFERINFOFUNCTION + """Callback function for c.XFERINFOFUNCTION https://stackoverflow.com/questions/19724222/pycurl-attachments-and-progress-functions - :param download_t: total - :param download_d: already downloaded - :return: + + Args: + download_t: total + download_d: already downloaded + *_: any additional param (won't be used) + """ if download_d > 0: nonlocal progress_bar, last_download_d @@ -93,13 +105,12 @@ def curl_url(url, postdata, verbose=False, fp=None, header=None): class TheiaDownloader: - """ - THEIA downloader - """ + """THEIA downloader""" def __init__(self, config_file, max_records=500): """ - :param config_file: Theia configuration file - :param max_records: Maximum number of records + Args: + config_file: Theia configuration file + max_records: Maximum number of records """ # Read the Theia config file @@ -129,9 +140,7 @@ class TheiaDownloader: self.max_records = max_records def _get_token(self): - """ - Get the THEIA token - """ + """Get the THEIA token""" postdata_token = {"ident": self.config["login_theia"], "pass": self.config["password_theia"]} url = "{}/services/authenticate/".format(self.config["serveur"]) token = curl_url(url, postdata_token) @@ -140,8 +149,8 @@ class TheiaDownloader: return token def _query(self, dict_query): - """ - Search products + """Search products + Return a dict with the following structure TILE_NAME +---- DATE @@ -149,8 +158,13 @@ class TheiaDownloader: +------ url +------ checksum +------ product_name - :param dict_query: query - :return tile dictionary + + Args: + dict_query: query + + Returns: + tile dictionary + """ url = "{}/{}/api/collections/SENTINEL2/search.json?{}".format(self.config["serveur"], self.config["resto"], @@ -175,18 +189,24 @@ class TheiaDownloader: return tiles_dict def _download_tiles_and_dates(self, tiles_dict, download_dir): - """ - Download a product. + """Download a product. + Updates the "tiles_dict" with the filename of the downloaded files - :param tiles_dict: tiles dictionary. Must have the following structure: - TILE_NAME - +---- DATE - +------ id - +------ url - +------ checksum - +------ product_name - :return: tiles_dict updated with local_file: - +------ local_file + + Args: + tiles_dict: tiles dictionary. Must have the following structure: + TILE_NAME + +---- DATE + +------ id + +------ url + +------ checksum + +------ product_name + download_dir: + + Returns: + tiles_dict updated with local_file: + +------ local_file + """ print("Get token...") token = self._get_token() @@ -212,12 +232,16 @@ class TheiaDownloader: return tiles_dict def download_in_range(self, bbox_wgs84, dates_range, download_dir=None, level="LEVEL3A"): - """ - Download all images within spatial and temporal ranges - :param bbox_wgs84: bounding box (WGS84) - :param dates_range: a tuple of datetime.datetime instances (start_date, end_date) - :param download_dir: download directory - :param level: LEVEL2A, LEVEL3A, ... + """Download all images within spatial and temporal ranges + + Args: + bbox_wgs84: bounding box (WGS84) + dates_range: a tuple of datetime.datetime instances (start_date, end_date) + download_dir: download directory (Default value = None) + level: LEVEL2A, LEVEL3A, ... (Default value = "LEVEL3A") + + Returns: + # TODO """ start_date, end_date = dates_range # lonmin, latmin, lonmax, latmax @@ -240,13 +264,17 @@ class TheiaDownloader: return search_results def download_closest(self, bbox_wgs84, acq_date, download_dir=None, level="LEVEL3A"): - """ - query theia catalog, download_closest the files - :param bbox_wgs84: bounding box (WGS84) - :param acq_date: acquisition date to look around - :param download_dir: download directory - :param level: LEVEL2A, LEVEL3A, ... - :return: downloaded files + """Query Theia catalog, download_closest the files + + Args: + bbox_wgs84: bounding box (WGS84) + acq_date: acquisition date to look around + download_dir: download directory (Default value = None) + level: LEVEL2A, LEVEL3A, ... (Default value = "LEVEL3A") + + Returns: + downloaded files + """ # Important parameters diff --git a/scenes/indexation.py b/scenes/indexation.py index 19689022da4c7e37bb88a21873e7a71d9b3aaf50..6843a177cfa63a727bd45dd199fe61c483ce8f87 100644 --- a/scenes/indexation.py +++ b/scenes/indexation.py @@ -6,20 +6,28 @@ import rtree def get_timestamp(dt): - """ - Converts datetime.datetime into a timestamp (in seconds) - :param dt: datetime.datetime instance - :return: timestamp (in seconds) + """Converts datetime.datetime into a timestamp (in seconds) + + Args: + dt: datetime.datetime instance + + Returns: + timestamp (in seconds) + """ return dt.replace(tzinfo=datetime.timezone.utc).timestamp() def new_bbox(bbox_wgs84, dt): - """ - Return a bounding box in the domain (lat, lon, time) - :param bbox_wgs84: Bounding box (in WGS84) - :param dt: date datetime.datetime - :return: item for rtree + """Return a bounding box in the domain (lat, lon, time) + + Args: + bbox_wgs84: Bounding box (in WGS84) + dt: date datetime.datetime + + Returns: + item for rtree + """ dt_min = dt - datetime.timedelta(days=1) dt_max = dt + datetime.timedelta(days=1) @@ -28,24 +36,28 @@ def new_bbox(bbox_wgs84, dt): def bbox(bbox_wgs84, dt_min, dt_max): - """ - Return a bounding box in the domain (lat, lon, time) - :param bbox_wgs84: Bounding box (in WGS84) - :param dt_min: date min (datetime.datetime) - :param dt_max: date max (datetime.datetime) - :return: item for rtree + """Return a bounding box in the domain (lat, lon, time) + + Args: + bbox_wgs84: Bounding box (in WGS84) + dt_min: date min (datetime.datetime) + dt_max: date max (datetime.datetime) + + Returns: + item for rtree + """ (xmin, xmax, ymin, ymax) = bbox_wgs84 return xmin, ymin, get_timestamp(dt_min), xmax, ymax, get_timestamp(dt_max) class Index: - """ - Stores an indexation structures for a list of Scenes - """ + """Stores an indexation structures for a list of Scenes""" def __init__(self, scenes_list): """ - :param scenes_list: list of scenes + Args: + scenes_list: list of scenes + """ self.scenes_list = scenes_list @@ -57,12 +69,16 @@ class Index: self.index.insert(scene_idx, new_bbox(bbox_wgs84=scene.bbox_wgs84, dt=scene.acquisition_date)) def find_indices(self, bbox_wgs84, dt_min=None, dt_max=None): - """ - Search the intersecting elements, and return their indices - :param bbox_wgs84: bounding box (WGS84) - :param dt_min: date min (datetime.datetime) - :param dt_max: date max (datetime.datetime) - :return: list of indices + """Search the intersecting elements, and return their indices + + Args: + bbox_wgs84: bounding box (WGS84) + dt_min: date min (datetime.datetime) (Default value = None) + dt_max: date max (datetime.datetime) (Default value = None) + + Returns: + list of indices + """ if not dt_min: dt_min = datetime.datetime.strptime("2000-01-01", "%Y-%m-%d") @@ -72,12 +88,16 @@ class Index: return self.index.intersection(bbox_search) def find(self, bbox_wgs84, dt_min=None, dt_max=None): - """ - Search the intersecting elements, and return them - :param bbox_wgs84: bounding box (WGS84) - :param dt_min: date min (datetime.datetime) - :param dt_max: date max (datetime.datetime) - :return: list of indices + """Search the intersecting elements, and return them + + Args: + bbox_wgs84: bounding box (WGS84) + dt_min: date min (datetime.datetime) (Default value = None) + dt_max: date max (datetime.datetime) (Default value = None) + + Returns: + list of indices + """ indices = self.find_indices(bbox_wgs84=bbox_wgs84, dt_min=dt_min, dt_max=dt_max) return [self.scenes_list[i] for i in indices] diff --git a/scenes/raster_manips.py b/scenes/raster_manips.py index 7218037423e3fee692eda5f14abd20170f58a66b..c97199ab5c87fc95ec645a100decf7cd23e52e13 100644 --- a/scenes/raster_manips.py +++ b/scenes/raster_manips.py @@ -7,14 +7,18 @@ import pyotb def align_on_raster(in_image, ref_image, spacing_ratio, interpolator="nn", nodata=0): - """ - Return a raster which is aligned on the reference, with the given spacing ratio - :param in_image: input image - :param ref_image: reference image - :param spacing_ratio: spacing ratio. When > 1, the output has larger pixel. When < 1, the output has smaller pixels. - :param interpolator: interpolator (nn/bco/linear) - :param nodata: no data value outside image - :return: output aligned image + """Return a raster which is aligned on the reference, with the given spacing ratio + + Args: + in_image: input image + ref_image: reference image + spacing_ratio: spacing ratio. When > 1, the output has larger pixel. When < 1, the output has smaller pixels. + interpolator: interpolator (nn/bco/linear) (Default value = "nn") + nodata: no data value outside image (Default value = 0) + + Returns: + output aligned image + """ def _get_gt(image): diff --git a/scenes/sentinel.py b/scenes/sentinel.py index ae909378955c787a97a0d828fbaa0c0d43e35b16..1774733f9315dadaa824cf1cb6e5791db63ed8e7 100644 --- a/scenes/sentinel.py +++ b/scenes/sentinel.py @@ -9,18 +9,20 @@ from scenes.core import Source, Imagery, Scene class Sentinel2Source(Source): - """ - Class for generic Sentinel-2 sources - """ + """Class for generic Sentinel-2 sources""" R1_SIZE = 10980 R2_SIZE = 5490 def msk_drilled(self, msk_dict, exp, nodata=0): """ - :param msk_dict: dict of masks - :param exp: bandmath expression to form the 0-255 binary mask - :param nodata: no-data value in masked output - :return: new masked source + Args: + msk_dict: dict of masks + exp: bandmath expression to form the 0-255 binary mask + nodata: no-data value in masked output (Default value = 0) + + Returns: + new masked source + """ img_size = pyotb.ReadImageInfo(self).GetParameterInt('sizex') bm = pyotb.BandMath({"il": msk_dict[img_size], "exp": exp}) @@ -28,15 +30,17 @@ class Sentinel2Source(Source): class Sentinel22ASource(Sentinel2Source): - """ - Sentinel-2 level 2A source class - """ + """Sentinel-2 level 2A source class""" def cld_msk_drilled(self, nodata=-10000): - """ - Return the source drilled from the cloud mask - :param nodata: nodata value inside holes - :return: drilled source + """Return the source drilled from the cloud mask + + Args: + nodata: nodata value inside holes (Default value = -10000) + + Returns: + drilled source + """ return self.msk_drilled(msk_dict={self.R1_SIZE: self.root_imagery.root_scene.cld_r1_msk_file, self.R2_SIZE: self.root_imagery.root_scene.cld_r2_msk_file}, @@ -45,22 +49,24 @@ class Sentinel22ASource(Sentinel2Source): class Sentinel23ASource(Sentinel2Source): - """ - Sentinel-2 level 3A source class - """ + """Sentinel-2 level 3A source class""" def flg_msk_drilled(self, keep_flags_values=(3, 4), nodata=-10000): - """ - Return the source drilled from the FLG mask - :param keep_flags_values: flags values to keep. Can be: - 0 = No data - 1 = Cloud - 2 = Snow - 3 = Water - 4 = Land - (source: https://labo.obs-mip.fr/multitemp/theias-l3a-product-format/) - :param nodata: nodata value inside holes - :return: drilled source + """Return the source drilled from the FLG mask + + 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/) + nodata: nodata value inside holes (Default value = -10000) + + Returns: + drilled source + """ exp = "||".join(["im1b1=={}".format(val) for val in keep_flags_values]) + "?255:0" return self.msk_drilled(msk_dict={self.R1_SIZE: self.root_imagery.root_scene.flg_r1_msk_file, @@ -70,23 +76,17 @@ class Sentinel23ASource(Sentinel2Source): class Sentinel2ImageryBase(Imagery): - """ - Base class for Sentinel-2 level 2A imagery classes. - """ + """Base class for Sentinel-2 level 2A imagery classes.""" def _concatenate_10m_bands(self): - """ - :return: 10m spacing bands stacking pipeline - """ + """Returns 10m spacing bands stacking pipeline""" return pyotb.ConcatenateImages([self.root_scene.band4_file, self.root_scene.band3_file, self.root_scene.band2_file, self.root_scene.band8_file]) def _concatenate_20m_bands(self): - """ - :return: 20m spacing bands stacking pipeline - """ + """Returns 20m spacing bands stacking pipeline""" return pyotb.ConcatenateImages([self.root_scene.band5_file, self.root_scene.band6_file, self.root_scene.band7_file, @@ -96,50 +96,38 @@ class Sentinel2ImageryBase(Imagery): class Sentinel22AImagery(Sentinel2ImageryBase): - """ - Sentinel-2 level 2A class. - """ + """Sentinel-2 level 2A class.""" def get_10m_bands(self): - """ - :return: 10m spacing bands - """ + """Returns 10m spacing bands""" return Sentinel22ASource(self, self._concatenate_10m_bands()) def get_20m_bands(self): - """ - :return: 20m spacing bands - """ + """Returns 20m spacing bands""" return Sentinel22ASource(self, self._concatenate_20m_bands()) class Sentinel23AImagery(Sentinel2ImageryBase): - """ - Sentinel-2 level 2A class. - """ + """Sentinel-2 level 2A class.""" def get_10m_bands(self): - """ - :return: 10m spacing bands - """ + """Returns 10m spacing bands""" return Sentinel23ASource(self, self._concatenate_10m_bands()) def get_20m_bands(self): - """ - :return: 20m spacing bands - """ + """Returns 20m spacing bands""" return Sentinel23ASource(self, self._concatenate_20m_bands()) class Sentinel2SceneBase(Scene): - """ - Base class for Sentinel-2 images - """ + """Base class for Sentinel-2 images""" @abstractmethod def __init__(self, archive, tag): """ - :param archive: product .zip or directory + Args: + archive: product .zip or directory + tag: pattern to match in filenames, e.g. "FRE" """ self.archive = archive @@ -177,11 +165,17 @@ class Sentinel2SceneBase(Scene): super().__init__(acquisition_date=acquisition_date, bbox_wgs84=bbox_wgs84, epsg=epsg) def get_file(self, endswith): - """ - Return the specified file. - Throw an exception if none or multiple candidates are found. - :param endswith: filtered extension - :return: the file + """Return the specified file. + + Args: + endswith: filtered extension + + Returns: + the file + + Raises: + ValueError: If none or multiple candidates are found. + """ filtered_files_list = [f for f in self.files if f.endswith(endswith)] nb_matches = len(filtered_files_list) @@ -191,19 +185,27 @@ class Sentinel2SceneBase(Scene): return filtered_files_list[0] def get_band(self, suffix1, suffix2): - """ - Return the file path for the specified band. - Throw an exception if none or multiple candidates are found. - :param suffix1: suffix 1, e.g. "FRE" - :param suffix2: suffix 2, e.g. "B3" - :return: file path for the band + """Return the file path for the specified band. + + Args: + suffix1: suffix 1, e.g. "FRE" + suffix2: suffix 2, e.g. "B3" + + Returns: + file path for the band + + Raises: + ValueError: If none or multiple candidates are found. + """ return self.get_file(endswith="_{}_{}.tif".format(suffix1, suffix2)) def get_metadata(self): - """ - Return the metadata - :return: metadata (dict) + """Get metadata + + Returns: + metadata: dict + """ metadata = super().get_metadata() metadata.update({ @@ -223,14 +225,16 @@ class Sentinel2SceneBase(Scene): class Sentinel22AScene(Sentinel2SceneBase): - """ - Sentinel-2 level 2A scene class. + """Sentinel-2 level 2A scene class. + The class carries all the metadata from the root_scene, and can be used to retrieve its imagery. + """ def __init__(self, archive): """ - :param archive: .zip file or folder. Must be a product from MAJA. + Args: + archive: .zip file or folder. Must be a product from MAJA. """ # Call parent constructor super().__init__(archive=archive, tag="FRE") @@ -242,16 +246,20 @@ class Sentinel22AScene(Sentinel2SceneBase): self.edg_r2_msk_file = self.get_file("_EDG_R2.tif") def get_imagery(self): # pylint: disable=arguments-differ - """ - Return the Sentinel-2 level 2A imagery - :return: Imagery instance + """ Get imagery + + Returns: + Imagery instance + """ return Sentinel22AImagery(self) def get_metadata(self): - """ - Return the metadata - :return: metadata (dict) + """ Get metadatas + + Returns: + metadata: dict + """ metadata = super().get_metadata() metadata.update({ @@ -264,14 +272,16 @@ class Sentinel22AScene(Sentinel2SceneBase): class Sentinel23AScene(Sentinel2SceneBase): - """ - Sentinel-2 level 3A scene class. + """Sentinel-2 level 3A scene class. + The class carries all the metadata from the root_scene, and can be used to retrieve its imagery. + """ def __init__(self, archive): """ - :param archive: .zip file or folder. Must be a product from WASP. + Args: + archive: .zip file or folder. Must be a product from WASP. """ super().__init__(archive=archive, tag="FRC") @@ -280,16 +290,20 @@ class Sentinel23AScene(Sentinel2SceneBase): self.flg_r2_msk_file = self.get_file("_FLG_R2.tif") def get_imagery(self): # pylint: disable=arguments-differ - """ - Return the Sentinel-2 level 3A imagery - :return: Imagery instance + """ Get imagery + + Returns: + Imagery instance + """ return Sentinel23AImagery(self) def get_metadata(self): - """ - Return the metadata - :return: metadata (dict) + """ Get metadatas + + Returns: + metadata: dict + """ metadata = super().get_metadata() metadata.update({ diff --git a/scenes/spot.py b/scenes/spot.py index 6faa498420caeda066a29e32b96c7dd175e5dc2a..0a573c5d8c0bd36519ea1f74b570e6b1502e0809 100644 --- a/scenes/spot.py +++ b/scenes/spot.py @@ -10,19 +10,27 @@ from scenes.core import Source, Imagery, Scene def find_all_dimaps(pth): - """ - Return the list of DIMAPS that are inside all subdirectories of the root directory - :param pth: root directory - :return: list of DIMAPS + """Return the list of DIMAPS that are inside all subdirectories of the root directory + + Args: + pth: root directory + + Returns: + list of DIMAPS + """ return utils.find_files_in_all_subdirs(pth=pth, pattern="DIM_*.XML") def get_spot67_scenes(root_dir): - """ - Return the list of pairs of PAN/XS DIMAPS - :param root_dir: directory containing "MS" and "PAN" subdirectories - :return: list of Spot67Scenes instances + """Return the list of pairs of PAN/XS DIMAPS + + Args: + root_dir: directory containing "MS" and "PAN" subdirectories + + Returns: + list of Spot67Scenes instances + """ # List files look_dir = root_dir + "/MS" @@ -66,29 +74,33 @@ def get_spot67_scenes(root_dir): class Spot67Source(Source): - """ - Spot 6/7 source class - """ + """Spot 6/7 source class""" def cld_msk_drilled(self, nodata=0): - """ - Return the source drilled from the cloud mask - :param nodata: nodata value inside holes - :return: drilled source + """Return the source drilled from the cloud mask + + Args: + nodata: nodata value inside holes (Default value = 0) + + Returns: + drilled source + """ return self.drilled(msk_vec_file=self.root_imagery.root_scene.cld_msk_file, nodata=nodata) class Spot67Imagery(Imagery): - """ - Spot 6/7 imagery class. + """Spot 6/7 imagery class. + This class carry the base image source, which can be radiometrically or geometrically corrected. + """ def __init__(self, root_scene, reflectance): """ - :param root_scene: The Scene of which the Imagery instance is attached - :param reflectance: optional level of reflectance (can be "dn", "toa") + Args: + root_scene: The Scene of which the Imagery instance is attached + reflectance: optional level of reflectance (can be "dn", "toa") """ super().__init__(root_scene=root_scene) @@ -105,20 +117,21 @@ class Spot67Imagery(Imagery): self.pan = _toa(self.pan) def get_xs(self): - """ - :return: XS source - """ + """Returns XS source""" return Spot67Source(self, self.xs) def get_pan(self): - """ - :return: PAN source - """ + """Returns PAN source""" return Spot67Source(self, self.pan) def get_pxs(self, method="bayes"): """ - :return: Pansharpened XS source + Args: + method: one of rcs, lmvm, bayes (Default value = "bayes") + + Returns: + Pansharpened XS source + """ xs = self.get_xs() new_app = pyotb.BundleToPerfectSensor({"inp": self.pan, "inxs": xs, "method": method}) @@ -128,16 +141,18 @@ class Spot67Imagery(Imagery): class Spot67Scene(Scene): - """ - Spot 6/7 root_scene class. + """Spot 6/7 root_scene class. + The class carries all the metadata from the root_scene, and can be used to retrieve its imagery. + """ PXS_OVERLAP_THRESH = 0.995 def __init__(self, dimap_file_xs, dimap_file_pan): """ - :param dimap_file_xs: DIMAP file for XS - :param dimap_file_pan: DIMAP file for PAN + Args: + dimap_file_xs: DIMAP file for XS + dimap_file_pan: DIMAP file for PAN """ # DIMAP files @@ -151,11 +166,18 @@ class Spot67Scene(Scene): # Cloud masks def _get_mask(dimap_file, pattern): - """ - Retrieve GML file. - An error is thrown when multiple files are found. - :param dimap_file: DIMAP file - :param pattern: vector data file pattern + """Retrieve GML file. + + Args: + dimap_file: DIMAP file + pattern: vector data file pattern + + Returns: + path of the file + + Raises: + FileNotFoundError: when multiple files are found. + """ cld_path = utils.get_parent_directory(dimap_file) + "/MASKS" plist = utils.find_file_in_dir(cld_path, pattern=pattern) @@ -247,22 +269,30 @@ class Spot67Scene(Scene): def has_partial_pxs_overlap(self): """ - :return: True if at least PAN or XS imagery lies completely inside the other one. False else. + Returns: + True if at least PAN or XS imagery lies completely inside the other one. False else. + """ return self.pxs_overlap < self.PXS_OVERLAP_THRESH def get_imagery(self, reflectance="dn"): # pylint: disable=arguments-differ - """ - Return the Spot 6/7 imagery - :param reflectance: optional level of reflectance - :return: Imagery instance + """Return the Spot 6/7 imagery + + Args: + reflectance: optional level of reflectance (Default value = "dn") + + Returns: + Imagery instance + """ return Spot67Imagery(self, reflectance=reflectance) def get_metadata(self): - """ - Return the metadata - :return: metadata (dict) + """ Get metadatas + + Returns: + metadata: dict + """ metadata = super().get_metadata() metadata.update({ diff --git a/scenes/utils.py b/scenes/utils.py index 198ddc45e18cf980d8e58afa6bf6d73658b2ce98..d15d88b52c75a16428c141726890ca08c64f0b06 100644 --- a/scenes/utils.py +++ b/scenes/utils.py @@ -12,10 +12,14 @@ from osgeo import osr, ogr, gdal def epsg2srs(epsg): - """ - Return a Spatial Reference System corresponding to an EPSG - :param epsg: EPSG (int) - :return: OSR spatial reference + """Return a Spatial Reference System corresponding to an EPSG + + Args: + epsg: EPSG (int) + + Returns: + OSR spatial reference + """ srs = osr.SpatialReference() srs.ImportFromEPSG(epsg) @@ -23,10 +27,14 @@ def epsg2srs(epsg): def get_epsg(gdal_ds): - """ - Get the EPSG code of a GDAL dataset - :param gdal_ds: GDAL dataset - :return: EPSG code (int) + """Get the EPSG code of a GDAL dataset + + Args: + gdal_ds: GDAL dataset + + Returns: + EPSG code (int) + """ proj = osr.SpatialReference(wkt=gdal_ds.GetProjection()) epsg = proj.GetAttrValue('AUTHORITY', 1) @@ -35,10 +43,14 @@ def get_epsg(gdal_ds): def get_extent(gdal_ds): - """ - Return list of corner coordinates from a GDAL dataset - :param gdal_ds: GDAL dataset - :return: list of coordinates + """Return list of corner coordinates from a GDAL dataset + + Args: + gdal_ds: GDAL dataset + + Returns: + list of coordinates + """ xmin, xpixel, _, ymax, _, ypixel = gdal_ds.GetGeoTransform() width, height = gdal_ds.RasterXSize, gdal_ds.RasterYSize @@ -49,11 +61,16 @@ def get_extent(gdal_ds): def reproject_coords(coords, src_srs, tgt_srs): - """ - Reproject a list of x,y coordinates. - :param coords: list of (x, y) tuples - :param src_srs: source CRS - :param tgt_srs: target CRS + """Reproject a list of x,y coordinates. + + Args: + coords: list of (x, y) tuples + src_srs: source CRS + tgt_srs: target CRS + + Returns: + trans_coords: coordinates in target CRS + """ trans_coords = [] transform = osr.CoordinateTransformation(src_srs, tgt_srs) @@ -65,10 +82,14 @@ def reproject_coords(coords, src_srs, tgt_srs): def get_extent_wgs84(gdal_ds): - """ - Returns the extent in WGS84 CRS from a GDAL dataset - :param gdal_ds: GDAL dataset - :return: extent coordinates in WGS84 CRS + """Returns the extent in WGS84 CRS from a GDAL dataset + + Args: + gdal_ds: GDAL dataset + + Returns: + extent: coordinates in WGS84 CRS + """ coords = get_extent(gdal_ds) src_srs = osr.SpatialReference() @@ -79,10 +100,14 @@ def get_extent_wgs84(gdal_ds): def extent_to_bbox(extent): - """ - Converts an extent into a bounding box - :param extent: extent - :return: bounding box (xmin, xmax, ymin, ymax) + """Converts an extent into a bounding box + + Args: + extent: extent + + Returns: + bounding box (xmin, xmax, ymin, ymax) + """ xmin, xmax = math.inf, -math.inf ymin, ymax = math.inf, -math.inf @@ -96,10 +121,14 @@ def extent_to_bbox(extent): def get_bbox_wgs84_from_gdal_ds(gdal_ds): - """ - Returns the bounding box in WGS84 CRS from a GDAL dataset - :param gdal_ds: GDAL dataset - :return: bounding box (xmin, xmax, ymin, ymax) + """Returns the bounding box in WGS84 CRS from a GDAL dataset + + Args: + gdal_ds: GDAL dataset + + Returns: + bounding box (xmin, xmax, ymin, ymax) + """ extend_wgs84 = get_extent_wgs84(gdal_ds) @@ -107,10 +136,14 @@ def get_bbox_wgs84_from_gdal_ds(gdal_ds): def get_epsg_extent_bbox(filename): - """ - Returns (epsg, extent_wgs84) from a raster file that GDAL can open. - :param filename: file name - :return: (epsg, extent_wgs84) + """Returns (epsg, extent_wgs84) from a raster file that GDAL can open. + + Args: + filename: file name + + Returns: + (epsg, extent_wgs84, bbox_wgs84) + """ gdal_ds = gdal.Open(filename) epsg = get_epsg(gdal_ds) @@ -121,10 +154,14 @@ def get_epsg_extent_bbox(filename): def coords2poly(coords): - """ - Converts a list of coordinates into a polygon - :param coords: list of (x, y) coordinates - :return: a polygon + """Converts a list of coordinates into a polygon + + Args: + coords: list of (x, y) coordinates + + Returns: + a polygon + """ ring = ogr.Geometry(ogr.wkbLinearRing) for coord in coords + [coords[0]]: @@ -137,10 +174,14 @@ def coords2poly(coords): def poly_union(layer): - """ - Compute the union all the geometrical features of layer. - :param layer: The layer - :return The union of the layer's polygons (as a geometry) + """Compute the union all the geometrical features of layer. + + Args: + layer: The layer + + Returns: + the union of the layer's polygons (as a geometry) + """ union1 = None for feat in layer: @@ -154,11 +195,15 @@ def poly_union(layer): def poly_overlap(poly, other_poly): - """ - Returns the ratio of polygons overlap. - :param poly: polygon - :param other_poly: other polygon - :return: overlap (in the [0, 1] range). 0 -> no overlap with other_poly, 1 -> poly is completely inside other_poly + """Returns the ratio of polygons overlap. + + Args: + poly: polygon + other_poly: other polygon + + Returns: + overlap (in the [0, 1] range). 0 -> no overlap with other_poly, 1 -> poly is completely inside other_poly + """ inter = poly.Intersection(other_poly) @@ -166,11 +211,15 @@ def poly_overlap(poly, other_poly): def extent_overlap(extent, other_extent): - """ - Returns the ratio of extents overlap. - :param extent: extent - :param other_extent: other extent - :return: overlap (in the [0, 1] range). 0 -> no overlap with other_extent, 1 -> extent lies inside other_extent + """Returns the ratio of extents overlap. + + Args: + extent: extent + other_extent: other extent + + Returns: + overlap (in the [0, 1] range). 0 -> no overlap with other_extent, 1 -> extent lies inside other_extent + """ poly = coords2poly(extent) other_poly = coords2poly(other_extent) @@ -178,10 +227,14 @@ def extent_overlap(extent, other_extent): def open_vector_layer(vector_file): - """ - Return the vector dataset from a vector file. If the vector is empty, None is returned. - :param vector_file: input vector file - :return: ogr ds, or None (if error) + """Return the vector dataset from a vector file. If the vector is empty, None is returned. + + Args: + vector_file: input vector file + + Returns: + ogr ds, or None (if error) + """ poly_ds = ogr.Open(vector_file) if poly_ds is None: @@ -190,10 +243,14 @@ def open_vector_layer(vector_file): def get_bbox_wgs84_from_vector(vector_file): - """ - Returns the bounding box in WGS84 CRS from a vector data - :param vector_file: vector data filename - :return: bounding box in WGS84 CRS + """Returns the bounding box in WGS84 CRS from a vector data + + Args: + vector_file: vector data filename + + Returns: + bounding box in WGS84 CRS + """ poly_ds = open_vector_layer(vector_file=vector_file) poly_layer = poly_ds.GetLayer() @@ -208,12 +265,16 @@ def get_bbox_wgs84_from_vector(vector_file): def find_files_in_all_subdirs(pth, pattern, case_sensitive=True): - """ - Returns the list of files matching the pattern in all subdirectories of pth - :param pth: path - :param pattern: pattern - :param case_sensitive: case sensitive True or False - :return: list of str + """Returns the list of files matching the pattern in all subdirectories of pth + + Args: + pth: path + pattern: pattern + case_sensitive: boolean (Default value = True) + + Returns: + list of str + """ result = [] reg_expr = re.compile(fnmatch.translate(pattern), 0 if case_sensitive else re.IGNORECASE) @@ -223,20 +284,28 @@ def find_files_in_all_subdirs(pth, pattern, case_sensitive=True): def find_file_in_dir(pth, pattern): - """ - Returns the list of files matching the pattern in the input directory - :param pth: path - :param pattern: pattern - :return: list of str + """Returns the list of files matching the pattern in the input directory + + Args: + pth: path + pattern: pattern + + Returns: + list of str + """ return glob.glob(os.path.join(pth, pattern)) def get_parent_directory(pth): - """ - Return the parent directory of the input directory or file - :param pth: input directory or file - :return: parent directory + """Return the parent directory of the input directory or file + + Args: + pth: input directory or file + + Returns: + parent directory + """ path = pathlib.Path(pth) if not path: @@ -245,11 +314,15 @@ def get_parent_directory(pth): def list_files_in_zip(filename, endswith=None): - """ - List files in zip archive - :param filename: path of the zip - :param endswith: optional, end of filename to be matched - :return: list of the filepaths + """List files in zip archive + + Args: + filename: path of the zip + endswith: optional, end of filename to be matched (Default value = None) + + Returns: + list of filepaths + """ with zipfile.ZipFile(filename) as zip_file: filelist = zip_file.namelist() @@ -260,19 +333,27 @@ def list_files_in_zip(filename, endswith=None): def to_vsizip(zipfn, relpth): - """ - Create path from zip file - :param zipfn: zip archive - :param relpth: relative path (inside archive) - :return: vsizip path + """Create path from zip file + + Args: + zipfn: zip archive + relpth: relative path (inside archive) + + Returns: + vsizip path + """ return "/vsizip/{}/{}".format(zipfn, relpth) def basename(pth): - """ - Returns the basename. Works with files and paths - :param pth: path - :return: basename of the path + """Returns the basename. Works with files and paths + + Args: + pth: path + + Returns: + basename of the path + """ return str(pathlib.Path(pth).name) diff --git a/test/scenes_test_base.py b/test/scenes_test_base.py index bad32a80f9d1ab7c8557c8d8ca3e857899fd0da4..1a46f3383e90bf4f81a4ca9aa52cdf669c90f7a6 100644 --- a/test/scenes_test_base.py +++ b/test/scenes_test_base.py @@ -40,8 +40,12 @@ class ScenesTestBase(ABC, unittest.TestCase): def compare_file(self, file, reference): """ Compare two files - :param file: file to compare - :param reference: baseline - :return: boolean + + Args: + file: file to compare + reference: baseline + + Return: + a boolean """ self.assertTrue(filecmp.cmp(file, reference))