From 7e2a2c1a89569175e8914082cdc7e4cd6d4fc0bc Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 11 Jan 2023 14:56:44 +0100 Subject: [PATCH 1/7] WIP: proposition to reduce code complexity, simplify app naming --- pyotb/apps.py | 22 ---------------------- pyotb/core.py | 18 ++++++++---------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/pyotb/apps.py b/pyotb/apps.py index 2a85fc6..b3f9fd7 100644 --- a/pyotb/apps.py +++ b/pyotb/apps.py @@ -67,28 +67,6 @@ class App(OTBObject): super().__init__(*args, **kwargs) self.description = self.app.GetDocLongDescription() - @property - def name(self): - """Application name that will be printed in logs. - - Returns: - user's defined name or appname - - """ - return self._name or self.appname - - @name.setter - def name(self, name): - """Set custom name. - - Args: - name: new name - - """ - if not isinstance(name, str): - raise TypeError(f"{self.name}: bad type ({type(name)}) for application name, only str is allowed") - self._name = name - @property def outputs(self): """List of application outputs.""" diff --git a/pyotb/core.py b/pyotb/core.py index cf13497..380602d 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -12,11 +12,11 @@ from .helpers import logger class OTBObject: """Base class that gathers common operations for any OTB application.""" - def __init__(self, appname, *args, frozen=False, quiet=False, image_dic=None, **kwargs): + def __init__(self, name, *args, frozen=False, quiet=False, image_dic=None, **kwargs): """Common constructor for OTB applications. Handles in-memory connection between apps. Args: - appname: name of the app, e.g. 'BandMath' + name: name of the app, 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") @@ -34,15 +34,13 @@ class OTBObject: """ self.parameters = {} - self.name = self.appname = appname + self.name = name self.frozen = frozen self.quiet = quiet self.image_dic = image_dic self.exports_dic = {} - if quiet: - self.app = otb.Registry.CreateApplicationWithoutLogger(appname) - else: - self.app = otb.Registry.CreateApplication(appname) + self.app = otb.Registry.CreateApplicationWithoutLogger(name) if quiet \ + else otb.Registry.CreateApplication(name) self.parameters_keys = tuple(self.app.GetParametersKeys()) self.out_param_types = dict(get_out_param_types(self)) self.out_param_keys = tuple(self.out_param_types.keys()) @@ -479,7 +477,7 @@ class OTBObject: def __str__(self): """Return a nice string representation with object id.""" - return f"<pyotb.App {self.appname} object id {id(self)}>" + return f"<pyotb.App {self.name} object id {id(self)}>" def __getattr__(self, name): """This method is called when the default attribute access fails. @@ -958,8 +956,8 @@ class Operation(OTBObject): self.unique_inputs = [mapping_str_to_input[str_input] for str_input in sorted(self.im_dic, key=self.im_dic.get)] self.exp_bands, self.exp = self.get_real_exp(self.fake_exp_bands) # Execute app - appname = "BandMath" if len(self.exp_bands) == 1 else "BandMathX" - super().__init__(appname, il=self.unique_inputs, exp=self.exp, quiet=True) + name = "BandMath" if len(self.exp_bands) == 1 else "BandMathX" + super().__init__(name, il=self.unique_inputs, exp=self.exp, quiet=True) self.name = f'Operation exp="{self.exp}"' def create_fake_exp(self, operator, inputs, nb_bands=None): -- GitLab From 277579963121dfd0b9bc979a4228836618958809 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 11 Jan 2023 15:34:25 +0100 Subject: [PATCH 2/7] FIX: Input back working with /vsiXX/ resources --- pyotb/core.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pyotb/core.py b/pyotb/core.py index 380602d..532c8f3 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -39,8 +39,8 @@ class OTBObject: self.quiet = quiet self.image_dic = image_dic self.exports_dic = {} - self.app = otb.Registry.CreateApplicationWithoutLogger(name) if quiet \ - else otb.Registry.CreateApplication(name) + create = otb.Registry.CreateApplicationWithoutLogger if quiet else otb.Registry.CreateApplication + self.app = create(name) self.parameters_keys = tuple(self.app.GetParametersKeys()) self.out_param_types = dict(get_out_param_types(self)) self.out_param_keys = tuple(self.out_param_types.keys()) @@ -68,7 +68,7 @@ class OTBObject: @property def dtype(self): - """Expose the pixel type of an output image using numpy convention. + """Expose the pixel type of output image using numpy convention. Returns: dtype: pixel type of the output image @@ -91,7 +91,7 @@ class OTBObject: raise TypeError(f"{self.name}: this application has no raster output") width, height = self.app.GetImageSize(self.key_output_image) bands = self.app.GetImageNbBands(self.key_output_image) - return (height, width, bands) + return height, width, bands @property def transform(self): @@ -106,7 +106,7 @@ class OTBObject: origin_x, origin_y = self.app.GetImageOrigin(self.key_output_image) # Shift image origin since OTB is giving coordinates of pixel center instead of corners origin_x, origin_y = origin_x - spacing_x / 2, origin_y - spacing_y / 2 - return (spacing_x, 0.0, origin_x, 0.0, spacing_y, origin_y) + return spacing_x, 0.0, origin_x, 0.0, spacing_y, origin_y def set_parameters(self, *args, **kwargs): """Set some parameters of the app. @@ -344,10 +344,12 @@ class OTBObject: return {"name": self.name, "parameters": params} def export(self, key=None, preserve_dtype=True): - """Export a specific output image as numpy array and store it in object's exports_dic. + """Export a specific output image as numpy array and store it in object exports_dic. 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 OTBObject first output. Default is True Returns: the exported numpy array @@ -367,7 +369,7 @@ class OTBObject: 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 OTBObject first output. Default is True. + the OTBObject 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 @@ -1178,24 +1180,22 @@ class LogicalOperation(Operation): class Input(OTBObject): """Class for transforming a filepath to pyOTB object.""" - def __init__(self, filepath): + def __init__(self, path): """Default constructor. Args: - filepath: the path of an input image + path: Anything supported by GDAL (local file on the filesystem, remote resource e.g. /vsicurl/.., etc.) """ - self.filepath = Path(filepath) - if not self.filepath.exists(): - raise FileNotFoundError(filepath) - super().__init__("ExtractROI", {"in": str(filepath)}, frozen=True) - self.name = f"Input from {filepath}" + self.path = path + super().__init__("ExtractROI", {"in": path}, frozen=True) + self.name = f"Input from {path}" self.propagate_dtype() self.execute() def __str__(self): """Return a nice string representation with file path.""" - return f"<pyotb.Input object from {self.filepath}>" + return f"<pyotb.Input object from {self.path}>" class Output: -- GitLab From 68ef3d21b3752ee3bf472b92d8adab2cb5cac80b Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 11 Jan 2023 15:34:45 +0100 Subject: [PATCH 3/7] FIX: remove file missing test --- tests/test_core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 2d99bd2..f053b2f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -17,7 +17,7 @@ def test_parameters(): # Catch errors before exec def test_missing_input_file(): - with pytest.raises(FileNotFoundError): + with pytest.raises(RuntimeError): pyotb.Input("missing_file.tif") @@ -129,7 +129,6 @@ def test_write(): # BandMath NDVI == RadiometricIndices NDVI ? -@pytest.mark.xfail(reason="Regression in OTB 8.2, waiting for Rémi's patch to be merged.") def test_ndvi_comparison(): ndvi_bandmath = (INPUT[:, :, -1] - INPUT[:, :, [0]]) / (INPUT[:, :, -1] + INPUT[:, :, 0]) ndvi_indices = pyotb.RadiometricIndices(INPUT, {"list": "Vegetation:NDVI", "channels.red": 1, "channels.nir": 4}) -- GitLab From 45e32e849579391020da1e9ef05d496f4a216071 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 11 Jan 2023 15:35:14 +0100 Subject: [PATCH 4/7] ENH: test_serialization can work with any input image path --- tests/test_serialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 2e0ce7f..e64f2f1 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -12,7 +12,7 @@ def test_pipeline_simple(): summary = app3.summarize() reference = {'name': 'ManageNoData', 'parameters': {'in': { 'name': 'OrthoRectification', 'parameters': {'io.in': { - 'name': 'BandMath', 'parameters': {'il': ('tests/image.tif',), 'exp': 'im1b1'}}, + 'name': 'BandMath', 'parameters': {'il': (filepath,), 'exp': 'im1b1'}}, 'map': 'utm', 'outputs.isotropic': True}}, 'mode': 'buildmask'}} @@ -28,12 +28,12 @@ def test_pipeline_diamond(): summary = app4.summarize() reference = {'name': 'BandMathX', 'parameters': {'il': [ {'name': 'OrthoRectification', 'parameters': {'io.in': { - 'name': 'BandMath', 'parameters': {'il': ('tests/image.tif',), 'exp': 'im1b1'}}, + 'name': 'BandMath', 'parameters': {'il': (filepath,), 'exp': 'im1b1'}}, 'map': 'utm', 'outputs.isotropic': True}}, {'name': 'ManageNoData', 'parameters': {'in': { 'name': 'OrthoRectification', 'parameters': { - 'io.in': {'name': 'BandMath', 'parameters': {'il': ('tests/image.tif',), 'exp': 'im1b1'}}, + 'io.in': {'name': 'BandMath', 'parameters': {'il': (filepath,), 'exp': 'im1b1'}}, 'map': 'utm', 'outputs.isotropic': True}}, 'mode': 'buildmask'}} -- GitLab From 93eb844b398c46142a4d75756c699d79916eb8c6 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 11 Jan 2023 15:47:31 +0100 Subject: [PATCH 5/7] FIX: remove file missing test --- tests/test_core.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index f053b2f..2eadef7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,12 +15,6 @@ def test_parameters(): assert (INPUT.parameters["sizex"], INPUT.parameters["sizey"]) == (251, 304) -# Catch errors before exec -def test_missing_input_file(): - with pytest.raises(RuntimeError): - pyotb.Input("missing_file.tif") - - def test_wrong_key(): with pytest.raises(KeyError): pyotb.BandMath(INPUT, expression="im1b1") -- GitLab From 210b3f70fb9732d0b22b0d222c5b2b26031c005e Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 11 Jan 2023 17:20:06 +0100 Subject: [PATCH 6/7] REFAC: simplify input/output keys accessors --- pyotb/apps.py | 8 ++-- pyotb/core.py | 105 ++++++++++++++++++++++---------------------------- 2 files changed, 50 insertions(+), 63 deletions(-) diff --git a/pyotb/apps.py b/pyotb/apps.py index b3f9fd7..5e30d0e 100644 --- a/pyotb/apps.py +++ b/pyotb/apps.py @@ -68,9 +68,9 @@ class App(OTBObject): self.description = self.app.GetDocLongDescription() @property - def outputs(self): - """List of application outputs.""" - return [getattr(self, key) for key in self.out_param_keys if key in self.parameters] + def used_outputs(self): + """List of used application outputs.""" + return [getattr(self, key) for key in self.out_param_types if key in self.parameters] def find_outputs(self): """Find output files on disk using path found in parameters. @@ -80,7 +80,7 @@ class App(OTBObject): """ files, missing = [], [] - for out in self.outputs: + for out in self.used_outputs: dest = files if out.exists() else missing dest.append(str(out.filepath.absolute())) for filename in missing: diff --git a/pyotb/core.py b/pyotb/core.py index 532c8f3..bbd681d 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -42,29 +42,45 @@ class OTBObject: create = otb.Registry.CreateApplicationWithoutLogger if quiet else otb.Registry.CreateApplication self.app = create(name) self.parameters_keys = tuple(self.app.GetParametersKeys()) - self.out_param_types = dict(get_out_param_types(self)) - self.out_param_keys = tuple(self.out_param_types.keys()) + # Output parameters types + self.all_param_types = {k: self.app.GetParameterType(k) for k in self.parameters_keys} + self.out_param_types = {k: v for k, v in self.all_param_types.items() + if v in (otb.ParameterType_OutputImage, + otb.ParameterType_OutputVectorData, + otb.ParameterType_OutputFilename)} if args or kwargs: self.set_parameters(*args, **kwargs) if not self.frozen: self.execute() - if any(key in self.parameters for key in self.out_param_keys): + if any(key in self.parameters for key in self.parameters_keys): self.flush() # auto flush if any output param was provided during app init + def get_first_key(self, param_types): + """Get the first output param key for specific file types.""" + for key, param_type in sorted(self.all_param_types.items()): + if param_type in param_types: + return key + return None + @property def key_input(self): """Get the name of first input parameter, raster > vector > file.""" - return key_input(self, "raster") or key_input(self, "vector") or key_input(self, "file") + return self.get_first_key(param_types=[otb.ParameterType_InputImage, + otb.ParameterType_InputImageList]) \ + or self.get_first_key(param_types=[otb.ParameterType_InputVectorData, + otb.ParameterType_InputVectorDataList]) \ + or self.get_first_key(param_types=[otb.ParameterType_InputFilename, + otb.ParameterType_InputFilenameList]) @property def key_input_image(self): """Get the name of first input image parameter.""" - return key_input(self, "raster") + return self.get_first_key(param_types=[otb.ParameterType_InputImage, otb.ParameterType_InputImageList]) @property def key_output_image(self): """Get the name of first output image parameter.""" - return key_output(self, "raster") + return self.get_first_key(param_types=[otb.ParameterType_OutputImage]) @property def dtype(self): @@ -165,7 +181,7 @@ class OTBObject: except RuntimeError: continue # this is when there is no value for key # Convert output param path to Output object - if key in self.out_param_keys: + if key in self.out_param_types: value = Output(self, key, value) # Save attribute setattr(self, key, value) @@ -224,7 +240,7 @@ class OTBObject: if not filename_extension.startswith("?"): filename_extension = "?" + filename_extension for key, value in kwargs.items(): - if self.out_param_types[key] == 'raster' and '?' not in value: + if self.out_param_types[key] == otb.ParameterType_OutputImage and '?' not in value: kwargs[key] = value + filename_extension # Manage output pixel types @@ -234,7 +250,7 @@ class OTBObject: type_name = self.app.ConvertPixelTypeToNumpy(parse_pixel_type(pixel_type)) logger.debug('%s: output(s) will be written with type "%s"', self.name, type_name) for key in kwargs: - if self.out_param_types.get(key) == "raster": + if self.out_param_types[key] == otb.ParameterType_OutputImage: dtypes[key] = parse_pixel_type(pixel_type) elif isinstance(pixel_type, dict): dtypes = {k: parse_pixel_type(v) for k, v in pixel_type.items()} @@ -274,7 +290,7 @@ class OTBObject: if target_key: keys = [target_key] else: - keys = [k for k in self.out_param_keys if self.out_param_types[k] == "raster"] + keys = [k for k, v in self.out_param_types.items() if v == otb.ParameterType_OutputImage] for key in keys: self.app.SetParameterOutputImagePixelType(key, dtype) @@ -303,7 +319,7 @@ class OTBObject: raise TypeError(f"{self.name}: type '{type(bands)}' cannot be interpreted as a valid slicing") if channels: app.app.Execute() - app.set_parameters({"cl": [f"Channel{n+1}" for n in channels]}) + app.set_parameters({"cl": [f"Channel{n + 1}" for n in channels]}) app.execute() data = literal_eval(app.app.GetParameterString("value")) if len(channels) == 1: @@ -413,7 +429,7 @@ class OTBObject: """ spacing_x, _, origin_x, _, spacing_y, origin_y = self.transform row, col = (origin_y - y) / spacing_y, (x - origin_x) / spacing_x - return (abs(int(row)), int(col)) + return abs(int(row)), int(col) # Private functions def __parse_args(self, args): @@ -1122,6 +1138,7 @@ class LogicalOperation(Operation): logical expression (e.g. "im1b1 > 0") """ + def __init__(self, operator, *inputs, nb_bands=None): """Constructor for a LogicalOperation object. @@ -1273,16 +1290,25 @@ def get_pixel_type(inp): try: info = OTBObject("ReadImageInfo", inp, quiet=True) except Exception as info_err: # this happens when we pass a str that is not a filepath - raise TypeError(f'Could not get the pixel type of `{inp}`. Not a filepath or wrong filepath') from info_err + raise TypeError(f"Could not get the pixel type of `{inp}`. Not a filepath or wrong filepath") from info_err datatype = info.GetParameterString("datatype") # which is such as short, float... if not datatype: - raise Exception(f'Unable to read pixel type of image {inp}') - datatype_to_pixeltype = {'unsigned_char': 'uint8', 'short': 'int16', 'unsigned_short': 'uint16', - 'int': 'int32', 'unsigned_int': 'uint32', 'long': 'int32', 'ulong': 'uint32', - 'float': 'float', 'double': 'double'} - pixel_type = datatype_to_pixeltype[datatype] - pixel_type = getattr(otb, f'ImagePixelType_{pixel_type}') - elif isinstance(inp, (OTBObject)): + raise TypeError(f"Unable to read pixel type of image {inp}") + datatype_to_pixeltype = { + 'unsigned_char': 'uint8', + 'short': 'int16', + 'unsigned_short': 'uint16', + 'int': 'int32', + 'unsigned_int': 'uint32', + 'long': 'int32', + 'ulong': 'uint32', + 'float': 'float', + 'double': 'double' + } + if datatype not in datatype_to_pixeltype: + raise TypeError(f"Unknown data type `{datatype}`. Available ones: {datatype_to_pixeltype}") + pixel_type = getattr(otb, f'ImagePixelType_{datatype_to_pixeltype[datatype]}') + elif isinstance(inp, OTBObject): pixel_type = inp.GetParameterOutputImagePixelType(inp.key_output_image) else: raise TypeError(f'Could not get the pixel type of {type(inp)} object {inp}') @@ -1324,45 +1350,6 @@ def is_key_images_list(pyotb_app, key): ) -def get_out_param_types(pyotb_app): - """Get output parameter data type (raster, vector, file).""" - outfile_types = { - otb.ParameterType_OutputImage: "raster", - otb.ParameterType_OutputVectorData: "vector", - otb.ParameterType_OutputFilename: "file", - } - for k in pyotb_app.parameters_keys: - t = pyotb_app.app.GetParameterType(k) - if t in outfile_types: - yield k, outfile_types[t] - - def get_out_images_param_keys(app): """Return every output parameter keys of an OTB app.""" return [key for key in app.GetParametersKeys() if app.GetParameterType(key) == otb.ParameterType_OutputImage] - - -def key_input(pyotb_app, file_type): - """Get the first input param key for a specific file type.""" - types = { - "raster": (otb.ParameterType_InputImage, otb.ParameterType_InputImageList), - "vector": (otb.ParameterType_InputVectorData, otb.ParameterType_InputVectorDataList), - "file": (otb.ParameterType_InputFilename, otb.ParameterType_InputFilenameList) - } - for key in pyotb_app.parameters_keys: - if pyotb_app.app.GetParameterType(key) in types[file_type]: - return key - return None - - -def key_output(pyotb_app, file_type): - """Get the first output param key for a specific file type.""" - types = { - "raster": otb.ParameterType_OutputImage, - "vector": otb.ParameterType_OutputVectorData, - "file": otb.ParameterType_OutputFilename - } - for key in pyotb_app.parameters_keys: - if pyotb_app.app.GetParameterType(key) == types[file_type]: - return key - return None -- GitLab From 4f8d363cb84e2e26ab72102c61723c6bb786066f Mon Sep 17 00:00:00 2001 From: Vincent Delbar <vincent.delbar@latelescop.fr> Date: Wed, 11 Jan 2023 17:05:44 +0000 Subject: [PATCH 7/7] Apply 1 suggestion(s) to 1 file(s) --- pyotb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyotb/core.py b/pyotb/core.py index bbd681d..86bfb9c 100644 --- a/pyotb/core.py +++ b/pyotb/core.py @@ -52,7 +52,7 @@ class OTBObject: self.set_parameters(*args, **kwargs) if not self.frozen: self.execute() - if any(key in self.parameters for key in self.parameters_keys): + if any(key in self.parameters for key in self.out_param_types): self.flush() # auto flush if any output param was provided during app init def get_first_key(self, param_types): -- GitLab