From 74473e8440f37c04ad061a585ddf55fc7c9c0e2c Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 16:57:27 +0100 Subject: [PATCH 01/30] ENH: remove useless encoding declaration --- pyotb/apps.py | 1 - pyotb/core.py | 1 - pyotb/functions.py | 1 - pyotb/helpers.py | 1 - 4 files changed, 4 deletions(-) diff --git a/pyotb/apps.py b/pyotb/apps.py index cd0c696..e5523a2 100644 --- a/pyotb/apps.py +++ b/pyotb/apps.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Search for OTB (set env if necessary), subclass core.App for each available application.""" from __future__ import annotations diff --git a/pyotb/core.py b/pyotb/core.py index d05c7d5..6f8c24c 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """This module is the core of pyotb.""" from __future__ import annotations diff --git a/pyotb/functions.py b/pyotb/functions.py index 96431cd..31e366f 100644 --- a/pyotb/functions.py +++ b/pyotb/functions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """This module provides a set of functions for pyotb.""" from __future__ import annotations diff --git a/pyotb/helpers.py b/pyotb/helpers.py index 3fc93b3..b6c09b3 100644 --- a/pyotb/helpers.py +++ b/pyotb/helpers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """This module ensure we properly initialize pyotb, or raise SystemExit in case of broken install.""" import logging import os -- GitLab From 50a51fd20e9d05b14816c6d520520a2ea58f4983 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 16:58:46 +0100 Subject: [PATCH 02/30] BUG: remove corner case for OTB < 7.4 --- pyotb/functions.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyotb/functions.py b/pyotb/functions.py index 31e366f..5eff2d4 100644 --- a/pyotb/functions.py +++ b/pyotb/functions.py @@ -457,12 +457,10 @@ def define_processing_area( # It should replace any 'outside' pixel by some NoData -> add `fillvalue` argument in the function # Applying this bounding box to all inputs + bounds = (ulx, uly, lrx, lry) logger.info( "Cropping all images to extent Upper Left (%s, %s), Lower Right (%s, %s)", - ulx, - uly, - lrx, - lry, + *bounds, ) new_inputs = [] for inp in inputs: @@ -472,11 +470,11 @@ def define_processing_area( "mode": "extent", "mode.extent.unit": "phy", "mode.extent.ulx": ulx, - "mode.extent.uly": lry, # bug in OTB <= 7.3 : + "mode.extent.uly": uly, "mode.extent.lrx": lrx, - "mode.extent.lry": uly, # ULY/LRY are inverted + "mode.extent.lry": lry, } - new_input = App("ExtractROI", params) + new_input = App("ExtractROI", params, quiet=True) # TODO: OTB 7.4 fixes this bug, how to handle different versions of OTB? new_inputs.append(new_input) # Potentially update the reference inputs for later resampling -- GitLab From e670786a81614e0596ba9b722a31f68bd3606b70 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 16:59:25 +0100 Subject: [PATCH 03/30] ENH: update functions.py docstrings and light refac --- pyotb/functions.py | 151 ++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 85 deletions(-) diff --git a/pyotb/functions.py b/pyotb/functions.py index 5eff2d4..ad2f24c 100644 --- a/pyotb/functions.py +++ b/pyotb/functions.py @@ -8,25 +8,28 @@ import sys import textwrap import uuid from collections import Counter +from pathlib import Path from .core import App, Input, LogicalOperation, Operation, get_nbchannels from .helpers import logger -def where( - cond: App | str, x: App | str | int | float, y: App | str | int | float -) -> Operation: +def where(cond: App | str, x: App | str | float, y: App | str | float) -> Operation: """Functionally similar to numpy.where. Where cond is True (!=0), returns x. Else returns y. + If cond is monoband whereas x or y are multiband, cond channels are expanded to match x & y ones. + Args: - cond: condition, must be a raster (filepath, App, Operation...). If cond is monoband whereas x or y are - multiband, cond channels are expanded to match x & y ones. + cond: condition, must be a raster (filepath, App, Operation...). x: value if cond is True. Can be: float, int, App, filepath, Operation... y: value if cond is False. Can be: float, int, App, filepath, Operation... Returns: an output where pixels are x if cond is True, else y + Raises: + ValueError: if x and y have different number of bands + """ # Checking the number of bands of rasters. Several cases : # - if cond is monoband, x and y can be multibands. Then cond will adapt to match x and y nb of bands @@ -61,19 +64,13 @@ def where( " of channels of condition to match the number of channels of X/Y", x_or_y_nb_channels, ) - # Get the number of bands of the result - if x_or_y_nb_channels: # if X or Y is a raster - out_nb_channels = x_or_y_nb_channels - else: # if only cond is a raster - out_nb_channels = cond_nb_channels + out_nb_channels = x_or_y_nb_channels or cond_nb_channels return Operation("?", cond, x, y, nb_bands=out_nb_channels) -def clip( - image: App | str, v_min: App | str | int | float, v_max: App | str | int | float -): +def clip(image: App | str, v_min: App | str | float, v_max: App | str | float): """Clip values of image in a range of values. Args: @@ -85,19 +82,18 @@ def clip( raster whose values are clipped in the range """ - if isinstance(image, str): + if isinstance(image, (str, Path)): image = Input(image) - res = where(image <= v_min, v_min, where(image >= v_max, v_max, image)) - return res + return where(image <= v_min, v_min, where(image >= v_max, v_max, image)) def all(*inputs): # pylint: disable=redefined-builtin """Check if value is different than 0 everywhere along the band axis. - For only one image, this function checks that all bands of the image are True (i.e. !=0) and outputs - a singleband boolean raster + For only one image, this function checks that all bands of the image are True (i.e. !=0) + and outputs a singleband boolean raster For several images, this function checks that all images are True (i.e. !=0) and outputs - a boolean raster, with as many bands as the inputs + a boolean raster, with as many bands as the inputs Args: inputs: inputs can be 1) a single image or 2) several images, either passed as separate arguments @@ -132,18 +128,18 @@ def all(*inputs): # pylint: disable=redefined-builtin res = res & inp[:, :, band] else: res = res & (inp[:, :, band] != 0) + return res + # Checking that all images are True + if isinstance(inputs[0], LogicalOperation): + res = inputs[0] else: - if isinstance(inputs[0], LogicalOperation): - res = inputs[0] + res = inputs[0] != 0 + for inp in inputs[1:]: + if isinstance(inp, LogicalOperation): + res = res & inp else: - res = inputs[0] != 0 - for inp in inputs[1:]: - if isinstance(inp, LogicalOperation): - res = res & inp - else: - res = res & (inp != 0) - + res = res & (inp != 0) return res @@ -158,6 +154,7 @@ def any(*inputs): # pylint: disable=redefined-builtin Args: inputs: inputs can be 1) a single image or 2) several images, either passed as separate arguments or inside a list + Returns: OR intersection @@ -188,19 +185,18 @@ def any(*inputs): # pylint: disable=redefined-builtin res = res | inp[:, :, band] else: res = res | (inp[:, :, band] != 0) + return res # Checking that at least one image is True + if isinstance(inputs[0], LogicalOperation): + res = inputs[0] else: - if isinstance(inputs[0], LogicalOperation): - res = inputs[0] + res = inputs[0] != 0 + for inp in inputs[1:]: + if isinstance(inp, LogicalOperation): + res = res | inp else: - res = inputs[0] != 0 - for inp in inputs[1:]: - if isinstance(inp, LogicalOperation): - res = res | inp - else: - res = res | (inp != 0) - + res = res | (inp != 0) return res @@ -224,17 +220,19 @@ def run_tf_function(func): Returns: wrapper: a function that returns a pyotb object + Raises: + SystemError: if OTBTF apps are missing + """ try: from .apps import ( # pylint: disable=import-outside-toplevel TensorflowModelServe, ) - except ImportError: - logger.error( + except ImportError as err: + raise SystemError( "Could not run Tensorflow function: failed to import TensorflowModelServe." "Check that you have OTBTF configured (https://github.com/remicres/otbtf#how-to-install)" - ) - raise + ) from err def get_tf_pycmd(output_dir, channels, scalar_inputs): """Create a string containing all python instructions necessary to create and save the Keras model. @@ -301,13 +299,13 @@ def run_tf_function(func): raster_inputs = [] for inp in inputs: try: - # this is for raster input + # This is for raster input channel = get_nbchannels(inp) channels.append(channel) scalar_inputs.append(None) raster_inputs.append(inp) except TypeError: - # this is for other inputs (float, int) + # This is for other inputs (float, int) channels.append(None) scalar_inputs.append(inp) @@ -316,6 +314,7 @@ def run_tf_function(func): out_savedmodel = os.path.join(tmp_dir, f"tmp_otbtf_model_{uuid.uuid4()}") pycmd = get_tf_pycmd(out_savedmodel, channels, scalar_inputs) cmd_args = [sys.executable, "-c", pycmd] + # TODO: remove subprocess execution since this issues has been fixed with OTBTF 4.0 try: subprocess.run( cmd_args, @@ -407,41 +406,25 @@ def define_processing_area( # TODO: there seems to have a bug, ImageMetaData is not updated when running an app, # cf https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/issues/2234. Should we use ImageOrigin instead? if not all( - metadata["UpperLeftCorner"] == any_metadata["UpperLeftCorner"] - and metadata["LowerRightCorner"] == any_metadata["LowerRightCorner"] - for metadata in metadatas.values() + md["UpperLeftCorner"] == any_metadata["UpperLeftCorner"] + and md["LowerRightCorner"] == any_metadata["LowerRightCorner"] + for md in metadatas.values() ): # Retrieving the bounding box that will be common for all inputs if window_rule == "intersection": # The coordinates depend on the orientation of the axis of projection if any_metadata["GeoTransform"][1] >= 0: - ulx = max( - metadata["UpperLeftCorner"][0] for metadata in metadatas.values() - ) - lrx = min( - metadata["LowerRightCorner"][0] for metadata in metadatas.values() - ) + ulx = max(md["UpperLeftCorner"][0] for md in metadatas.values()) + lrx = min(md["LowerRightCorner"][0] for md in metadatas.values()) else: - ulx = min( - metadata["UpperLeftCorner"][0] for metadata in metadatas.values() - ) - lrx = max( - metadata["LowerRightCorner"][0] for metadata in metadatas.values() - ) + ulx = min(md["UpperLeftCorner"][0] for md in metadatas.values()) + lrx = max(md["LowerRightCorner"][0] for md in metadatas.values()) if any_metadata["GeoTransform"][-1] >= 0: - lry = min( - metadata["LowerRightCorner"][1] for metadata in metadatas.values() - ) - uly = max( - metadata["UpperLeftCorner"][1] for metadata in metadatas.values() - ) + lry = min(md["LowerRightCorner"][1] for md in metadatas.values()) + uly = max(md["UpperLeftCorner"][1] for md in metadatas.values()) else: - lry = max( - metadata["LowerRightCorner"][1] for metadata in metadatas.values() - ) - uly = min( - metadata["UpperLeftCorner"][1] for metadata in metadatas.values() - ) + lry = max(md["LowerRightCorner"][1] for md in metadatas.values()) + uly = min(md["UpperLeftCorner"][1] for md in metadatas.values()) elif window_rule == "same_as_input": ulx = metadatas[reference_window_input]["UpperLeftCorner"][0] @@ -449,12 +432,12 @@ def define_processing_area( lry = metadatas[reference_window_input]["LowerRightCorner"][1] uly = metadatas[reference_window_input]["UpperLeftCorner"][1] elif window_rule == "specify": - pass - # TODO : it is when the user explicitly specifies the bounding box -> add some arguments in the function + # TODO : when the user explicitly specifies the bounding box -> add some arguments in the function + ... elif window_rule == "union": - pass - # TODO : it is when the user wants the final bounding box to be the union of all bounding box + # TODO : when the user wants the final bounding box to be the union of all bounding box # It should replace any 'outside' pixel by some NoData -> add `fillvalue` argument in the function + ... # Applying this bounding box to all inputs bounds = (ulx, uly, lrx, lry) @@ -478,16 +461,14 @@ def define_processing_area( # TODO: OTB 7.4 fixes this bug, how to handle different versions of OTB? new_inputs.append(new_input) # Potentially update the reference inputs for later resampling - if str(inp) == str( - reference_pixel_size_input - ): # we use comparison of string because calling '==' + if str(inp) == str(reference_pixel_size_input): + # We use comparison of string because calling '==' # on pyotb objects implicitly calls BandMathX application, which is not desirable reference_pixel_size_input = new_input - except RuntimeError as e: - logger.error( - "Cannot define the processing area for input %s: %s", inp, e - ) - raise + except RuntimeError as err: + raise ValueError( + f"Cannot define the processing area for input {inp}" + ) from err inputs = new_inputs # Update metadatas metadatas = {input: input.app.GetImageMetaData("out") for input in inputs} @@ -496,9 +477,9 @@ def define_processing_area( any_metadata = next(iter(metadatas.values())) # Handling different pixel sizes if not all( - metadata["GeoTransform"][1] == any_metadata["GeoTransform"][1] - and metadata["GeoTransform"][5] == any_metadata["GeoTransform"][5] - for metadata in metadatas.values() + md["GeoTransform"][1] == any_metadata["GeoTransform"][1] + and md["GeoTransform"][5] == any_metadata["GeoTransform"][5] + for md in metadatas.values() ): # Retrieving the pixel size that will be common for all inputs if pixel_size_rule == "minimal": -- GitLab From e277304176fc09824667ceec2620651bbde51f0d Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 16:59:54 +0100 Subject: [PATCH 04/30] ENH: rename pyOTB logger to just pyotb --- pyotb/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyotb/helpers.py b/pyotb/helpers.py index b6c09b3..e0feb63 100644 --- a/pyotb/helpers.py +++ b/pyotb/helpers.py @@ -15,13 +15,13 @@ DOCS_URL = "https://www.orfeo-toolbox.org/CookBook/Installation.html" # Logging # User can also get logger with `logging.getLogger("pyOTB")` # then use pyotb.set_logger_level() to adjust logger verbosity -logger = logging.getLogger("pyOTB") +logger = logging.getLogger("pyotb") logger_handler = logging.StreamHandler(sys.stdout) formatter = logging.Formatter( - fmt="%(asctime)s (%(levelname)-4s) [pyOTB] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" + fmt="%(asctime)s (%(levelname)-4s) [pyotb] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) logger_handler.setFormatter(formatter) -# Search for PYOTB_LOGGER_LEVEL, else use OTB_LOGGER_LEVEL as pyOTB level, or fallback to INFO +# Search for PYOTB_LOGGER_LEVEL, else use OTB_LOGGER_LEVEL as pyotb level, or fallback to INFO LOG_LEVEL = ( os.environ.get("PYOTB_LOGGER_LEVEL") or os.environ.get("OTB_LOGGER_LEVEL") or "INFO" ) -- GitLab From a21aa4dfbcb7212184c3226652c36d5dc1e2abb7 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 17:01:34 +0100 Subject: [PATCH 05/30] ENH: refactor duplicated function in Operation --- pyotb/core.py | 69 ++++++++++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 6f8c24c..d56e9e9 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1266,6 +1266,32 @@ class Operation(App): appname, il=self.unique_inputs, exp=self.exp, quiet=True, name=name ) + def get_nb_bands(self, inputs: list[OTBObject | str | int | float]) -> int: + """Guess the number of bands of the output image, from the inputs. + + Args: + inputs: the Operation operands + + Raises: + ValueError: if all inputs don't have the same number of bands + + """ + if any( + isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced") + for inp in inputs + ): + return 1 + # Check that all inputs have the same band count + nb_bands_list = [ + get_nbchannels(inp) + for inp in inputs + if not isinstance(inp, (float, int)) + ] + all_same = all(x == nb_bands_list[0] for x in nb_bands_list) + if len(nb_bands_list) > 1 and not all_same: + raise ValueError("All images do not have the same number of bands") + return nb_bands_list[0] + def build_fake_expressions( self, operator: str, @@ -1285,29 +1311,12 @@ class Operation(App): self.inputs.clear() self.nb_channels.clear() logger.debug("%s, %s", operator, inputs) - # This is when we use the ternary operator with `pyotb.where` function. The output nb of bands is already known + # When we use the ternary operator with `pyotb.where` function, the output nb of bands is already known if operator == "?" and nb_bands: pass # For any other operations, the output number of bands is the same as inputs else: - if any( - isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced") - for inp in inputs - ): - nb_bands = 1 - else: - nb_bands_list = [ - get_nbchannels(inp) - for inp in inputs - if not isinstance(inp, (float, int)) - ] - # check that all inputs have the same nb of bands - if len(nb_bands_list) > 1 and not all( - x == nb_bands_list[0] for x in nb_bands_list - ): - raise ValueError("All images do not have the same number of bands") - nb_bands = nb_bands_list[0] - + nb_bands = self.get_nb_bands(inputs) # Create a list of fake expressions, each item of the list corresponding to one band self.fake_exp_bands.clear() for i, band in enumerate(range(1, nb_bands + 1)): @@ -1463,29 +1472,11 @@ class LogicalOperation(Operation): Args: operator: str (one of >, <, >=, <=, ==, !=, &, |) inputs: Can be OTBObject, filepath, int or float - nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where + nb_bands: optionnaly specify the output nb of bands - used only internally by pyotb.where """ - # For any other operations, the output number of bands is the same as inputs - if any( - isinstance(inp, Slicer) and hasattr(inp, "one_band_sliced") - for inp in inputs - ): - nb_bands = 1 - else: - nb_bands_list = [ - get_nbchannels(inp) - for inp in inputs - if not isinstance(inp, (float, int)) - ] - # check that all inputs have the same nb of bands - if len(nb_bands_list) > 1 and not all( - x == nb_bands_list[0] for x in nb_bands_list - ): - raise ValueError("All images do not have the same number of bands") - nb_bands = nb_bands_list[0] # Create a list of fake exp, each item of the list corresponding to one band - for i, band in enumerate(range(1, nb_bands + 1)): + for i, band in enumerate(range(1, self.get_nb_bands(inputs) + 1)): expressions = [] for inp in inputs: fake_exp, corresp_inputs, nb_channels = super().make_fake_exp( -- GitLab From 04d3c20d8441c637d6cbe5801a139b5f52670926 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 17:02:55 +0100 Subject: [PATCH 06/30] ENH: add more exceptions to prevent errors during __set_param --- pyotb/core.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index d56e9e9..2a385ca 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1043,9 +1043,18 @@ class App(OTBObject): else: # here `input` should be an image filepath # Append `input` to the list, do not overwrite any previously set element of the image list self.app.AddParameterStringList(key, inp) + else: + raise TypeError( + f"{self.name}: wrong input parameter type ({type(inp)})" + f" found in '{key}' list: {inp}" + ) # List of any other types (str, int...) - else: + elif self.is_key_list(key): self.app.SetParameterValue(key, obj) + else: + raise TypeError( + f"{self.name}: wrong input parameter type ({type(obj)}) for '{key}'" + ) def __sync_parameters(self): """Save app parameters in _auto_parameters or data dict. -- GitLab From 9bd6c3e5f19d55f10a78ebb2d34146bcfbbad4f3 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 17:11:05 +0100 Subject: [PATCH 07/30] DOC: update or enh comments and docstrings, add "Raises", fix type hints --- pyotb/apps.py | 18 +-- pyotb/core.py | 301 ++++++++++++++++++++++-------------------- pyotb/depreciation.py | 10 +- pyotb/functions.py | 17 +-- pyotb/helpers.py | 22 ++- pyotb/install.py | 9 +- 6 files changed, 202 insertions(+), 175 deletions(-) diff --git a/pyotb/apps.py b/pyotb/apps.py index e5523a2..45e49c4 100644 --- a/pyotb/apps.py +++ b/pyotb/apps.py @@ -12,12 +12,12 @@ from .helpers import logger def get_available_applications() -> tuple[str]: """Find available OTB applications. - Args: - as_subprocess: indicate if function should list available applications using subprocess call - Returns: tuple of available applications + Raises: + SystemExit: if no application is found + """ app_list = otb.Registry.GetAvailableApplications() if app_list: @@ -29,7 +29,7 @@ def get_available_applications() -> tuple[str]: class OTBTFApp(App): - """Helper for OTBTF.""" + """Helper for OTBTF to ensure the nb_sources variable is set.""" @staticmethod def set_nb_sources(*args, n_sources: int = None): @@ -59,10 +59,8 @@ class OTBTFApp(App): Args: name: name of the OTBTF app - *args: arguments (dict). NB: we don't need kwargs because it cannot contain source#.il n_sources: number of sources. Default is None (resolves the number of sources based on the content of the dict passed in args, where some 'source' str is found) - **kwargs: kwargs """ self.set_nb_sources(*args, n_sources=n_sources) @@ -71,16 +69,12 @@ class OTBTFApp(App): AVAILABLE_APPLICATIONS = get_available_applications() -# This is to enable aliases of Apps, i.e. using apps like `pyotb.AppName(...)` instead of `pyotb.App("AppName", ...)` -_CODE_TEMPLATE = ( - """ +# This is to enable aliases of Apps, i.e. `pyotb.AppName(...)` instead of `pyotb.App("AppName", ...)` +_CODE_TEMPLATE = """ class {name}(App): - """ - """ def __init__(self, *args, **kwargs): super().__init__('{name}', *args, **kwargs) """ -) for _app in AVAILABLE_APPLICATIONS: # Customize the behavior for some OTBTF applications. `OTB_TF_NSOURCES` is now handled by pyotb diff --git a/pyotb/core.py b/pyotb/core.py index 2a385ca..0f27547 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -21,12 +21,12 @@ class OTBObject(ABC): @property @abstractmethod def name(self) -> str: - """By default, should return the application name, but a custom name may be passed during init.""" + """Application name by default, but a custom name may be passed during init.""" @property @abstractmethod def app(self) -> otb.Application: - """Reference to the main (or last in pipeline) otb.Application instance linked to this object.""" + """Reference to the otb.Application instance linked to this object.""" @property @abstractmethod @@ -41,11 +41,11 @@ class OTBObject(ABC): @property @abstractmethod def exports_dic(self) -> dict[str, dict]: - """Return an internal dict object containing np.array exports, to avoid duplicated ExportImage() calls.""" + """Ref to an internal dict of np.array exports, to avoid duplicated ExportImage().""" @property def metadata(self) -> dict[str, (str, float, list[float])]: - """Return metadata. + """Return image metadata as dictionary. The returned dict results from the concatenation of the first output image metadata dictionary and the metadata dictionary. @@ -61,10 +61,7 @@ class OTBObject(ABC): if getattr(otb_imd, "has")(key) } - # Metadata dictionary - # Replace items like {"metadata_1": "TIFFTAG_SOFTWARE=CSinG - 13 - # SEPTEMBRE 2012"} with {"TIFFTAG_SOFTWARE": "CSinG - 13 SEPTEMBRE - # 2012"} + # Other metadata dictionary: key-value pairs parsing is required mdd = dict(self.app.GetMetadataDictionary(self.output_image_key)) new_mdd = {} for key, val in mdd.items(): @@ -104,7 +101,9 @@ class OTBObject(ABC): @property def transform(self) -> tuple[int]: - """Get image affine transform, rasterio style (see https://www.perrygeo.com/python-affine-transforms.html). + """Get image affine transform, rasterio style. + + See https://www.perrygeo.com/python-affine-transforms.html Returns: transform: (X spacing, X offset, X origin, Y offset, Y spacing, Y origin) @@ -116,7 +115,7 @@ class OTBObject(ABC): return spacing_x, 0.0, origin_x, 0.0, spacing_y, origin_y def summarize(self, *args, **kwargs): - """Recursively summarize parameters and parents. + """Recursively summarize an app parameters and its parents. Args: *args: args for `pyotb.summarize()` @@ -138,7 +137,7 @@ class OTBObject(ABC): def get_values_at_coords( self, row: int, col: int, bands: int = None - ) -> list[int | float] | int | float: + ) -> list[float] | float: """Get pixel value(s) at a given YX coordinates. Args: @@ -149,6 +148,9 @@ class OTBObject(ABC): Returns: single numerical value or a list of values for each band + Raises: + TypeError: if bands is not a slice or list + """ channels = [] app = App("PixelValue", self, coordx=col, coordy=row, frozen=True, quiet=True) @@ -170,8 +172,19 @@ class OTBObject(ABC): data = literal_eval(app.app.GetParameterString("value")) return data[0] if len(channels) == 1 else data - def channels_list_from_slice(self, bands: int) -> list[int]: - """Get list of channels to read values at, from a slice.""" + def channels_list_from_slice(self, bands: slice) -> list[int]: + """Get list of channels to read values at, from a slice. + + Args: + bands: slice obtained when using app[:] + + Returns: + list of channels to select + + Raises: + ValueError: if the slice is malformed + + """ nb_channels = self.shape[2] start, stop, step = bands.start, bands.stop, bands.step start = nb_channels + start if isinstance(start, int) and start < 0 else start @@ -226,7 +239,7 @@ class OTBObject(ABC): required to True if preserve_dtype is False and the source app reference is lost Returns: - a numpy array + a numpy array that may already have been cached in self.exports_dic """ data = self.export(key, preserve_dtype) @@ -257,6 +270,7 @@ class OTBObject(ABC): Returns: pixel index: (row, col) + """ spacing_x, _, origin_x, _, spacing_y, origin_y = self.transform row, col = (origin_y - y) / spacing_y, (x - origin_x) / spacing_x @@ -272,8 +286,8 @@ class OTBObject(ABC): x: first element y: second element - Return: - operator + Returns: + an Operation object instance """ if isinstance(y, (np.ndarray, np.generic)): @@ -420,7 +434,6 @@ class OTBObject(ABC): Args: item: attribute name - """ note = ( "Since pyotb 2.0.0, OTBObject instances have stopped to forward " @@ -430,8 +443,6 @@ class OTBObject(ABC): hint = None if item in dir(self.app): - # Because otbApplication instances methods names start with an - # upper case hint = f"Maybe try `pyotb_app.app.{item}` instead of `pyotb_app.{item}`? " if item.startswith("GetParameter"): hint += ( @@ -439,10 +450,8 @@ class OTBObject(ABC): "shorten with `pyotb_app['paramname']` to access parameters " "values." ) - elif item in self.parameters_keys: - # Because in pyotb 1.5.4, applications outputs were added as - # attributes of the instance + # Because in pyotb 1.5.4, app outputs were added as instance attributes hint = ( "Note: `pyotb_app.paramname` is no longer supported. Starting " "from pyotb 2.0.0, `pyotb_app['paramname']` can be used to " @@ -469,6 +478,9 @@ class OTBObject(ABC): Returns: list of pixel values if vector image, or pixel value, or Slicer + Raises: + ValueError: if key is not a valid pixel index or slice + """ # Accessing pixel value(s) using Y/X coordinates if isinstance(key, tuple) and len(key) >= 2: @@ -488,12 +500,15 @@ class OTBObject(ABC): f'"{key}" cannot be interpreted as valid slicing. Slicing should be 2D or 3D.' ) if isinstance(key, tuple) and len(key) == 2: - # Adding a 3rd dimension - key = key + (slice(None, None, None),) + key = key + (slice(None, None, None),) # adding 3rd dimension return Slicer(self, *key) def __repr__(self) -> str: - """Return a string representation with object id, this is a key used to store image ref in Operation dicts.""" + """Return a string representation with object id. + + This is used as key to store image ref in Operation dicts. + + """ return f"<pyotb.{self.__class__.__name__} object, id {id(self)}>" @@ -510,24 +525,20 @@ class App(OTBObject): otb.ParameterType_InputFilename, otb.ParameterType_InputFilenameList, ] - OUTPUT_IMAGE_TYPES = [otb.ParameterType_OutputImage] OUTPUT_PARAM_TYPES = OUTPUT_IMAGE_TYPES + [ otb.ParameterType_OutputVectorData, otb.ParameterType_OutputFilename, ] - - INPUT_LIST_TYPES = [ - otb.ParameterType_InputImageList, - otb.ParameterType_StringList, - otb.ParameterType_InputFilenameList, - otb.ParameterType_ListView, - otb.ParameterType_InputVectorDataList, - ] INPUT_IMAGES_LIST_TYPES = [ otb.ParameterType_InputImageList, otb.ParameterType_InputFilenameList, ] + INPUT_LIST_TYPES = [ + otb.ParameterType_StringList, + otb.ParameterType_ListView, + otb.ParameterType_InputVectorDataList, + ] + INPUT_IMAGES_LIST_TYPES def __init__( self, @@ -550,7 +561,6 @@ class App(OTBObject): frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___ quiet: whether to print logs of the OTB app name: custom name that will show up in logs, appname will be used if not provided - **kwargs: used for passing application parameters. e.g. il=['input1.tif', App_object2, App_object3.out], out='output.tif' @@ -606,7 +616,7 @@ class App(OTBObject): @property def app(self) -> otb.Application: - """Property to return an internal _app instance.""" + """Reference to this app otb.Application instance.""" return self._app @property @@ -616,41 +626,25 @@ class App(OTBObject): @property def exports_dic(self) -> dict[str, dict]: - """Returns internal _exports_dic object that contains numpy array exports.""" + """Reference to an internal dict object that contains numpy array exports.""" return self._exports_dic def __is_one_of_types(self, key: str, param_types: list[int]) -> bool: - """Helper to factor is_input and is_output.""" + """Helper to check the type of a parameter.""" if key not in self._all_param_types: raise KeyError(f"key {key} not found in the application parameters types") return self._all_param_types[key] in param_types def __is_multi_output(self): - """Check if app has multiple outputs to ensure execution during write().""" + """Check if app has multiple outputs to ensure re-execution during write().""" return len(self.outputs) > 1 def is_input(self, key: str) -> bool: - """Returns True if the key is an input. - - Args: - key: parameter key - - Returns: - True if the parameter is an input, else False - - """ + """Returns True if the parameter key is an input.""" return self.__is_one_of_types(key=key, param_types=self.INPUT_PARAM_TYPES) def is_output(self, key: str) -> bool: - """Returns True if the key is an output. - - Args: - key: parameter key - - Returns: - True if the parameter is an output, else False - - """ + """Returns True if the parameter key is an output.""" return self.__is_one_of_types(key=key, param_types=self.OUTPUT_PARAM_TYPES) def is_key_list(self, key: str) -> bool: @@ -670,7 +664,7 @@ class App(OTBObject): if value == param_type: return key raise TypeError( - f"{self.name}: could not find any parameter key matching the provided types" + f"{self.name}: could not find any key matching the provided types" ) @property @@ -705,14 +699,14 @@ class App(OTBObject): instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths Args: - *args: Can be : - dictionary containing key-arguments enumeration. Useful when a key is python-reserved - (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit") + *args: Can be : - dictionary containing key-arguments enumeration. Useful when a keyword is reserved (e.g. "in") - string or OTBObject, useful when the user implicitly wants to set the param "in" - list, useful when the user implicitly wants to set the param "il" **kwargs: keyword arguments e.g. il=['input1.tif', oApp_object2, App_object3.out], out='output.tif' Raises: - Exception: when the setting of a parameter failed + KeyError: when the parameter name wasn't recognized + RuntimeError: failed to set parameter valie """ parameters = kwargs @@ -723,7 +717,8 @@ class App(OTBObject): key = key.replace("_", ".") if key not in self.parameters_keys: raise KeyError( - f"{self.name}: parameter '{key}' was not recognized. Available keys are {self.parameters_keys}" + f"{self.name}: parameter '{key}' was not recognized." + f" Available keys are {self.parameters_keys}" ) # When the parameter expects a list, if needed, change the value to list if self.is_key_list(key) and not isinstance(obj, (list, tuple)): @@ -741,7 +736,8 @@ class App(OTBObject): self.__set_param(key, obj) except (RuntimeError, TypeError, ValueError, KeyError) as e: raise RuntimeError( - f"{self.name}: error before execution, while setting parameter '{key}' to '{obj}': {e})" + f"{self.name}: error before execution," + f" while setting '{key}' to '{obj}': {e})" ) from e # Save / update setting value and update the Output object initialized in __init__ without a filepath self._settings[key] = obj @@ -873,7 +869,6 @@ class App(OTBObject): if isinstance(ext_fname, str): ext_fname = _str2dict(ext_fname) - logger.debug("%s: extended filename for all outputs:", self.name) for key, ext in ext_fname.items(): logger.debug("%s: %s", key, ext) @@ -881,14 +876,12 @@ class App(OTBObject): for key, filepath in kwargs.items(): if self._out_param_types[key] == otb.ParameterType_OutputImage: new_ext_fname = ext_fname.copy() - - # grab already set extended filename key/values + # Grab already set extended filename key/values if "?&" in filepath: filepath, already_set_ext = filepath.split("?&", 1) - # extensions in filepath prevail over `new_ext_fname` + # Extensions in filepath prevail over `new_ext_fname` new_ext_fname.update(_str2dict(already_set_ext)) - - # transform dict to str + # tyransform dict to str ext_fname_str = "&".join( [f"{key}={value}" for key, value in new_ext_fname.items()] ) @@ -911,7 +904,7 @@ class App(OTBObject): key: parse_pixel_type(dtype) for key, dtype in pixel_type.items() } elif preserve_dtype: - self.propagate_dtype() # all outputs will have the same type as the main input raster + self.propagate_dtype() # Set parameters and flush to disk for key, filepath in parameters.items(): @@ -942,12 +935,11 @@ class App(OTBObject): ) return bool(files) and not missing - # Private functions def __parse_args(self, args: list[str | OTBObject | dict | list]) -> dict[str, Any]: """Gather all input arguments in kwargs dict. Args: - args: the list of arguments passed to set_parameters() + args: the list of arguments passed to set_parameters (__init__ *args) Returns: a dictionary with the right keyword depending on the object @@ -966,12 +958,11 @@ class App(OTBObject): return kwargs def __check_input_param( - self, obj: list | OTBObject | str | Path + self, obj: list | tuple | OTBObject | str | Path ) -> list | OTBObject | str: """Check the type and value of an input parameter, add vsi prefixes if needed.""" - if isinstance(obj, list): + if isinstance(obj, (list, tuple)): return [self.__check_input_param(o) for o in obj] - # May be we could add some checks here if isinstance(obj, OTBObject): return obj if isinstance(obj, Path): @@ -981,7 +972,6 @@ class App(OTBObject): # Remote file. TODO: add support for S3 / GS / AZ if obj.startswith(("https://", "http://", "ftp://")): obj = "/vsicurl/" + obj - # Compressed file prefixes = { ".tar": "vsitar", ".tar.gz": "vsitar", @@ -1000,9 +990,9 @@ class App(OTBObject): return obj raise TypeError(f"{self.name}: wrong input parameter type ({type(obj)})") - def __check_output_param(self, obj: list | str | Path) -> list | str: + def __check_output_param(self, obj: list | tuple | str | Path) -> list | str: """Check the type and value of an output parameter.""" - if isinstance(obj, list): + if isinstance(obj, (list, tuple)): return [self.__check_output_param(o) for o in obj] if isinstance(obj, Path): obj = str(obj) @@ -1020,27 +1010,22 @@ class App(OTBObject): # Single-parameter cases if isinstance(obj, OTBObject): self.app.ConnectImage(key, obj.app, obj.output_image_key) - elif isinstance( - obj, otb.Application - ): # this is for backward comp with plain OTB + elif isinstance(obj, otb.Application): self.app.ConnectImage(key, obj, get_out_images_param_keys(obj)[0]) - elif ( - key == "ram" - ): # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf gitlab OTB issue 2200 + elif key == "ram": + # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf OTB issue 2200 self.app.SetParameterInt("ram", int(obj)) - elif not isinstance(obj, list): # any other parameters (str, int...) + # Any other parameters (str, int...) + elif not isinstance(obj, (list, tuple)): self.app.SetParameterValue(key, obj) # Images list elif self.is_key_images_list(key): - # To enable possible in-memory connections, we go through the list and set the parameters one by one for inp in obj: if isinstance(inp, OTBObject): self.app.ConnectImage(key, inp.app, inp.output_image_key) - elif isinstance( - inp, otb.Application - ): # this is for backward comp with plain OTB + elif isinstance(inp, otb.Application): self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0]) - else: # here `input` should be an image filepath + elif isinstance(inp, (str, Path)): # Append `input` to the list, do not overwrite any previously set element of the image list self.app.AddParameterStringList(key, inp) else: @@ -1101,7 +1086,9 @@ class App(OTBObject): self.data[key] = value # Special functions - def __getitem__(self, key: str) -> Any | list[int | float] | int | float | Slicer: + def __getitem__( + self, key: str | tuple + ) -> Any | list[int | float] | int | float | Slicer: """This function is called when we use App()[...]. We allow to return attr if key is a parameter, or call OTBObject __getitem__ for pixel values or Slicer @@ -1141,7 +1128,10 @@ class Slicer(App): obj: input rows: slice along Y / Latitude axis cols: slice along X / Longitude axis - channels: channels, can be slicing, list or int + channels: bands to extract. can be slicing, list or int + + Raises: + TypeError: if channels param isn't slice, list or int """ super().__init__( @@ -1157,20 +1147,18 @@ class Slicer(App): # Channel slicing if channels != slice(None, None, None): - # Trigger source app execution if needed nb_channels = get_nbchannels(obj) self.app.Execute() # this is needed by ExtractROI for setting the `cl` parameter - # if needed, converting int to list if isinstance(channels, int): channels = [channels] - # if needed, converting slice to list elif isinstance(channels, slice): channels = self.channels_list_from_slice(channels) elif isinstance(channels, tuple): channels = list(channels) elif not isinstance(channels, list): - raise ValueError( - f"Invalid type for channels, should be int, slice or list of bands. : {channels}" + raise TypeError( + f"Invalid type for channels ({type(channels)})." + f" Should be int, slice or list of bands." ) # Change the potential negative index values to reverse index channels = [c if c >= 0 else nb_channels + c for c in channels] @@ -1178,28 +1166,24 @@ class Slicer(App): # Spatial slicing spatial_slicing = False - # TODO: handle the step value in the slice so that NN undersampling is possible ? e.g. raster[::2, ::2] if rows.start is not None: parameters.update({"mode.extent.uly": rows.start}) spatial_slicing = True if rows.stop is not None and rows.stop != -1: - parameters.update( - {"mode.extent.lry": rows.stop - 1} - ) # subtract 1 to respect python convention + # Subtract 1 to respect python convention + parameters.update({"mode.extent.lry": rows.stop - 1}) spatial_slicing = True if cols.start is not None: parameters.update({"mode.extent.ulx": cols.start}) spatial_slicing = True if cols.stop is not None and cols.stop != -1: - parameters.update( - {"mode.extent.lrx": cols.stop - 1} - ) # subtract 1 to respect python convention + # Subtract 1 to respect python convention + parameters.update({"mode.extent.lrx": cols.stop - 1}) spatial_slicing = True - # These are some attributes when the user simply wants to extract *one* band to be used in an Operation + # When the user simply wants to extract *one* band to be used in an Operation if not spatial_slicing and isinstance(channels, list) and len(channels) == 1: - self.one_band_sliced = ( - channels[0] + 1 - ) # OTB convention: channels start at 1 + # OTB convention: channels start at 1 + self.one_band_sliced = channels[0] + 1 self.input = obj # Execute app @@ -1230,33 +1214,32 @@ class Operation(App): """ def __init__(self, operator: str, *inputs, nb_bands: int = None, name: str = None): - """Given some inputs and an operator, this function enables to transform this into an OTB application. + """Given some inputs and an operator, this object enables to python operator to a BandMath operation. Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator. It can have 3 inputs for the ternary operator `cond ? x : y`. Args: operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ? - *inputs: inputs. Can be OTBObject, filepath, int or float - nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where - name: override the Operation name + *inputs: operands, can be OTBObject, filepath, int or float + nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where + name: override the default Operation name """ self.operator = operator - # We first create a 'fake' expression. E.g for the operation `input1 + input2` , we create a fake expression - # that is like "str(input1) + str(input2)" + # We first create a 'fake' expression. E.g for the operation `input1 + input2` + # we create a fake expression like "str(input1) + str(input2)" self.inputs = [] self.nb_channels = {} self.fake_exp_bands = [] self.build_fake_expressions(operator, inputs, nb_bands=nb_bands) # Transforming images to the adequate im#, e.g. `input1` to "im1" - # creating a dictionary that is like {str(input1): 'im1', 'image2.tif': 'im2', ...}. + # using a dictionary : {str(input1): 'im1', 'image2.tif': 'im2', ...}. # NB: the keys of the dictionary are strings-only, instead of 'complex' objects, to enable easy serialization self.im_dic = {} self.im_count = 1 - map_repr_to_input = ( - {} - ) # to be able to retrieve the real python object from its string representation + # To be able to retrieve the real python object from its string representation + map_repr_to_input = {} for inp in self.inputs: if not isinstance(inp, (int, float)): if str(inp) not in self.im_dic: @@ -1316,6 +1299,9 @@ class Operation(App): inputs: inputs. Can be OTBObject, filepath, int or float nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where + Raises: + ValueError: if all inputs don't have the same number of bands + """ self.inputs.clear() self.nb_channels.clear() @@ -1336,30 +1322,30 @@ class Operation(App): if len(inputs) == 3 and k == 0: # When cond is monoband whereas the result is multiband, we expand the cond to multiband cond_band = 1 if nb_bands != inp.shape[2] else band - fake_exp, corresponding_inputs, nb_channels = self.make_fake_exp( + fake_exp, corresp_inputs, nb_channels = self.make_fake_exp( inp, cond_band, keep_logical=True ) else: # Any other input - fake_exp, corresponding_inputs, nb_channels = self.make_fake_exp( + fake_exp, corresp_inputs, nb_channels = self.make_fake_exp( inp, band, keep_logical=False ) expressions.append(fake_exp) # Reference the inputs and nb of channels (only on first pass in the loop to avoid duplicates) - if i == 0 and corresponding_inputs and nb_channels: - self.inputs.extend(corresponding_inputs) + if i == 0 and corresp_inputs and nb_channels: + self.inputs.extend(corresp_inputs) self.nb_channels.update(nb_channels) # Generating the fake expression of the whole operation - if len(inputs) == 1: # this is only for 'abs' + if len(inputs) == 1: + # This is only for 'abs()' fake_exp = f"({operator}({expressions[0]}))" elif len(inputs) == 2: # We create here the "fake" expression. For example, for a BandMathX expression such as '2 * im1 + im2', # the false expression stores the expression 2 * str(input1) + str(input2) fake_exp = f"({expressions[0]} {operator} {expressions[1]})" - elif ( - len(inputs) == 3 and operator == "?" - ): # this is only for ternary expression + elif len(inputs) == 3 and operator == "?": + # This is only for ternary expression fake_exp = f"({expressions[0]} ? {expressions[1]} : {expressions[2]})" self.fake_exp_bands.append(fake_exp) @@ -1445,10 +1431,12 @@ class Operation(App): class LogicalOperation(Operation): - """A specialization of Operation class for boolean logical operations i.e. >, <, >=, <=, ==, !=, `&` and `|`. + """A specialization of Operation class for boolean logical operations. - The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"), but also the - logical expression (e.g. "im1b1 > 0") + Supported operators are >, <, >=, <=, ==, !=, `&` and `|`. + + The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"), + but also the logical expression (e.g. "im1b1 > 0") """ @@ -1458,7 +1446,7 @@ class LogicalOperation(Operation): Args: operator: string operator (one of >, <, >=, <=, ==, !=, &, |) *inputs: inputs - nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where + nb_bands: optionally specify the output nb of bands - used only by pyotb.where """ self.logical_fake_exp_bands = [] @@ -1481,7 +1469,7 @@ class LogicalOperation(Operation): Args: operator: str (one of >, <, >=, <=, ==, !=, &, |) inputs: Can be OTBObject, filepath, int or float - nb_bands: optionnaly specify the output nb of bands - used only internally by pyotb.where + nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where """ # Create a list of fake exp, each item of the list corresponding to one band @@ -1530,7 +1518,7 @@ class Input(App): class Output(OTBObject): - """Object that behave like a pointer to a specific application output file.""" + """Object that behave like a pointer to a specific application in-memory output or file.""" _filepath: str | Path = None @@ -1547,7 +1535,7 @@ class Output(OTBObject): Args: pyotb_app: The pyotb App to store reference from param_key: Output parameter key of the target app - filepath: path of the output file (if not in memory) + filepath: path of the output file (if not memory) mkdir: create missing parent directories """ @@ -1574,7 +1562,7 @@ class Output(OTBObject): @property def exports_dic(self) -> dict[str, dict]: - """Returns internal _exports_dic object that contains numpy array exports.""" + """Reference to parent _exports_dic object that contains np array exports.""" return self.parent_pyotb_app.exports_dic @property @@ -1597,19 +1585,34 @@ class Output(OTBObject): self._filepath = path def exists(self) -> bool: - """Check file exist.""" + """Check if the output file exist on disk. + + Raises: + ValueError: if filepath is not set or is remote URL + + """ if not isinstance(self.filepath, Path): raise ValueError("Filepath is not set or points to a remote URL") return self.filepath.exists() def make_parent_dirs(self): - """Create missing parent directories.""" + """Create missing parent directories. + + Raises: + ValueError: if filepath is not set or is remote URL + + """ if not isinstance(self.filepath, Path): raise ValueError("Filepath is not set or points to a remote URL") self.filepath.parent.mkdir(parents=True, exist_ok=True) def write(self, filepath: None | str | Path = None, **kwargs) -> bool: - """Write output to disk, filepath is not required if it was provided to parent App during init.""" + """Write output to disk, filepath is not required if it was provided to parent App during init. + + Args: + filepath: path of the output file, can be None if a value was passed during app init + + """ if filepath is None: return self.parent_pyotb_app.write( {self.output_image_key: self.filepath}, **kwargs @@ -1630,6 +1633,9 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int: Returns: number of bands in image + Raises: + TypeError: if inp band count cannot be retrieved + """ if isinstance(inp, OTBObject): return inp.shape[-1] @@ -1657,6 +1663,9 @@ def get_pixel_type(inp: str | Path | OTBObject) -> str: pixel_type: OTB enum e.g. `otbApplication.ImagePixelType_uint8', which actually is an int. For an OTBObject with several outputs, only the pixel type of the first output is returned + Raises: + TypeError: if inp pixel type cannot be retrieved + """ if isinstance(inp, OTBObject): return inp.app.GetParameterOutputImagePixelType(inp.output_image_key) @@ -1679,10 +1688,14 @@ def parse_pixel_type(pixel_type: str | int) -> int: """Convert one str pixel type to OTB integer enum if necessary. Args: - pixel_type: pixel type. can be str, int or dict + pixel_type: pixel type. can be int or str (either OTB or numpy convention) Returns: - pixel_type integer value + pixel_type OTB enum integer value + + Raises: + KeyError: if pixel_type name is unknown + TypeError: if type(pixel_type) isn't int or str """ if isinstance(pixel_type, int): # normal OTB int enum @@ -1704,19 +1717,19 @@ def parse_pixel_type(pixel_type: str | int) -> int: if pixel_type in datatype_to_pixeltype: return getattr(otb, f"ImagePixelType_{datatype_to_pixeltype[pixel_type]}") raise KeyError( - f"Unknown data type `{pixel_type}`. Available ones: {datatype_to_pixeltype}" + f"Unknown dtype `{pixel_type}`. Available ones: {datatype_to_pixeltype}" ) raise TypeError( f"Bad pixel type specification ({pixel_type} of type {type(pixel_type)})" ) -def get_out_images_param_keys(app: OTBObject) -> list[str]: - """Return every output parameter keys of an OTB app.""" +def get_out_images_param_keys(otb_app: otb.Application) -> list[str]: + """Return every output parameter keys of a bare OTB app.""" return [ key - for key in app.GetParametersKeys() - if app.GetParameterType(key) == otb.ParameterType_OutputImage + for key in otb_app.GetParametersKeys() + if otb_app.GetParameterType(key) == otb.ParameterType_OutputImage ] diff --git a/pyotb/depreciation.py b/pyotb/depreciation.py index 687e6ca..6794373 100644 --- a/pyotb/depreciation.py +++ b/pyotb/depreciation.py @@ -1,7 +1,6 @@ """Helps with deprecated classes and methods. -Taken from https://stackoverflow.com/questions/49802412/how-to-implement- -deprecation-in-python-with-argument-alias +Taken from https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias """ from typing import Callable, Dict, Any import functools @@ -17,7 +16,7 @@ def depreciation_warning(message: str): """Shows a warning message. Args: - message: message + message: message to log """ warnings.warn( @@ -63,11 +62,14 @@ def rename_kwargs(func_name: str, kwargs: Dict[str, Any], aliases: Dict[str, str kwargs: keyword args aliases: aliases + Raises: + ValueError: if both old and new arguments are provided + """ for alias, new in aliases.items(): if alias in kwargs: if new in kwargs: - raise TypeError( + raise ValueError( f"{func_name} received both {alias} and {new} as arguments!" f" {alias} is deprecated, use {new} instead." ) diff --git a/pyotb/functions.py b/pyotb/functions.py index ad2f24c..8fde2cd 100644 --- a/pyotb/functions.py +++ b/pyotb/functions.py @@ -247,7 +247,7 @@ def run_tf_function(func): """ # Getting the string definition of the tf function (e.g. "def multiply(x1, x2):...") - # TODO: maybe not entirely foolproof, maybe we should use dill instead? but it would add a dependency + # Maybe not entirely foolproof, maybe we should use dill instead? but it would add a dependency func_def_str = inspect.getsource(func) func_name = func.__name__ @@ -342,7 +342,7 @@ def run_tf_function(func): for i, inp in enumerate(raster_inputs): model_serve.set_parameters({f"source{i + 1}.il": [inp]}) model_serve.execute() - # TODO: handle the deletion of the temporary model ? + # Possible ENH: handle the deletion of the temporary model ? return model_serve @@ -403,7 +403,7 @@ def define_processing_area( ) # Handling different spatial footprints - # TODO: there seems to have a bug, ImageMetaData is not updated when running an app, + # TODO: find possible bug - ImageMetaData is not updated when running an app # cf https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/issues/2234. Should we use ImageOrigin instead? if not all( md["UpperLeftCorner"] == any_metadata["UpperLeftCorner"] @@ -432,10 +432,10 @@ def define_processing_area( lry = metadatas[reference_window_input]["LowerRightCorner"][1] uly = metadatas[reference_window_input]["UpperLeftCorner"][1] elif window_rule == "specify": - # TODO : when the user explicitly specifies the bounding box -> add some arguments in the function + # When the user explicitly specifies the bounding box -> add some arguments in the function ... elif window_rule == "union": - # TODO : when the user wants the final bounding box to be the union of all bounding box + # When the user wants the final bounding box to be the union of all bounding box # It should replace any 'outside' pixel by some NoData -> add `fillvalue` argument in the function ... @@ -458,7 +458,7 @@ def define_processing_area( "mode.extent.lry": lry, } new_input = App("ExtractROI", params, quiet=True) - # TODO: OTB 7.4 fixes this bug, how to handle different versions of OTB? + # OTB 7.4 fixes this bug, how to handle different versions of OTB? new_inputs.append(new_input) # Potentially update the reference inputs for later resampling if str(inp) == str(reference_pixel_size_input): @@ -495,8 +495,9 @@ def define_processing_area( elif pixel_size_rule == "same_as_input": reference_input = reference_pixel_size_input elif pixel_size_rule == "specify": - pass - # TODO : when the user explicitly specify the pixel size -> add argument inside the function + # When the user explicitly specify the pixel size -> add argument inside the function + ... + pixel_size = metadatas[reference_input]["GeoTransform"][1] # Perform resampling on inputs that do not comply with the target pixel size diff --git a/pyotb/helpers.py b/pyotb/helpers.py index e0feb63..0e6ea2a 100644 --- a/pyotb/helpers.py +++ b/pyotb/helpers.py @@ -60,6 +60,10 @@ def find_otb(prefix: str = OTB_ROOT, scan: bool = True): Returns: otbApplication module + Raises: + SystemError: is OTB is not found (when using interactive mode) + SystemExit: if OTB is not found, since pyotb won't be usable + """ otb = None # Try OTB_ROOT env variable first (allow override default OTB version) @@ -125,6 +129,9 @@ def set_environment(prefix: str): Args: prefix: path to OTB root directory + Raises: + SystemError: if OTB or GDAL is not found + """ logger.info("Preparing environment for OTB in %s", prefix) # OTB root directory @@ -138,16 +145,18 @@ def set_environment(prefix: str): lib_dir = __find_lib(prefix) if not lib_dir: raise SystemError("Can't find OTB external libraries") - # This does not seems to work + # LD library path : this does not seems to work if sys.platform == "linux" and built_from_source: new_ld_path = f"{lib_dir}:{os.environ.get('LD_LIBRARY_PATH') or ''}" os.environ["LD_LIBRARY_PATH"] = new_ld_path + # Add python bindings directory first in PYTHONPATH otb_api = __find_python_api(lib_dir) if not otb_api: raise SystemError("Can't find OTB Python API") if otb_api not in sys.path: sys.path.insert(0, otb_api) + # Add /bin first in PATH, in order to avoid conflicts with another GDAL install os.environ["PATH"] = f"{prefix / 'bin'}{os.pathsep}{os.environ['PATH']}" # Ensure APPLICATION_PATH is set @@ -159,6 +168,7 @@ def set_environment(prefix: str): os.environ["LC_NUMERIC"] = "C" os.environ["GDAL_DRIVER_PATH"] = "disable" + # Find GDAL libs if (prefix / "share/gdal").exists(): # Local GDAL (OTB Superbuild, .run, .exe) gdal_data = str(prefix / "share/gdal") @@ -186,7 +196,7 @@ def __find_lib(prefix: str = None, otb_module=None): otb_module: try with otbApplication library path if found, else None Returns: - lib path + lib path, or None if not found """ if prefix is not None: @@ -216,7 +226,7 @@ def __find_python_api(lib_dir: Path): prefix: prefix Returns: - python API path if found, else None + OTB python API path, or None if not found """ otb_api = lib_dir / "python" @@ -235,7 +245,7 @@ def __find_apps_path(lib_dir: Path): lib_dir: library path Returns: - application path if found, else empty string + application path, or empty string if not found """ if lib_dir.exists(): @@ -251,7 +261,7 @@ def __find_otb_root(): """Search for OTB root directory in well known locations. Returns: - str path of the OTB directory + str path of the OTB directory, or None if not found """ prefix = None @@ -339,5 +349,5 @@ def __suggest_fix_import(error_message: str, prefix: str): # This part of pyotb is the first imported during __init__ and checks if OTB is found -# If OTB is not found, a SystemExit is raised, to prevent execution of the core module +# If OTB isn't found, a SystemExit is raised to prevent execution of the core module find_otb() diff --git a/pyotb/install.py b/pyotb/install.py index 47a6ee7..219967a 100644 --- a/pyotb/install.py +++ b/pyotb/install.py @@ -33,15 +33,17 @@ def otb_latest_release_tag(): return releases[-1] -def check_versions(sysname: str, python_minor: int, otb_major: int): +def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[bool, int]: """Verify if python version is compatible with major OTB version. Args: sysname: OTB's system name convention (Linux64, Darwin64, Win64) python_minor: minor version of python otb_major: major version of OTB to be installed + Returns: (True, 0) or (False, expected_version) if case of version conflict + """ if sysname == "Win64": expected = 5 if otb_major in (6, 7) else 7 @@ -103,6 +105,10 @@ def install_otb(version: str = "latest", path: str = "", edit_env: bool = True): Returns: full path of the new installation + Raises: + SystemExit: if python version is not compatible with major OTB version + SystemError: if automatic env config failed + """ # Read env config if sys.version_info.major == 2: @@ -120,6 +126,7 @@ def install_otb(version: str = "latest", path: str = "", edit_env: bool = True): raise SystemExit( f"Python 3.{expected} is required to import bindings on Windows." ) + # Fetch archive and run installer filename = f"OTB-{version}-{sysname}.{ext}" url = f"https://www.orfeo-toolbox.org/packages/archives/OTB/{filename}" -- GitLab From 3da1b9f61c955faa7e3a9db7fa0ac1217dc0bcd8 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 17:14:13 +0100 Subject: [PATCH 08/30] CI: codespell typos --- pyotb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index 0f27547..d6336b5 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -706,7 +706,7 @@ class App(OTBObject): Raises: KeyError: when the parameter name wasn't recognized - RuntimeError: failed to set parameter valie + RuntimeError: failed to set parameter value """ parameters = kwargs -- GitLab From 0770dac0db935663b42193ed457ffb6ca97af7ab Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 17:16:32 +0100 Subject: [PATCH 09/30] ENH: use integer in check_versions --- pyotb/install.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyotb/install.py b/pyotb/install.py index 219967a..2d5561c 100644 --- a/pyotb/install.py +++ b/pyotb/install.py @@ -33,7 +33,7 @@ def otb_latest_release_tag(): return releases[-1] -def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[bool, int]: +def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[int]: """Verify if python version is compatible with major OTB version. Args: @@ -42,22 +42,22 @@ def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[boo otb_major: major version of OTB to be installed Returns: - (True, 0) or (False, expected_version) if case of version conflict + (1, 0) or (0, expected_version) if case of version conflict """ if sysname == "Win64": expected = 5 if otb_major in (6, 7) else 7 if python_minor == expected: - return True, 0 + return 1, 0 elif sysname == "Darwin64": expected = 7, 0 if python_minor == expected: - return True, 0 + return 1, 0 elif sysname == "Linux64": expected = 5 if otb_major in (6, 7) else 8 if python_minor == expected: - return True, 0 - return False, expected + return 1, 0 + return 0, expected def env_config_unix(otb_path: Path): -- GitLab From 853b206fba8701ff304a726fc44c8de8dd761159 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 17:28:47 +0100 Subject: [PATCH 10/30] FIX: regression in __set_param --- pyotb/core.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index d6336b5..80916d0 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1034,12 +1034,9 @@ class App(OTBObject): f" found in '{key}' list: {inp}" ) # List of any other types (str, int...) - elif self.is_key_list(key): - self.app.SetParameterValue(key, obj) else: - raise TypeError( - f"{self.name}: wrong input parameter type ({type(obj)}) for '{key}'" - ) + # TODO: use self.is_key_list, but this is not working for ExtractROI param "cl" which is ParameterType_UNKNOWN + self.app.SetParameterValue(key, obj) def __sync_parameters(self): """Save app parameters in _auto_parameters or data dict. -- GitLab From e1791ea4b0ceb3069ae6012a11b8c9842f70a944 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 18:06:47 +0100 Subject: [PATCH 11/30] BUG: add missing annotations import from __future__ --- pyotb/install.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyotb/install.py b/pyotb/install.py index 2d5561c..5a8256a 100644 --- a/pyotb/install.py +++ b/pyotb/install.py @@ -1,4 +1,6 @@ """This module contains functions for interactive auto installation of OTB.""" +from __future__ import annotations + import json import os import re -- GitLab From d09c718ad945fc0a0632a1248cd28fef9fe415c2 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 18:07:39 +0100 Subject: [PATCH 12/30] DOC: remove useless comment since bug is fixed --- pyotb/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyotb/functions.py b/pyotb/functions.py index 8fde2cd..fe93641 100644 --- a/pyotb/functions.py +++ b/pyotb/functions.py @@ -458,7 +458,6 @@ def define_processing_area( "mode.extent.lry": lry, } new_input = App("ExtractROI", params, quiet=True) - # OTB 7.4 fixes this bug, how to handle different versions of OTB? new_inputs.append(new_input) # Potentially update the reference inputs for later resampling if str(inp) == str(reference_pixel_size_input): -- GitLab From 5c8f8dcccc73ddbbe1042208488f299aea44c03f Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 18:52:24 +0100 Subject: [PATCH 13/30] ENH: back to bool since type hints bug is fixed --- pyotb/install.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyotb/install.py b/pyotb/install.py index 5a8256a..0cf139e 100644 --- a/pyotb/install.py +++ b/pyotb/install.py @@ -35,7 +35,7 @@ def otb_latest_release_tag(): return releases[-1] -def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[int]: +def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[bool, int]: """Verify if python version is compatible with major OTB version. Args: @@ -44,22 +44,22 @@ def check_versions(sysname: str, python_minor: int, otb_major: int) -> tuple[int otb_major: major version of OTB to be installed Returns: - (1, 0) or (0, expected_version) if case of version conflict + (True, 0) if compatible or (False, expected_version) in case of conflicts """ if sysname == "Win64": expected = 5 if otb_major in (6, 7) else 7 if python_minor == expected: - return 1, 0 + return True, 0 elif sysname == "Darwin64": expected = 7, 0 if python_minor == expected: - return 1, 0 + return True, 0 elif sysname == "Linux64": expected = 5 if otb_major in (6, 7) else 8 if python_minor == expected: - return 1, 0 - return 0, expected + return True, 0 + return False, expected def env_config_unix(otb_path: Path): -- GitLab From 014a07574aa05a0cffb7c573564b5815e77ef0a8 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Fri, 3 Nov 2023 19:35:57 +0100 Subject: [PATCH 14/30] ENH: remove useless exceptions since already checked in set_parameters --- pyotb/core.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 80916d0..df3297a 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1025,14 +1025,9 @@ class App(OTBObject): self.app.ConnectImage(key, inp.app, inp.output_image_key) elif isinstance(inp, otb.Application): self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0]) - elif isinstance(inp, (str, Path)): + else: # Append `input` to the list, do not overwrite any previously set element of the image list self.app.AddParameterStringList(key, inp) - else: - raise TypeError( - f"{self.name}: wrong input parameter type ({type(inp)})" - f" found in '{key}' list: {inp}" - ) # List of any other types (str, int...) else: # TODO: use self.is_key_list, but this is not working for ExtractROI param "cl" which is ParameterType_UNKNOWN -- GitLab From 848f4e4aab6d81ad773f81fcf15cef9d2e23fb87 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 14:15:31 +0100 Subject: [PATCH 15/30] ENH: add exception, use if self.is_key_list(key) in __set_param --- pyotb/core.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index df3297a..c0a824b 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -534,11 +534,12 @@ class App(OTBObject): otb.ParameterType_InputImageList, otb.ParameterType_InputFilenameList, ] - INPUT_LIST_TYPES = [ + INPUT_LIST_TYPES = INPUT_IMAGES_LIST_TYPES + [ otb.ParameterType_StringList, otb.ParameterType_ListView, otb.ParameterType_InputVectorDataList, - ] + INPUT_IMAGES_LIST_TYPES + otb.ParameterType_Band, + ] def __init__( self, @@ -1025,13 +1026,17 @@ class App(OTBObject): self.app.ConnectImage(key, inp.app, inp.output_image_key) elif isinstance(inp, otb.Application): self.app.ConnectImage(key, obj, get_out_images_param_keys(inp)[0]) + # Here inp is either str or Path, already checked by __check_*_param else: - # Append `input` to the list, do not overwrite any previously set element of the image list + # Append it to the list, do not overwrite any previously set element of the image list self.app.AddParameterStringList(key, inp) # List of any other types (str, int...) - else: - # TODO: use self.is_key_list, but this is not working for ExtractROI param "cl" which is ParameterType_UNKNOWN + elif self.is_key_list(key): self.app.SetParameterValue(key, obj) + else: + raise TypeError( + f"{self.name}: wrong parameter type ({type(obj)}) for '{key}'" + ) def __sync_parameters(self): """Save app parameters in _auto_parameters or data dict. -- GitLab From 8b257b5073c5ce5ce142a73eff8e3f32cb569c14 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 14:40:24 +0100 Subject: [PATCH 16/30] ENH: type hints (float | int) -> (float) --- pyotb/core.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index c0a824b..f03e9e1 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -294,35 +294,35 @@ class OTBObject(ABC): return NotImplemented # this enables to fallback on numpy emulation thanks to __array_ufunc__ return op_cls(name, x, y) - def __add__(self, other: OTBObject | str | int | float) -> Operation: + def __add__(self, other: OTBObject | str | float) -> Operation: """Addition.""" return self.__create_operator(Operation, "+", self, other) - def __sub__(self, other: OTBObject | str | int | float) -> Operation: + def __sub__(self, other: OTBObject | str | float) -> Operation: """Subtraction.""" return self.__create_operator(Operation, "-", self, other) - def __mul__(self, other: OTBObject | str | int | float) -> Operation: + def __mul__(self, other: OTBObject | str | float) -> Operation: """Multiplication.""" return self.__create_operator(Operation, "*", self, other) - def __truediv__(self, other: OTBObject | str | int | float) -> Operation: + def __truediv__(self, other: OTBObject | str | float) -> Operation: """Division.""" return self.__create_operator(Operation, "/", self, other) - def __radd__(self, other: OTBObject | str | int | float) -> Operation: + def __radd__(self, other: OTBObject | str | float) -> Operation: """Right addition.""" return self.__create_operator(Operation, "+", other, self) - def __rsub__(self, other: OTBObject | str | int | float) -> Operation: + def __rsub__(self, other: OTBObject | str | float) -> Operation: """Right subtraction.""" return self.__create_operator(Operation, "-", other, self) - def __rmul__(self, other: OTBObject | str | int | float) -> Operation: + def __rmul__(self, other: OTBObject | str | float) -> Operation: """Right multiplication.""" return self.__create_operator(Operation, "*", other, self) - def __rtruediv__(self, other: OTBObject | str | int | float) -> Operation: + def __rtruediv__(self, other: OTBObject | str | float) -> Operation: """Right division.""" return self.__create_operator(Operation, "/", other, self) @@ -330,35 +330,35 @@ class OTBObject(ABC): """Absolute value.""" return Operation("abs", self) - def __ge__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __ge__(self, other: OTBObject | str | float) -> LogicalOperation: """Greater of equal than.""" return self.__create_operator(LogicalOperation, ">=", self, other) - def __le__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __le__(self, other: OTBObject | str | float) -> LogicalOperation: """Lower of equal than.""" return self.__create_operator(LogicalOperation, "<=", self, other) - def __gt__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __gt__(self, other: OTBObject | str | float) -> LogicalOperation: """Greater than.""" return self.__create_operator(LogicalOperation, ">", self, other) - def __lt__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __lt__(self, other: OTBObject | str | float) -> LogicalOperation: """Lower than.""" return self.__create_operator(LogicalOperation, "<", self, other) - def __eq__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __eq__(self, other: OTBObject | str | float) -> LogicalOperation: """Equality.""" return self.__create_operator(LogicalOperation, "==", self, other) - def __ne__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __ne__(self, other: OTBObject | str | float) -> LogicalOperation: """Inequality.""" return self.__create_operator(LogicalOperation, "!=", self, other) - def __or__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __or__(self, other: OTBObject | str | float) -> LogicalOperation: """Logical or.""" return self.__create_operator(LogicalOperation, "||", self, other) - def __and__(self, other: OTBObject | str | int | float) -> LogicalOperation: + def __and__(self, other: OTBObject | str | float) -> LogicalOperation: """Logical and.""" return self.__create_operator(LogicalOperation, "&&", self, other) @@ -1085,7 +1085,7 @@ class App(OTBObject): # Special functions def __getitem__( self, key: str | tuple - ) -> Any | list[int | float] | int | float | Slicer: + ) -> Any | list[float] | float | Slicer: """This function is called when we use App()[...]. We allow to return attr if key is a parameter, or call OTBObject __getitem__ for pixel values or Slicer @@ -1255,7 +1255,7 @@ class Operation(App): appname, il=self.unique_inputs, exp=self.exp, quiet=True, name=name ) - def get_nb_bands(self, inputs: list[OTBObject | str | int | float]) -> int: + def get_nb_bands(self, inputs: list[OTBObject | str | float]) -> int: """Guess the number of bands of the output image, from the inputs. Args: @@ -1284,7 +1284,7 @@ class Operation(App): def build_fake_expressions( self, operator: str, - inputs: list[OTBObject | str | int | float], + inputs: list[OTBObject | str | float], nb_bands: int = None, ): """Create a list of 'fake' expressions, one for each band. @@ -1455,7 +1455,7 @@ class LogicalOperation(Operation): def build_fake_expressions( self, operator: str, - inputs: list[OTBObject | str | int | float], + inputs: list[OTBObject | str | float], nb_bands: int = None, ): """Create a list of 'fake' expressions, one for each band. -- GitLab From d4384d14129d1c5843e8bb191a52bb24a879b6e1 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 15:06:48 +0100 Subject: [PATCH 17/30] ENH: summarize comments --- pyotb/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index f03e9e1..b614a4d 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1758,6 +1758,7 @@ def summarize( return [summarize(o) for o in obj] if isinstance(obj, Output): return summarize(obj.parent_pyotb_app) + # => This is the deepest recursion level if not isinstance(obj, App): return obj @@ -1769,8 +1770,8 @@ def summarize( return param.split("?")[0] # Call / top level of recursion : obj is an App - # We need to return parameters values, summarized if param is an App parameters = {} + # We need to return parameters values, summarized if param is an App for key, param in obj.parameters.items(): if strip_inpath and obj.is_input(key) or strip_outpath and obj.is_output(key): parameters[key] = strip_path(param) -- GitLab From b219d44312a63d0e01372f3d1310c5a4f40e2d9b Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 15:06:50 +0100 Subject: [PATCH 18/30] DOC: avoid multiline argument text in docstrings --- pyotb/core.py | 104 +++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index b614a4d..5528039 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -136,14 +136,14 @@ class OTBObject(ABC): return App("ComputeImagesStatistics", self, quiet=True).data def get_values_at_coords( - self, row: int, col: int, bands: int = None + self, row: int, col: int, bands: int | list[int] = None ) -> list[float] | float: """Get pixel value(s) at a given YX coordinates. Args: row: index along Y / latitude axis col: index along X / longitude axis - bands: band number, list or slice to fetch values from + bands: band number(s) to fetch values from Returns: single numerical value or a list of values for each band @@ -209,8 +209,7 @@ class OTBObject(ABC): Args: key: parameter key to export, if None then the default one will be used - preserve_dtype: when set to True, the numpy array is converted to the same pixel type as - the App first output. Default is True + preserve_dtype: convert the array to the same pixel type as the App first output Returns: the exported numpy array @@ -231,12 +230,13 @@ class OTBObject(ABC): ) -> np.ndarray: """Export a pyotb object to numpy array. + A copy is avoided by default, but may be required if preserve_dtype is False + and the source app reference is lost. + Args: key: the output parameter name to export as numpy array - preserve_dtype: when set to True, the numpy array is converted to the same pixel type as - the App first output. Default is True - copy: whether to copy the output array, default is False - required to True if preserve_dtype is False and the source app reference is lost + preserve_dtype: convert the array to the same pixel type as the App first output + copy: whether to copy the output array instead of returning a reference Returns: a numpy array that may already have been cached in self.exports_dic @@ -378,12 +378,12 @@ class OTBObject(ABC): """This is called whenever a numpy function is called on a pyotb object. Operation is performed in numpy, then imported back to pyotb with the same georeference as input. + At least one obj is unputs has to be an OTBObject. Args: ufunc: numpy function method: an internal numpy argument - inputs: inputs, at least one being pyotb object. If there are several pyotb objects, they must all have - the same georeference and pixel size. + inputs: inputs, with equal shape in case of several images / OTBObject **kwargs: kwargs of the numpy function Returns: @@ -552,18 +552,19 @@ class App(OTBObject): ): """Common constructor for OTB applications. Handles in-memory connection between apps. + There are several ways to pass parameters to init the app. *args can be : + - dictionary containing key-arguments enumeration. Useful when a key is python-reserved + (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit") + - string, App or Output, useful when the user wants to specify the input "in" + - list, useful when the user wants to specify the input list 'il' + Args: appname: name of the OTB application to initialize, e.g. 'BandMath' - *args: used for passing application parameters. Can be : - - dictionary containing key-arguments enumeration. Useful when a key is python-reserved - (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit") - - string, App or Output, useful when the user wants to specify the input "in" - - list, useful when the user wants to specify the input list 'il' + *args: used to pass an app input as argument and ommiting the key frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___ quiet: whether to print logs of the OTB app name: custom name that will show up in logs, appname will be used if not provided - **kwargs: used for passing application parameters. - e.g. il=['input1.tif', App_object2, App_object3.out], out='output.tif' + **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"]) """ # Attributes and data structures used by properties @@ -700,10 +701,8 @@ class App(OTBObject): instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths Args: - *args: Can be : - dictionary containing key-arguments enumeration. Useful when a keyword is reserved (e.g. "in") - - string or OTBObject, useful when the user implicitly wants to set the param "in" - - list, useful when the user implicitly wants to set the param "il" - **kwargs: keyword arguments e.g. il=['input1.tif', oApp_object2, App_object3.out], out='output.tif' + *args: any input OTBObject, filepath or images list, or a dict of parameters + **kwargs: app parameters, with "_" instead of dots e.g. io_in="image.tif" Raises: KeyError: when the parameter name wasn't recognized @@ -818,20 +817,19 @@ class App(OTBObject): ) -> bool: """Set output pixel type and write the output raster files. + The first argument is expected to be: + - filepath, useful when there is only one output, e.g. 'output.tif' + - dictionary containing output filepath + - None if output file was passed during App init + In case of multiple outputs, pixel_type may also be a dictionary with parameter names as keys. + Accepted pixel types : uint8, uint16, uint32, int16, int32, float, double, cint16, cint32, cfloat, cdouble + Args: - path: Can be : - filepath, useful when there is only one output, e.g. 'output.tif' - - dictionary containing key-arguments enumeration. Useful when a key contains - non-standard characters such as a point, e.g. {'io.out':'output.tif'} - - None if output file was passed during App init - pixel_type: Can be : - dictionary {out_param_key: pixeltype} when specifying for several outputs - - str (e.g. 'uint16') or otbApplication.ImagePixelType_... When there are several - outputs, all outputs are written with this unique type. - Valid pixel types are uint8, uint16, uint32, int16, int32, float, double, - cint16, cint32, cfloat, cdouble. (Default value = None) + path: output filepath or dict of filepath with param keys + pixel_type: pixel type string representation preserve_dtype: propagate main input pixel type to outputs, in case pixel_type is None - ext_fname: Optional, an extended filename as understood by OTB (e.g. "&gdal:co:TILED=YES") - Will be used for all outputs (Default value = None) - **kwargs: keyword arguments e.g. out='output.tif' + ext_fname: an OTB extended filename, will be applied to every output (but won't overwrite existing keys in output filepath) + **kwargs: keyword arguments e.g. out='output.tif' or io_out='output.tif' Returns: True if all files are found on disk @@ -1125,7 +1123,7 @@ class Slicer(App): obj: input rows: slice along Y / Latitude axis cols: slice along X / Longitude axis - channels: bands to extract. can be slicing, list or int + channels: bands to extract Raises: TypeError: if channels param isn't slice, list or int @@ -1218,7 +1216,7 @@ class Operation(App): Args: operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ? - *inputs: operands, can be OTBObject, filepath, int or float + *inputs: operands of the expression to build nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where name: override the default Operation name @@ -1292,7 +1290,7 @@ class Operation(App): E.g for the operation input1 + input2, we create a fake expression that is like "str(input1) + str(input2)" Args: - operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ? + operator: one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ? inputs: inputs. Can be OTBObject, filepath, int or float nb_bands: to specify the output nb of bands. Optional. Used only internally by pyotb.where @@ -1375,14 +1373,14 @@ class Operation(App): """This an internal function, only to be used by `build_fake_expressions`. Enable to create a fake expression just for one input and one band. + Regarding the "keep_logical" param: + - if True, for `input1 > input2`, returned fake expression is "str(input1) > str(input2)" + - if False, for `input1 > input2`, returned fake exp is "str(input1) > str(input2) ? 1 : 0"] Default False Args: x: input band: which band to consider (bands start at 1) keep_logical: whether to keep the logical expressions "as is" in case the input is a logical operation. - ex: if True, for `input1 > input2`, returned fake expression is "str(input1) > str(input2)" - if False, for `input1 > input2`, returned fake exp is "str(input1) > str(input2) ? 1 : 0"] - Default False Returns: fake_exp: the fake expression for this band and input @@ -1498,7 +1496,7 @@ class Input(App): """Default constructor. Args: - filepath: Anything supported by GDAL (local file on the filesystem, remote resource e.g. /vsicurl/.., etc.) + filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.) """ super().__init__("ExtractROI", {"in": filepath}, quiet=True, frozen=True) @@ -1625,7 +1623,7 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int: """Get the nb of bands of input image. Args: - inp: can be filepath or OTBObject object + inp: input file or OTBObject Returns: number of bands in image @@ -1651,14 +1649,16 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int: def get_pixel_type(inp: str | Path | OTBObject) -> str: - """Get the encoding of input image pixels. + """Get the encoding of input image pixels as integer enum. + + OTB enum e.g. `otbApplication.ImagePixelType_uint8'. + For an OTBObject with several outputs, only the pixel type of the first output is returned Args: - inp: can be filepath or pyotb object + inp: input file or OTBObject Returns: - pixel_type: OTB enum e.g. `otbApplication.ImagePixelType_uint8', which actually is an int. - For an OTBObject with several outputs, only the pixel type of the first output is returned + OTB enum Raises: TypeError: if inp pixel type cannot be retrieved @@ -1685,7 +1685,7 @@ def parse_pixel_type(pixel_type: str | int) -> int: """Convert one str pixel type to OTB integer enum if necessary. Args: - pixel_type: pixel type. can be int or str (either OTB or numpy convention) + pixel_type: pixel type to parse Returns: pixel_type OTB enum integer value @@ -1739,21 +1739,19 @@ def summarize( At the deepest recursion level, this function just return any parameter value, path stripped if needed, or app summarized in case of a pipeline. + If strip_path is enabled, paths are truncated after the first "?" character. + Can be useful to remove URLs tokens from inputs (e.g. SAS or S3 credentials), + or extended filenames from outputs. Args: obj: input object / parameter value to summarize - strip_inpath: strip all input paths: If enabled, paths related to - inputs are truncated after the first "?" character. Can be useful - to remove URLs tokens (e.g. SAS or S3 credentials). - strip_outpath: strip all output paths: If enabled, paths related - to outputs are truncated after the first "?" character. Can be - useful to remove extended filenames. + strip_inpath: strip all input paths + strip_outpath: strip all output paths Returns: nested dictionary containing name and parameters of an app and its parents """ - # This is the deepest recursion level if isinstance(obj, list): return [summarize(o) for o in obj] if isinstance(obj, Output): -- GitLab From d729f02568fc4949538be2b413630efc418c0976 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 15:11:40 +0100 Subject: [PATCH 19/30] STYLE: apply black --- pyotb/core.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 5528039..5c33706 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1081,9 +1081,7 @@ class App(OTBObject): self.data[key] = value # Special functions - def __getitem__( - self, key: str | tuple - ) -> Any | list[float] | float | Slicer: + def __getitem__(self, key: str | tuple) -> Any | list[float] | float | Slicer: """This function is called when we use App()[...]. We allow to return attr if key is a parameter, or call OTBObject __getitem__ for pixel values or Slicer @@ -1270,9 +1268,7 @@ class Operation(App): return 1 # Check that all inputs have the same band count nb_bands_list = [ - get_nbchannels(inp) - for inp in inputs - if not isinstance(inp, (float, int)) + get_nbchannels(inp) for inp in inputs if not isinstance(inp, (float, int)) ] all_same = all(x == nb_bands_list[0] for x in nb_bands_list) if len(nb_bands_list) > 1 and not all_same: @@ -1373,7 +1369,7 @@ class Operation(App): """This an internal function, only to be used by `build_fake_expressions`. Enable to create a fake expression just for one input and one band. - Regarding the "keep_logical" param: + Regarding the "keep_logical" param: - if True, for `input1 > input2`, returned fake expression is "str(input1) > str(input2)" - if False, for `input1 > input2`, returned fake exp is "str(input1) > str(input2) ? 1 : 0"] Default False @@ -1689,7 +1685,7 @@ def parse_pixel_type(pixel_type: str | int) -> int: Returns: pixel_type OTB enum integer value - + Raises: KeyError: if pixel_type name is unknown TypeError: if type(pixel_type) isn't int or str -- GitLab From 09bfa60e027fb6bc7fd6b036e2d97649e4eb5952 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 15:17:17 +0100 Subject: [PATCH 20/30] DOC: typo --- pyotb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index 5c33706..b6655f3 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -560,7 +560,7 @@ class App(OTBObject): Args: appname: name of the OTB application to initialize, e.g. 'BandMath' - *args: used to pass an app input as argument and ommiting the key + *args: used to pass an app input as argument and omitting the key frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___ quiet: whether to print logs of the OTB app name: custom name that will show up in logs, appname will be used if not provided -- GitLab From 3ee79f5672397fd4d985a7c43f96d9c6a0182482 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 15:38:43 +0100 Subject: [PATCH 21/30] DOC: test move docs from __init__ to class docstring --- pyotb/core.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index b6655f3..f578a66 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -513,7 +513,24 @@ class OTBObject(ABC): class App(OTBObject): - """Base class that gathers common operations for any OTB application.""" + """Wrapper around otb.Application to handle settings and execution. + + Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.) + There are several ways to pass parameters to init the app. *args can be : + - dictionary containing key-arguments enumeration. Useful when a key is python-reserved + (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit") + - string, App or Output, useful when the user wants to specify the input "in" + - list, useful when the user wants to specify the input list 'il' + + Args: + appname: name of the OTB application to initialize, e.g. 'BandMath' + *args: used to pass an app input as argument and omitting the key + frozen: freeze OTB app in order avoid blocking during __init___ + quiet: whether to print logs of the OTB app + name: custom name that will show up in logs, appname will be used if not provided + **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"]) + + """ INPUT_IMAGE_TYPES = [ otb.ParameterType_InputImage, @@ -550,23 +567,7 @@ class App(OTBObject): name: str = "", **kwargs, ): - """Common constructor for OTB applications. Handles in-memory connection between apps. - - There are several ways to pass parameters to init the app. *args can be : - - dictionary containing key-arguments enumeration. Useful when a key is python-reserved - (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit") - - string, App or Output, useful when the user wants to specify the input "in" - - list, useful when the user wants to specify the input list 'il' - - Args: - appname: name of the OTB application to initialize, e.g. 'BandMath' - *args: used to pass an app input as argument and omitting the key - frozen: freeze OTB app in order to use execute() later and avoid blocking process during __init___ - quiet: whether to print logs of the OTB app - name: custom name that will show up in logs, appname will be used if not provided - **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"]) - - """ + """Common constructor for OTB applications, automatically handles in-memory connections.""" # Attributes and data structures used by properties create = ( otb.Registry.CreateApplicationWithoutLogger -- GitLab From d44ffae4691678b535c067effa31a3d4677e5371 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 15:51:57 +0100 Subject: [PATCH 22/30] ENH: App docstring --- pyotb/core.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index f578a66..d6a60d6 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -16,7 +16,7 @@ from .depreciation import deprecated_alias, depreciation_warning, deprecated_att class OTBObject(ABC): - """Abstraction of an image object.""" + """Abstraction of an image object, for a whole app or one specific output.""" @property @abstractmethod @@ -269,7 +269,7 @@ class OTBObject(ABC): y: latitude or projected Y Returns: - pixel index: (row, col) + pixel index as (row, col) """ spacing_x, _, origin_x, _, spacing_y, origin_y = self.transform @@ -516,17 +516,17 @@ class App(OTBObject): """Wrapper around otb.Application to handle settings and execution. Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.) - There are several ways to pass parameters to init the app. *args can be : - - dictionary containing key-arguments enumeration. Useful when a key is python-reserved - (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit") - - string, App or Output, useful when the user wants to specify the input "in" - - list, useful when the user wants to specify the input list 'il' + Any app parameter may be passed either using a dict of parameters or keyword argument. + The first argument can be : + - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in") + - string, App or Output, useful when the user wants to specify the input "in" + - list, useful when the user wants to specify the input list 'il' Args: appname: name of the OTB application to initialize, e.g. 'BandMath' *args: used to pass an app input as argument and omitting the key frozen: freeze OTB app in order avoid blocking during __init___ - quiet: whether to print logs of the OTB app + quiet: whether to print logs of the OTB app and the default progress bar name: custom name that will show up in logs, appname will be used if not provided **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"]) -- GitLab From 12881408bcb8308f9834d0df6e9eaebb6ed3e147 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 15:57:02 +0100 Subject: [PATCH 23/30] DOC: enh class docstring --- pyotb/core.py | 106 ++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index d6a60d6..29535f3 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -517,7 +517,7 @@ class App(OTBObject): Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.) Any app parameter may be passed either using a dict of parameters or keyword argument. - The first argument can be : + The first argument can be : - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in") - string, App or Output, useful when the user wants to specify the input "in" - list, useful when the user wants to specify the input list 'il' @@ -1103,7 +1103,22 @@ class App(OTBObject): class Slicer(App): - """Slicer objects i.e. when we call something like raster[:, :, 2] from Python.""" + """Slicer objects, automatically created when using slicing e.g. app[:, :, 2]. + + It contains : + - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines + - in case the user only wants to extract one band, an expression such as "im1b#" + + Args: + obj: input + rows: slice along Y / Latitude axis + cols: slice along X / Longitude axis + channels: bands to extract + + Raises: + TypeError: if channels param isn't slice, list or int + + """ def __init__( self, @@ -1112,22 +1127,7 @@ class Slicer(App): cols: slice, channels: slice | list[int] | int, ): - """Create a slicer object, that can be used directly for writing or inside a BandMath. - - It contains : - - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines - - in case the user only wants to extract one band, an expression such as "im1b#" - - Args: - obj: input - rows: slice along Y / Latitude axis - cols: slice along X / Longitude axis - channels: bands to extract - - Raises: - TypeError: if channels param isn't slice, list or int - - """ + """Create a slicer object, that can be used directly for writing or inside a BandMath.""" super().__init__( "ExtractROI", obj, @@ -1189,6 +1189,16 @@ class Slicer(App): class Operation(App): """Class for arithmetic/math operations done in Python. + Given some inputs and an operator, this object enables to python operator to a BandMath operation. + Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator. + It can have 3 inputs for the ternary operator `cond ? x : y`. + + Args: + operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ? + *inputs: operands of the expression to build + nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where + name: override the default Operation name + Example: Consider the python expression (input1 + 2 * input2) > 0. This class enables to create a BandMathX app, with expression such as (im2 + 2 * im1) > 0 ? 1 : 0 @@ -1208,18 +1218,7 @@ class Operation(App): """ def __init__(self, operator: str, *inputs, nb_bands: int = None, name: str = None): - """Given some inputs and an operator, this object enables to python operator to a BandMath operation. - - Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator. - It can have 3 inputs for the ternary operator `cond ? x : y`. - - Args: - operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ? - *inputs: operands of the expression to build - nb_bands: optionally specify the output nb of bands - used only internally by pyotb.where - name: override the default Operation name - - """ + """Operation constructor, one part of the logic is handled by App.__create_operator""" self.operator = operator # We first create a 'fake' expression. E.g for the operation `input1 + input2` # we create a fake expression like "str(input1) + str(input2)" @@ -1426,21 +1425,18 @@ class LogicalOperation(Operation): """A specialization of Operation class for boolean logical operations. Supported operators are >, <, >=, <=, ==, !=, `&` and `|`. - The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"), but also the logical expression (e.g. "im1b1 > 0") + Args: + operator: string operator (one of >, <, >=, <=, ==, !=, &, |) + *inputs: inputs + nb_bands: optionally specify the output nb of bands - used only by pyotb.where + """ def __init__(self, operator: str, *inputs, nb_bands: int = None): - """Constructor for a LogicalOperation object. - - Args: - operator: string operator (one of >, <, >=, <=, ==, !=, &, |) - *inputs: inputs - nb_bands: optionally specify the output nb of bands - used only by pyotb.where - - """ + """Constructor for a LogicalOperation object.""" self.logical_fake_exp_bands = [] super().__init__(operator, *inputs, nb_bands=nb_bands, name="LogicalOperation") self.logical_exp_bands, self.logical_exp = self.get_real_exp( @@ -1487,15 +1483,15 @@ class LogicalOperation(Operation): class Input(App): - """Class for transforming a filepath to pyOTB object.""" + """Class for transforming a filepath to pyOTB object. - def __init__(self, filepath: str): - """Default constructor. + Args: + filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.) - Args: - filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.) + """ - """ + def __init__(self, filepath: str): + """Initialize an ExtractROI OTB app from a filepath, set dtype and store filepath.""" super().__init__("ExtractROI", {"in": filepath}, quiet=True, frozen=True) self._name = f"Input from {filepath}" if not filepath.startswith(("/vsi", "http://", "https://", "ftp://")): @@ -1510,7 +1506,15 @@ class Input(App): class Output(OTBObject): - """Object that behave like a pointer to a specific application in-memory output or file.""" + """Object that behave like a pointer to a specific application in-memory output or file. + + Args: + pyotb_app: The pyotb App to store reference from + param_key: Output parameter key of the target app + filepath: path of the output file (if not memory) + mkdir: create missing parent directories + + """ _filepath: str | Path = None @@ -1522,15 +1526,7 @@ class Output(OTBObject): filepath: str = None, mkdir: bool = True, ): - """Constructor for an Output object. - - Args: - pyotb_app: The pyotb App to store reference from - param_key: Output parameter key of the target app - filepath: path of the output file (if not memory) - mkdir: create missing parent directories - - """ + """Constructor for an Output object, initialized during App.__init__.""" self.parent_pyotb_app = pyotb_app # keep trace of parent app self.param_key = param_key self.filepath = filepath -- GitLab From 212e40feebade49a6cb65a35bd788fb3805050a5 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 16:04:18 +0100 Subject: [PATCH 24/30] DOC: format indent to fix bulletpoints --- pyotb/core.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 29535f3..7f60d0b 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -467,10 +467,8 @@ class OTBObject(ABC): """Override the default __getitem__ behaviour. This function enables 2 things : - - slicing, i.e. selecting ROI/bands. For example, selecting first 3 bands: object[:, :, :3] - selecting bands 1, 2 & 5 : object[:, :, [0, 1, 4]] - selecting 1000x1000 subset : object[:1000, :1000] - - access pixel value(s) at a specified row, col index + - slicing, i.e. selecting ROI/bands + - access pixel value(s) at a specified row, col index Args: key: attribute key @@ -517,10 +515,10 @@ class App(OTBObject): Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.) Any app parameter may be passed either using a dict of parameters or keyword argument. - The first argument can be : - - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in") - - string, App or Output, useful when the user wants to specify the input "in" - - list, useful when the user wants to specify the input list 'il' + The first argument can be: + - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in") + - string, App or Output, useful when the user wants to specify the input "in" + - list, useful when the user wants to specify the input list 'il' Args: appname: name of the OTB application to initialize, e.g. 'BandMath' @@ -1105,9 +1103,9 @@ class App(OTBObject): class Slicer(App): """Slicer objects, automatically created when using slicing e.g. app[:, :, 2]. - It contains : - - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines - - in case the user only wants to extract one band, an expression such as "im1b#" + It contains: + - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines + - in case the user only wants to extract one band, an expression such as "im1b#" Args: obj: input -- GitLab From 36d0892b9303c6a948e8a3af16465898f0885ce7 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 16:17:21 +0100 Subject: [PATCH 25/30] DOC: enh docstrings with bulletpoints --- pyotb/core.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 7f60d0b..47109da 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -515,6 +515,7 @@ class App(OTBObject): Base class that gathers common operations for any OTB application lifetime (settings, exec, export, etc.) Any app parameter may be passed either using a dict of parameters or keyword argument. + The first argument can be: - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in") - string, App or Output, useful when the user wants to specify the input "in" @@ -820,6 +821,7 @@ class App(OTBObject): - filepath, useful when there is only one output, e.g. 'output.tif' - dictionary containing output filepath - None if output file was passed during App init + In case of multiple outputs, pixel_type may also be a dictionary with parameter names as keys. Accepted pixel types : uint8, uint16, uint32, int16, int32, float, double, cint16, cint32, cfloat, cdouble @@ -1103,9 +1105,8 @@ class App(OTBObject): class Slicer(App): """Slicer objects, automatically created when using slicing e.g. app[:, :, 2]. - It contains: - - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines - - in case the user only wants to extract one band, an expression such as "im1b#" + Can be used to select a subset of pixel and / or bands in the image. + This is a shortcut to an ExtractROI app that can be written to disk or used in pipelines. Args: obj: input @@ -1423,8 +1424,8 @@ class LogicalOperation(Operation): """A specialization of Operation class for boolean logical operations. Supported operators are >, <, >=, <=, ==, !=, `&` and `|`. - The only difference is that not only the BandMath expression is saved (e.g. "im1b1 > 0 ? 1 : 0"), - but also the logical expression (e.g. "im1b1 > 0") + The only difference is that not only the BandMath expression is saved + (e.g. "im1b1 > 0 ? 1 : 0"), but also the logical expression (e.g. "im1b1 > 0") Args: operator: string operator (one of >, <, >=, <=, ==, !=, &, |) @@ -1449,8 +1450,8 @@ class LogicalOperation(Operation): ): """Create a list of 'fake' expressions, one for each band. - e.g for the operation input1 > input2, we create a fake expression that is like - "str(input1) > str(input2) ? 1 : 0" and a logical fake expression that is like "str(input1) > str(input2)" + For the operation input1 > input2, we create a fake expression like `str(input1) > str(input2) ? 1 : 0` + and a logical fake expression like `str(input1) > str(input2)` Args: operator: str (one of >, <, >=, <=, ==, !=, &, |) @@ -1630,9 +1631,7 @@ def get_nbchannels(inp: str | Path | OTBObject) -> int: try: info = App("ReadImageInfo", inp, quiet=True) return info["numberbands"] - except ( - RuntimeError - ) as info_err: # this happens when we pass a str that is not a filepath + except RuntimeError as info_err: # e.g. file is missing raise TypeError( f"Could not get the number of channels file '{inp}' ({info_err})" ) from info_err -- GitLab From 8a131d01d0b642a8971489763973a77821a4b422 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 16:23:29 +0100 Subject: [PATCH 26/30] DOC: add missing dot --- pyotb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index 47109da..346dc31 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -1217,7 +1217,7 @@ class Operation(App): """ def __init__(self, operator: str, *inputs, nb_bands: int = None, name: str = None): - """Operation constructor, one part of the logic is handled by App.__create_operator""" + """Operation constructor, one part of the logic is handled by App.__create_operator.""" self.operator = operator # We first create a 'fake' expression. E.g for the operation `input1 + input2` # we create a fake expression like "str(input1) + str(input2)" -- GitLab From b16538db952eec7e012816f4e7d88993c1860d8a Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 16:37:59 +0100 Subject: [PATCH 27/30] ENH: docstrings --- pyotb/core.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 346dc31..bc6529e 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -517,9 +517,9 @@ class App(OTBObject): Any app parameter may be passed either using a dict of parameters or keyword argument. The first argument can be: - - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in") - - string, App or Output, useful when the user wants to specify the input "in" + - string, App or Output, the main input parameter name is automatically set - list, useful when the user wants to specify the input list 'il' + - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in", "map") Args: appname: name of the OTB application to initialize, e.g. 'BandMath' @@ -695,8 +695,10 @@ class App(OTBObject): return self._time_end - self._time_start def set_parameters(self, *args, **kwargs): - """Set some parameters of the app. + """Set parameters, using the right OTB API function depending on the key and type. + Parameters with dots may be passed as keyword arguments using "_", e.g. map_epsg_code=4326. + Additional checks are done for input and output (in-memory objects, remote filepaths, etc.). When useful, e.g. for images list, this function appends the parameters instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths @@ -1482,7 +1484,7 @@ class LogicalOperation(Operation): class Input(App): - """Class for transforming a filepath to pyOTB object. + """Class for transforming a filepath to pyotb object. Args: filepath: Anything supported by GDAL (local file on the filesystem, remote resource, etc.) -- GitLab From 238dce9911046b714eab8baea38ed24dedaeeaf1 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 16:49:11 +0100 Subject: [PATCH 28/30] ENH: App docstring --- pyotb/core.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index bc6529e..8afd168 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -517,17 +517,18 @@ class App(OTBObject): Any app parameter may be passed either using a dict of parameters or keyword argument. The first argument can be: - - string, App or Output, the main input parameter name is automatically set - - list, useful when the user wants to specify the input list 'il' - - dictionary containing key-arguments enumeration. Useful when a key is python-reserved (e.g. "in", "map") + - filepath or OTBObject, the main input parameter name is automatically used + - list of inputs, useful when the user wants to specify the input list `il` + - dictionary of parameters, useful when a key is python-reserved (e.g. `in`, `map`) + Any key except reserved keywards may also be passed via kwargs, if you replace dots with "_" e.g `map_epsg_code=4326` Args: appname: name of the OTB application to initialize, e.g. 'BandMath' - *args: used to pass an app input as argument and omitting the key + *args: can be a filepath, OTB object or a dict or parameters, several dicts will be merged in **kwargs frozen: freeze OTB app in order avoid blocking during __init___ quiet: whether to print logs of the OTB app and the default progress bar name: custom name that will show up in logs, appname will be used if not provided - **kwargs: used for passing application parameters (e.g. il=["image_1.tif", "image_1.tif"]) + **kwargs: any OTB application parameter key is accepted except "in" """ -- GitLab From e55ec155aa0ca9338a66c02d53b6341a081c2fbb Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 17:00:25 +0100 Subject: [PATCH 29/30] STYLE: use comprehensive list instead of filter + whitespace --- pyotb/core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 8afd168..2b7af8c 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -581,6 +581,7 @@ class App(OTBObject): self._time_start, self._time_end = 0.0, 0.0 self.data, self.outputs = {}, {} self.quiet, self.frozen = quiet, frozen + # Param keys and types self.parameters_keys = tuple(self.app.GetParametersKeys()) self._all_param_types = { @@ -596,15 +597,18 @@ class App(OTBObject): for key in self.parameters_keys if self.app.GetParameterType(key) == otb.ParameterType_Choice } + # Init, execute and write (auto flush only when output param was provided) if args or kwargs: self.set_parameters(*args, **kwargs) # Create Output image objects - for key in filter( - lambda k: self._out_param_types[k] == otb.ParameterType_OutputImage, - self._out_param_types, + for key in ( + key + for key, param in self._out_param_types.items() + if param == otb.ParameterType_OutputImage ): self.outputs[key] = Output(self, key, self._settings.get(key)) + if not self.frozen: self.execute() if any(key in self._settings for key in self._out_param_types): -- GitLab From e18bcfc3394a4b1cca5fbe4c22eaee74cb328cf2 Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Mon, 6 Nov 2023 18:37:43 +0100 Subject: [PATCH 30/30] CI: typo --- pyotb/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 2b7af8c..9cd9d4e 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -520,7 +520,7 @@ class App(OTBObject): - filepath or OTBObject, the main input parameter name is automatically used - list of inputs, useful when the user wants to specify the input list `il` - dictionary of parameters, useful when a key is python-reserved (e.g. `in`, `map`) - Any key except reserved keywards may also be passed via kwargs, if you replace dots with "_" e.g `map_epsg_code=4326` + Any key except "in" or "map" can also be passed via kwargs, replace "." with "_" e.g `map_epsg_code=4326` Args: appname: name of the OTB application to initialize, e.g. 'BandMath' @@ -528,7 +528,7 @@ class App(OTBObject): frozen: freeze OTB app in order avoid blocking during __init___ quiet: whether to print logs of the OTB app and the default progress bar name: custom name that will show up in logs, appname will be used if not provided - **kwargs: any OTB application parameter key is accepted except "in" + **kwargs: any OTB application parameter key is accepted except "in" or "map" """ -- GitLab