Skip to content

Commit 8185e10

Browse files
thewtexhjmjohnson
authored andcommitted
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 InsightSoftwareConsortium/ITKElastix#212
1 parent 73841d0 commit 8185e10

5 files changed

Lines changed: 53 additions & 13 deletions

File tree

Wrapping/Generators/Python/Tests/PythonTemplateTest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@
119119
median_kwarg = itk.MedianImageFilter.New(Input=reader.GetOutput())
120120
assert itk.class_(median) == itk.class_(median_kwarg)
121121

122+
# filter type determined by the input passed as a primary input name input
123+
median_primary_kwarg = itk.MedianImageFilter.New(Primary=reader.GetOutput())
124+
assert itk.class_(median) == itk.class_(median_primary_kwarg)
125+
126+
# First RequiredInputName: "FixedImage"
127+
fixed_image = reader.GetOutput()
128+
moving_image = fixed_image
129+
pde_registration = itk.PDEDeformableRegistrationFilter.New(
130+
FixedImage=fixed_image, MovingImage=moving_image
131+
)
132+
122133
# to a filter with a SetImage method
123134
calculator = itk.MinimumMaximumImageCalculator[ImageType].New(reader)
124135
# not GetImage() method here to verify it's the right image

Wrapping/Generators/Python/Tests/extras.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,18 @@ def custom_callback(name, progress):
163163
image = itk.imread(filename, imageio=itk.PNGImageIO.New())
164164
assert type(image) == itk.Image[itk.RGBPixel[itk.UC], 2]
165165

166+
# Python functional interface that determines the filter type
167+
# based on the primary input (dispatch)
168+
image = itk.imread(filename, itk.UC)
169+
# positional argument
170+
filtered_positional = itk.median_image_filter(image)
171+
# required primary named input argument
172+
filtered_kwarg = itk.median_image_filter(primary=image)
173+
comparison = itk.comparison_image_filter(
174+
filtered_positional, filtered_kwarg, verify_input_information=True
175+
)
176+
assert np.sum(comparison) == 0.0
177+
166178
# imread using a dicom series
167179
image = itk.imread(sys.argv[8])
168180
image0 = itk.imread(sys.argv[8], series_uid=0)

Wrapping/Generators/Python/itk/support/extras.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from .helpers import wasm_type_from_image_type, image_type_from_wasm_type
4040
from .helpers import wasm_type_from_mesh_type, mesh_type_from_wasm_type, python_to_js
4141
from .helpers import wasm_type_from_pointset_type, pointset_type_from_wasm_type
42+
from .helpers import snake_to_camel_case
4243

4344
from .xarray import xarray_from_image, image_from_xarray
4445

@@ -1552,17 +1553,6 @@ def search(s: str, case_sensitive: bool = False) -> List[str]: # , fuzzy=True):
15521553
return res
15531554

15541555

1555-
def _snake_to_camel(keyword: str):
1556-
# Helpers for set_inputs snake case to CamelCase keyword argument conversion
1557-
_snake_underscore_re = re.compile("(_)([a-z0-9A-Z])")
1558-
1559-
def _underscore_upper(match_obj):
1560-
return match_obj.group(2).upper()
1561-
1562-
camel = keyword[0].upper()
1563-
if _snake_underscore_re.search(keyword[1:]):
1564-
return camel + _snake_underscore_re.sub(_underscore_upper, keyword[1:])
1565-
return camel + keyword[1:]
15661556

15671557

15681558
def set_inputs(
@@ -1656,7 +1646,7 @@ def SetInputs(self, *args, **kargs):
16561646
# (Ex: itk.ImageFileReader.UC2.New(SetFileName='image.png'))
16571647
if attribName not in ["auto_progress", "template_parameters"]:
16581648
if attribName.islower():
1659-
attribName = _snake_to_camel(attribName)
1649+
attribName = snake_to_camel_case(attribName)
16601650
attrib = getattr(new_itk_object, "Set" + attribName)
16611651

16621652
# Do not use try-except mechanism as this leads to
@@ -2162,7 +2152,7 @@ def ipython_kw_matches(text: str):
21622152
namespace = split_name_parts[:-1]
21632153
function_name = split_name_parts[-1]
21642154
# Find corresponding object name
2165-
object_name = _snake_to_camel(function_name)
2155+
object_name = snake_to_camel_case(function_name)
21662156
# Check that this object actually exists
21672157
try:
21682158
object_callable_match = ".".join(namespace + [object_name])

Wrapping/Generators/Python/itk/support/helpers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@
4040
pass
4141

4242

43+
def snake_to_camel_case(keyword: str):
44+
# Helpers for set_inputs snake case to CamelCase keyword argument conversion
45+
_snake_underscore_re = re.compile("(_)([a-z0-9A-Z])")
46+
47+
def _underscore_upper(match_obj):
48+
return match_obj.group(2).upper()
49+
50+
camel = keyword[0].upper()
51+
if _snake_underscore_re.search(keyword[1:]):
52+
return camel + _snake_underscore_re.sub(_underscore_upper, keyword[1:])
53+
return camel + keyword[1:]
54+
4355
def camel_to_snake_case(name):
4456
snake = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
4557
snake = re.sub("([a-z0-9])([A-Z])", r"\1_\2", snake)

Wrapping/Generators/Python/itk/support/template_class.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from itk.support import base
3131
from itk.support.extras import output
3232
from itk.support.types import itkCType
33+
from itk.support.helpers import snake_to_camel_case
3334
import math
3435
from collections.abc import Mapping
3536

@@ -718,6 +719,20 @@ def ttype_for_input_type(keys_l, input_type_l):
718719
# try to find a type suitable for the input provided
719720
input_type = output(cur).__class__
720721
keys = ttype_for_input_type(keys, input_type)
722+
else:
723+
inst = self.values()[0].New()
724+
if hasattr(inst, "GetRequiredInputNames"):
725+
required_input_names = inst.GetRequiredInputNames()
726+
if len(required_input_names) > 0:
727+
primary_input_name = required_input_names[0]
728+
kwargs_camel = {snake_to_camel_case(k): v for k,v in kwargs.items()}
729+
if primary_input_name in kwargs_camel.keys():
730+
input_type = output(kwargs_camel[primary_input_name]).__class__
731+
keys = ttype_for_input_type(keys, input_type)
732+
if not hasattr(inst, f"Set{primary_input_name}"):
733+
arg_0 = kwargs_camel.pop(primary_input_name)
734+
kwargs = kwargs_camel
735+
args = (arg_0,) + args
721736

722737
if len(keys) == 0:
723738
if not input_type:

0 commit comments

Comments
 (0)