From 8185e10aab866eaab0569071fcdced48bde9a053 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Wed, 30 Oct 2024 16:29:33 -0400 Subject: [PATCH] ENH: Python dispatch on the first RequiredInputName Dispatch Python filter types based on the type of the first ProcessObject RequiredInputName, which is exposed in Python via GetRequiredInputName and which can be the ProcessObject PrimaryInputName. This helps to address uses cases such as `itk.elastix_registration_method`, where you still want to infer the filter type based on the input fixed image type, but it may be passed in as a keyword argument, such as `itk.elastix_registration_method(moving_image=moving_image, fixed_image=fixed_image)`. Re: #4858 https://github.com/InsightSoftwareConsortium/ITKElastix/issues/212 --- .../Python/Tests/PythonTemplateTest.py | 11 +++++++++++ Wrapping/Generators/Python/Tests/extras.py | 12 ++++++++++++ Wrapping/Generators/Python/itk/support/extras.py | 16 +++------------- .../Generators/Python/itk/support/helpers.py | 12 ++++++++++++ .../Python/itk/support/template_class.py | 15 +++++++++++++++ 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/Wrapping/Generators/Python/Tests/PythonTemplateTest.py b/Wrapping/Generators/Python/Tests/PythonTemplateTest.py index e429b5c9744..b32ab72f0d6 100644 --- a/Wrapping/Generators/Python/Tests/PythonTemplateTest.py +++ b/Wrapping/Generators/Python/Tests/PythonTemplateTest.py @@ -119,6 +119,17 @@ median_kwarg = itk.MedianImageFilter.New(Input=reader.GetOutput()) assert itk.class_(median) == itk.class_(median_kwarg) +# filter type determined by the input passed as a primary input name input +median_primary_kwarg = itk.MedianImageFilter.New(Primary=reader.GetOutput()) +assert itk.class_(median) == itk.class_(median_primary_kwarg) + +# First RequiredInputName: "FixedImage" +fixed_image = reader.GetOutput() +moving_image = fixed_image +pde_registration = itk.PDEDeformableRegistrationFilter.New( + FixedImage=fixed_image, MovingImage=moving_image +) + # to a filter with a SetImage method calculator = itk.MinimumMaximumImageCalculator[ImageType].New(reader) # not GetImage() method here to verify it's the right image diff --git a/Wrapping/Generators/Python/Tests/extras.py b/Wrapping/Generators/Python/Tests/extras.py index aa03f539194..96eed9c743f 100644 --- a/Wrapping/Generators/Python/Tests/extras.py +++ b/Wrapping/Generators/Python/Tests/extras.py @@ -163,6 +163,18 @@ def custom_callback(name, progress): image = itk.imread(filename, imageio=itk.PNGImageIO.New()) assert type(image) == itk.Image[itk.RGBPixel[itk.UC], 2] +# Python functional interface that determines the filter type +# based on the primary input (dispatch) +image = itk.imread(filename, itk.UC) +# positional argument +filtered_positional = itk.median_image_filter(image) +# required primary named input argument +filtered_kwarg = itk.median_image_filter(primary=image) +comparison = itk.comparison_image_filter( + filtered_positional, filtered_kwarg, verify_input_information=True +) +assert np.sum(comparison) == 0.0 + # imread using a dicom series image = itk.imread(sys.argv[8]) image0 = itk.imread(sys.argv[8], series_uid=0) diff --git a/Wrapping/Generators/Python/itk/support/extras.py b/Wrapping/Generators/Python/itk/support/extras.py index 5116b086655..c4c90272777 100644 --- a/Wrapping/Generators/Python/itk/support/extras.py +++ b/Wrapping/Generators/Python/itk/support/extras.py @@ -39,6 +39,7 @@ from .helpers import wasm_type_from_image_type, image_type_from_wasm_type from .helpers import wasm_type_from_mesh_type, mesh_type_from_wasm_type, python_to_js from .helpers import wasm_type_from_pointset_type, pointset_type_from_wasm_type +from .helpers import snake_to_camel_case from .xarray import xarray_from_image, image_from_xarray @@ -1552,17 +1553,6 @@ def search(s: str, case_sensitive: bool = False) -> List[str]: # , fuzzy=True): return res -def _snake_to_camel(keyword: str): - # Helpers for set_inputs snake case to CamelCase keyword argument conversion - _snake_underscore_re = re.compile("(_)([a-z0-9A-Z])") - - def _underscore_upper(match_obj): - return match_obj.group(2).upper() - - camel = keyword[0].upper() - if _snake_underscore_re.search(keyword[1:]): - return camel + _snake_underscore_re.sub(_underscore_upper, keyword[1:]) - return camel + keyword[1:] def set_inputs( @@ -1656,7 +1646,7 @@ def SetInputs(self, *args, **kargs): # (Ex: itk.ImageFileReader.UC2.New(SetFileName='image.png')) if attribName not in ["auto_progress", "template_parameters"]: if attribName.islower(): - attribName = _snake_to_camel(attribName) + attribName = snake_to_camel_case(attribName) attrib = getattr(new_itk_object, "Set" + attribName) # Do not use try-except mechanism as this leads to @@ -2162,7 +2152,7 @@ def ipython_kw_matches(text: str): namespace = split_name_parts[:-1] function_name = split_name_parts[-1] # Find corresponding object name - object_name = _snake_to_camel(function_name) + object_name = snake_to_camel_case(function_name) # Check that this object actually exists try: object_callable_match = ".".join(namespace + [object_name]) diff --git a/Wrapping/Generators/Python/itk/support/helpers.py b/Wrapping/Generators/Python/itk/support/helpers.py index 5ff75eb8270..32a16fda540 100644 --- a/Wrapping/Generators/Python/itk/support/helpers.py +++ b/Wrapping/Generators/Python/itk/support/helpers.py @@ -40,6 +40,18 @@ pass +def snake_to_camel_case(keyword: str): + # Helpers for set_inputs snake case to CamelCase keyword argument conversion + _snake_underscore_re = re.compile("(_)([a-z0-9A-Z])") + + def _underscore_upper(match_obj): + return match_obj.group(2).upper() + + camel = keyword[0].upper() + if _snake_underscore_re.search(keyword[1:]): + return camel + _snake_underscore_re.sub(_underscore_upper, keyword[1:]) + return camel + keyword[1:] + def camel_to_snake_case(name): snake = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) snake = re.sub("([a-z0-9])([A-Z])", r"\1_\2", snake) diff --git a/Wrapping/Generators/Python/itk/support/template_class.py b/Wrapping/Generators/Python/itk/support/template_class.py index 9601df7976c..3a14333a527 100644 --- a/Wrapping/Generators/Python/itk/support/template_class.py +++ b/Wrapping/Generators/Python/itk/support/template_class.py @@ -30,6 +30,7 @@ from itk.support import base from itk.support.extras import output from itk.support.types import itkCType +from itk.support.helpers import snake_to_camel_case import math from collections.abc import Mapping @@ -718,6 +719,20 @@ def ttype_for_input_type(keys_l, input_type_l): # try to find a type suitable for the input provided input_type = output(cur).__class__ keys = ttype_for_input_type(keys, input_type) + else: + inst = self.values()[0].New() + if hasattr(inst, "GetRequiredInputNames"): + required_input_names = inst.GetRequiredInputNames() + if len(required_input_names) > 0: + primary_input_name = required_input_names[0] + kwargs_camel = {snake_to_camel_case(k): v for k,v in kwargs.items()} + if primary_input_name in kwargs_camel.keys(): + input_type = output(kwargs_camel[primary_input_name]).__class__ + keys = ttype_for_input_type(keys, input_type) + if not hasattr(inst, f"Set{primary_input_name}"): + arg_0 = kwargs_camel.pop(primary_input_name) + kwargs = kwargs_camel + args = (arg_0,) + args if len(keys) == 0: if not input_type: