diff --git a/.github/workflows/build-test-publish.yml b/.github/workflows/build-test-publish.yml index 4c4b2b9..d6a3fbe 100644 --- a/.github/workflows/build-test-publish.yml +++ b/.github/workflows/build-test-publish.yml @@ -13,12 +13,14 @@ jobs: python-version: "3.11" - name: "Build pure Python wheels" run: | - python -m pip wheel -w dist -e src/ + python -m pip install hatch + cd src + hatch build - uses: actions/upload-artifact@v4 with: name: wheel - path: ./dist/itk_dreg-*.whl + path: ./src/dist/itk_dreg-*.whl pytest: name: "Test with PyTest" @@ -36,26 +38,21 @@ jobs: - name: "Install Dependencies" shell: bash + working-directory: src run: | - python -m pip install flit - cd src - flit install --deps develop - - - name: Download Python Wheel Artifact - uses: actions/download-artifact@v4 - with: - name: wheel + python -m pip install -e '.[test]' - - name: "Install itk_dreg from Wheel Artifact" + - name: "Install itk-dreg from source" + working-directory: src run: | wheel_name_full=`(find . -name "itk_dreg-*.whl")` - python -m pip uninstall -y itk_dreg - python -m pip install ${wheel_name_full} + python -m pip install -e '.[test]' - name: "Run Tests" shell: bash + working-directory: src run: | - pytest ./test -vvv -s -k "not localcluster and not serialize_pairwise_result" + pytest -vvv -s -k "not localcluster and not serialize_pairwise_result" publish: needs: diff --git a/src/itk_dreg/__init__.py b/src/itk_dreg/__init__.py index 238e73a..2728588 100644 --- a/src/itk_dreg/__init__.py +++ b/src/itk_dreg/__init__.py @@ -1,5 +1,5 @@ """ -ITK-DREG Distributed registration framework. +itk-dreg distributed registration framework. """ -__version__ = "0.0.1" +__version__ = "0.1.0" diff --git a/src/pyproject.toml b/src/pyproject.toml index db2a241..a268f58 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -1,10 +1,13 @@ [build-system] -requires = ["flit_core >=3.2,<4"] -build-backend = "flit_core.buildapi" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] -name = "itk_dreg" -authors = [{name = "InsightSoftwareConsortium", email = "matt.mccormick@kitware.com"}] +name = "itk-dreg" +authors = [ + {name = "Matt McCormick", email = "matt.mccormick@kitware.com"}, + {name = "Tom Birdsong", email = "tom.birdsong@kitware.com"} +] license = {file = "LICENSE"} classifiers = [ "License :: OSI Approved :: Apache Software License", @@ -26,23 +29,25 @@ classifiers = [ "Operating System :: MacOS" ] requires-python = ">=3.9" -dynamic = ["version", "description"] +dynamic = ["version"] readme = "../README.md" -keywords = ['ITK','InsightToolkit'] +keywords = ['itk','InsightToolkit','registration', 'brain','distributed', 'dask'] dependencies = [ 'dask[distributed] >=2023.10.0', - 'itk >=5.4rc02', + 'itk >=5.4rc04', 'numpy' ] [project.urls] -Home = "https://github.com/InsightSoftwareConsortium/itk-dreg" +Home = "https://itk-dreg.readthedocs.io/" +Source = "https://github.com/InsightSoftwareConsortium/itk-dreg" +Issues = "https://github.com/InsightSoftwareConsortium/issues" [project.optional-dependencies] test = [ - 'itk-elastix>=0.19.1', - 'itk-ioomezarrngff>=0.2.1', + 'itk-elastix>=0.19.2', + 'itk-ioomezarrngff>=0.3rc1', 'nbmake', 'pytest', 'scipy>=1.11.3' @@ -60,7 +65,7 @@ doc = [ ] impl = [ - 'itk-elastix>=0.19.0', + 'itk-elastix>=0.19.2', 'scipy>=1.11.3' ] @@ -77,6 +82,25 @@ notebook = [ 'graphviz', ] -[tool.ruff] +[tool.ruff.lint] extend-ignore = ["E501"] +[tool.hatch.version] +path = "itk_dreg/__init__.py" + +[tool.hatch.envs.default] +dependencies = [ + 'itk-elastix>=0.19.2', + 'scipy>=1.11.3', + 'itk-ioomezarrngff>=0.3rc1', + 'nbmake', + 'pytest', +] + +[tool.hatch.envs.default.scripts] +test = [ + "pytest -vvv -s" +] + +[tool.black] +line-length = 88 \ No newline at end of file diff --git a/test/elastix/test_elx_import.py b/src/test/elastix/test_elx_import.py similarity index 66% rename from test/elastix/test_elx_import.py rename to src/test/elastix/test_elx_import.py index 73035d1..5388f08 100644 --- a/test/elastix/test_elx_import.py +++ b/src/test/elastix/test_elx_import.py @@ -12,6 +12,4 @@ def test_loadmodule(): - import itk_dreg.elastix - import itk_dreg.elastix.util - import itk_dreg.elastix.register + pass diff --git a/test/elastix/test_elx_integration.py b/src/test/elastix/test_elx_integration.py similarity index 72% rename from test/elastix/test_elx_integration.py rename to src/test/elastix/test_elx_integration.py index 6e6e2fe..6617598 100644 --- a/test/elastix/test_elx_integration.py +++ b/src/test/elastix/test_elx_integration.py @@ -7,9 +7,7 @@ import itk import numpy as np import dask -import dask.array as da -sys.path.append('./src') import itk_dreg.itk from itk_dreg.register import register_images @@ -30,41 +28,47 @@ https://github.com/InsightSoftwareConsortium/ITKElastix/issues/255 """ + def test_run_dreg(): - dask.config.set(scheduler='single-threaded') + dask.config.set(scheduler="single-threaded") + + fixed_arr = np.ones([100] * 3) + moving_arr = np.random.random_sample([50] * 3).astype(np.float32) - fixed_arr = np.ones([100]*3) - moving_arr = np.random.random_sample([50]*3).astype(np.float32) - register_method = ElastixDRegBlockPairRegistrationMethod() reduce_method = dreg_mock.CountingReduceResultsMethod() - + registration_result = None registration_schedule = None with tempfile.TemporaryDirectory() as testdir: - FIXED_FILEPATH = f'{testdir}/fixed_image.mha' - MOVING_FILEPATH = f'{testdir}/moving_image.mha' + FIXED_FILEPATH = f"{testdir}/fixed_image.mha" + MOVING_FILEPATH = f"{testdir}/moving_image.mha" fixed_image = itk.image_view_from_array(fixed_arr) itk.imwrite(fixed_image, FIXED_FILEPATH, compression=False) - fixed_cb = lambda : itk_dreg.itk.make_reader(FIXED_FILEPATH) + + def fixed_cb(): + return itk_dreg.itk.make_reader(FIXED_FILEPATH) + moving_image = itk.image_view_from_array(moving_arr) - moving_image.SetSpacing([2]*3) + moving_image.SetSpacing([2] * 3) itk.imwrite(moving_image, MOVING_FILEPATH, compression=False) - moving_cb = lambda : itk_dreg.itk.make_reader(MOVING_FILEPATH) + + def moving_cb(): + return itk_dreg.itk.make_reader(MOVING_FILEPATH) parameter_object = itk.ParameterObject.New() parameter_object.AddParameterMap( - parameter_object.GetDefaultParameterMap('rigid') + parameter_object.GetDefaultParameterMap("rigid") ) registration_schedule = register_images( - fixed_chunk_size=(10,20,100), - initial_transform=itk.TranslationTransform[itk.D,3].New(), + fixed_chunk_size=(10, 20, 100), + initial_transform=itk.TranslationTransform[itk.D, 3].New(), moving_reader_ctor=moving_cb, fixed_reader_ctor=fixed_cb, reduce_method=reduce_method, - overlap_factors=[0.1]*3, + overlap_factors=[0.1] * 3, block_registration_method=register_method, elx_parameter_object_serial=parameter_object_to_list(parameter_object), itk_transform_types=[itk.Euler3DTransform[itk.D]], @@ -76,4 +80,3 @@ def test_run_dreg(): assert reduce_method.num_calls == 1 assert registration_result.status.shape == registration_schedule.fixed_da.numblocks - diff --git a/test/elastix/test_elx_serialize.py b/src/test/elastix/test_elx_serialize.py similarity index 78% rename from test/elastix/test_elx_serialize.py rename to src/test/elastix/test_elx_serialize.py index e3de240..2e805ec 100644 --- a/test/elastix/test_elx_serialize.py +++ b/src/test/elastix/test_elx_serialize.py @@ -1,34 +1,42 @@ -from unittest.mock import DEFAULT import itk -itk.auto_progress(2) import pickle -import dask.distributed.protocol import itk_dreg.elastix.serialize +itk.auto_progress(2) + + def validate_parameter_maps(m1, m2): assert all([k in m2.keys() for k in m1.keys()]) assert all([k in m1.keys() for k in m2.keys()]) for key in m1.keys(): assert m1[key] == m2[key] + def validate_parameter_objects(p1, p2): assert p1.GetNumberOfParameterMaps() == p2.GetNumberOfParameterMaps() for map_index in range(p1.GetNumberOfParameterMaps()): - validate_parameter_maps(p1.GetParameterMap(map_index), p2.GetParameterMap(map_index)) + validate_parameter_maps( + p1.GetParameterMap(map_index), p2.GetParameterMap(map_index) + ) + def test_serialize_elx_parameter_object(): - DEFAULT_PARAMETER_MAPS = ['rigid'] + DEFAULT_PARAMETER_MAPS = ["rigid"] parameter_object = itk.ParameterObject.New() parameter_object.AddParameterMap( itk.ParameterObject.GetDefaultParameterMap(DEFAULT_PARAMETER_MAPS[0]) ) - parameter_list = itk_dreg.elastix.serialize.parameter_object_to_list(parameter_object) + parameter_list = itk_dreg.elastix.serialize.parameter_object_to_list( + parameter_object + ) pickled_parameter_list = pickle.dumps(parameter_list) unpickled_parameter_list = pickle.loads(pickled_parameter_list) - unpickled_parameter_object = itk_dreg.elastix.serialize.list_to_parameter_object(unpickled_parameter_list) + unpickled_parameter_object = itk_dreg.elastix.serialize.list_to_parameter_object( + unpickled_parameter_list + ) validate_parameter_objects(unpickled_parameter_object, parameter_object) # TODO: https://github.com/InsightSoftwareConsortium/ITKElastix/issues/257 @@ -43,7 +51,7 @@ def test_serialize_elx_parameter_object(): # Validate baseline is unchanged assert parameter_object.GetNumberOfParameterMaps() == 1 - validate_parameter_maps(parameter_object.GetParameterMap(0), - itk.ParameterObject.GetDefaultParameterMap(DEFAULT_PARAMETER_MAPS[0])) - - + validate_parameter_maps( + parameter_object.GetParameterMap(0), + itk.ParameterObject.GetDefaultParameterMap(DEFAULT_PARAMETER_MAPS[0]), + ) diff --git a/test/itk_dreg/test_block_interface.py b/src/test/itk_dreg/test_block_interface.py similarity index 56% rename from test/itk_dreg/test_block_interface.py rename to src/test/itk_dreg/test_block_interface.py index cf1fa1f..ad7268b 100644 --- a/test/itk_dreg/test_block_interface.py +++ b/src/test/itk_dreg/test_block_interface.py @@ -1,85 +1,97 @@ - import numpy as np import itk -itk.auto_progress(2) import pytest -from itk_dreg.base.image_block_interface import BlockRegStatus, BlockPairRegistrationResult +from itk_dreg.base.image_block_interface import ( + BlockRegStatus, + BlockPairRegistrationResult, +) + +itk.auto_progress(2) + def test_construct_failed_pairwise_result(): # Verify no inputs required for failure result = BlockPairRegistrationResult(status=BlockRegStatus.FAILURE) assert result.status == BlockRegStatus.FAILURE - assert result.transform == None - assert result.transform_domain == None - assert result.inv_transform == None - assert result.inv_transform_domain == None + assert result.transform is None + assert result.transform_domain is None + assert result.inv_transform is None + assert result.inv_transform_domain is None + def test_construct_forward_pairwise_result(): - valid_transform = itk.TranslationTransform[itk.D,3].New() - valid_transform_domain = itk.Image[itk.UC,3].New() - valid_transform_domain.SetRegions([1,1,1]) + valid_transform = itk.TranslationTransform[itk.D, 3].New() + valid_transform_domain = itk.Image[itk.UC, 3].New() + valid_transform_domain.SetRegions([1, 1, 1]) result = BlockPairRegistrationResult( status=BlockRegStatus.SUCCESS, transform=valid_transform, - transform_domain=valid_transform_domain + transform_domain=valid_transform_domain, ) assert result.status == BlockRegStatus.SUCCESS assert result.transform == valid_transform assert result.transform_domain == valid_transform_domain - assert result.inv_transform == None - assert result.inv_transform_domain == None + assert result.inv_transform is None + assert result.inv_transform_domain is None # Validate incomplete construction with pytest.raises(ValueError): BlockPairRegistrationResult( status=BlockRegStatus.SUCCESS, - transform=valid_transform + transform=valid_transform, # transform_domain required ) - - invalid_transform_domain = [1,2,3] + + invalid_transform_domain = [1, 2, 3] with pytest.raises(KeyError): BlockPairRegistrationResult( status=BlockRegStatus.SUCCESS, transform=valid_transform, - transform_domain=invalid_transform_domain + transform_domain=invalid_transform_domain, ) - invalid_transform_domain = itk.Image[itk.UC,3].New() - assert all([size == 0 for size in invalid_transform_domain.GetLargestPossibleRegion().GetSize()]) + invalid_transform_domain = itk.Image[itk.UC, 3].New() + assert all( + [ + size == 0 + for size in invalid_transform_domain.GetLargestPossibleRegion().GetSize() + ] + ) with pytest.raises(ValueError): BlockPairRegistrationResult( status=BlockRegStatus.SUCCESS, transform=valid_transform, - transform_domain=invalid_transform_domain + transform_domain=invalid_transform_domain, ) - + def test_construct_inverse_pairwise_result(): - valid_transform = itk.TranslationTransform[itk.D,3].New() - valid_transform_domain = itk.Image[itk.UC,3].New() - valid_transform_domain.SetRegions([1,1,1]) - valid_inverse_transform = itk.TranslationTransform[itk.D,3].New() - valid_inverse_transform_domain = itk.Image[itk.UC,3].New() - valid_inverse_transform_domain.SetRegions([1,1,1]) - valid_inverse_transform_domain.SetOrigin([-1]*3) - valid_inverse_transform_domain.SetSpacing([0.1]*3) - valid_inverse_transform_domain.SetDirection(np.array([[0,-1,0],[-1,0,0],[0,0,1]])) + valid_transform = itk.TranslationTransform[itk.D, 3].New() + valid_transform_domain = itk.Image[itk.UC, 3].New() + valid_transform_domain.SetRegions([1, 1, 1]) + valid_inverse_transform = itk.TranslationTransform[itk.D, 3].New() + valid_inverse_transform_domain = itk.Image[itk.UC, 3].New() + valid_inverse_transform_domain.SetRegions([1, 1, 1]) + valid_inverse_transform_domain.SetOrigin([-1] * 3) + valid_inverse_transform_domain.SetSpacing([0.1] * 3) + valid_inverse_transform_domain.SetDirection( + np.array([[0, -1, 0], [-1, 0, 0], [0, 0, 1]]) + ) result = BlockPairRegistrationResult( status=BlockRegStatus.SUCCESS, transform=valid_transform, transform_domain=valid_transform_domain, inv_transform=valid_inverse_transform, - inv_transform_domain=valid_inverse_transform_domain + inv_transform_domain=valid_inverse_transform_domain, ) assert result.status == BlockRegStatus.SUCCESS assert result.transform == valid_transform assert result.transform_domain == valid_transform_domain assert result.inv_transform == valid_inverse_transform assert result.inv_transform_domain == valid_inverse_transform_domain - + # validate incomplete construction with pytest.raises(ValueError): BlockPairRegistrationResult( @@ -89,27 +101,29 @@ def test_construct_inverse_pairwise_result(): inv_transform=valid_transform, # inv_transform_domain required ) - - invalid_transform_domain = [1,2,3] + + invalid_transform_domain = [1, 2, 3] with pytest.raises(KeyError): BlockPairRegistrationResult( status=BlockRegStatus.SUCCESS, transform=valid_transform, transform_domain=valid_transform_domain, inv_transform=valid_transform, - inv_transform_domain=invalid_transform_domain + inv_transform_domain=invalid_transform_domain, ) - invalid_transform_domain = itk.Image[itk.UC,3].New() - assert all([size == 0 for size in invalid_transform_domain.GetLargestPossibleRegion().GetSize()]) + invalid_transform_domain = itk.Image[itk.UC, 3].New() + assert all( + [ + size == 0 + for size in invalid_transform_domain.GetLargestPossibleRegion().GetSize() + ] + ) with pytest.raises(ValueError): BlockPairRegistrationResult( status=BlockRegStatus.SUCCESS, transform=valid_transform, transform_domain=valid_transform_domain, inv_transform=valid_transform, - inv_transform_domain=invalid_transform_domain + inv_transform_domain=invalid_transform_domain, ) - - - diff --git a/test/itk_dreg/test_dreg_block_subdivide.py b/src/test/itk_dreg/test_dreg_block_subdivide.py similarity index 63% rename from test/itk_dreg/test_dreg_block_subdivide.py rename to src/test/itk_dreg/test_dreg_block_subdivide.py index a36b4b2..0c2a57c 100644 --- a/test/itk_dreg/test_dreg_block_subdivide.py +++ b/src/test/itk_dreg/test_dreg_block_subdivide.py @@ -20,33 +20,39 @@ before any sampling/resampling is performed. """ + def test_rescale_physical_region_norescale(): SCALE_FACTORS = [1] * 3 IMAGE_SIZE = [10] * 3 - input_image = itk.Image[itk.F,3].New() + input_image = itk.Image[itk.F, 3].New() input_image.SetRegions(IMAGE_SIZE) physical_region = itk_dreg.block.convert.image_to_physical_region( - image_region=input_image.GetLargestPossibleRegion(), - ref_image=input_image - ) + image_region=input_image.GetLargestPossibleRegion(), ref_image=input_image + ) output_image = itk_dreg.block.image.physical_region_to_itk_image( physical_region=physical_region, - spacing = [spacing * scale for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS)], + spacing=[ + spacing * scale + for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS) + ], direction=np.array(input_image.GetDirection()), - extend_beyond=False + extend_beyond=False, ) assert itk.size(output_image) == itk.size(input_image) assert itk.origin(output_image) == itk.origin(input_image) assert itk.spacing(output_image) == itk.spacing(input_image) - assert np.all(itk_dreg.block.image.get_sample_bounds(output_image) == - itk_dreg.block.image.get_sample_bounds(input_image)) - + assert np.all( + itk_dreg.block.image.get_sample_bounds(output_image) + == itk_dreg.block.image.get_sample_bounds(input_image) + ) + + def test_rescale_physical_region_downscale(): SCALE_FACTORS = [2] * 3 IMAGE_SIZE = [10] * 3 - input_image = itk.Image[itk.F,3].New() + input_image = itk.Image[itk.F, 3].New() input_image.SetRegions(IMAGE_SIZE) EXPECTED_SIZE = [5] * 3 @@ -54,25 +60,28 @@ def test_rescale_physical_region_downscale(): EXPECTED_ORIGIN = [0.5] * 3 physical_region = itk_dreg.block.convert.image_to_physical_region( - image_region=input_image.GetLargestPossibleRegion(), - ref_image=input_image - ) + image_region=input_image.GetLargestPossibleRegion(), ref_image=input_image + ) output_image = itk_dreg.block.image.physical_region_to_itk_image( physical_region=physical_region, - spacing = [spacing * scale for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS)], + spacing=[ + spacing * scale + for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS) + ], direction=np.array(input_image.GetDirection()), - extend_beyond=False + extend_beyond=False, ) assert itk.size(output_image) == EXPECTED_SIZE assert itk.spacing(output_image) == EXPECTED_SPACING assert itk.origin(output_image) == EXPECTED_ORIGIN + def test_rescale_physical_region_offset(): SCALE_FACTORS = [2] * 3 IMAGE_SIZE = [10] * 3 IMAGE_INDEX = [10] * 3 - input_image = itk.Image[itk.F,3].New() + input_image = itk.Image[itk.F, 3].New() image_region = itk.ImageRegion[3]() image_region.SetIndex(IMAGE_INDEX) @@ -85,14 +94,16 @@ def test_rescale_physical_region_offset(): EXPECTED_SIZE = [5] * 3 physical_region = itk_dreg.block.convert.image_to_physical_region( - image_region=input_image.GetLargestPossibleRegion(), - ref_image=input_image - ) + image_region=input_image.GetLargestPossibleRegion(), ref_image=input_image + ) output_image = itk_dreg.block.image.physical_region_to_itk_image( physical_region=physical_region, - spacing = [spacing * scale for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS)], + spacing=[ + spacing * scale + for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS) + ], direction=np.array(input_image.GetDirection()), - extend_beyond=False + extend_beyond=False, ) assert itk.size(output_image) == EXPECTED_SIZE @@ -103,11 +114,11 @@ def test_rescale_physical_region_offset(): def test_rescale_physical_region_requested(): SCALE_FACTORS = [2] * 3 - IMAGE_SIZE=[100]*3 - input_image = itk.Image[itk.F,3].New() + IMAGE_SIZE = [100] * 3 + input_image = itk.Image[itk.F, 3].New() input_image.SetRegions(IMAGE_SIZE) - REQUESTED_REGION = itk.ImageRegion[3]([1]*3,[10]*3) + REQUESTED_REGION = itk.ImageRegion[3]([1] * 3, [10] * 3) assert input_image.GetLargestPossibleRegion().IsInside(REQUESTED_REGION) EXPECTED_ORIGIN = [1.5] * 3 @@ -115,42 +126,46 @@ def test_rescale_physical_region_requested(): EXPECTED_SIZE = [5] * 3 physical_region = itk_dreg.block.convert.image_to_physical_region( - image_region=REQUESTED_REGION, - ref_image=input_image - ) + image_region=REQUESTED_REGION, ref_image=input_image + ) output_image = itk_dreg.block.image.physical_region_to_itk_image( physical_region=physical_region, - spacing = [spacing * scale for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS)], + spacing=[ + spacing * scale + for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS) + ], direction=np.array(input_image.GetDirection()), - extend_beyond=False + extend_beyond=False, ) assert itk.size(output_image) == EXPECTED_SIZE assert itk.spacing(output_image) == EXPECTED_SPACING assert itk.origin(output_image) == EXPECTED_ORIGIN - assert np.all(itk_dreg.block.image.get_sample_bounds(output_image) == - itk_dreg.block.convert.image_to_physical_region( - REQUESTED_REGION, - ref_image=input_image - )) - + assert np.all( + itk_dreg.block.image.get_sample_bounds(output_image) + == itk_dreg.block.convert.image_to_physical_region( + REQUESTED_REGION, ref_image=input_image + ) + ) + def test_rescale_physical_region_with_direction(): SCALE_FACTORS = [2] * 3 - IMAGE_SIZE=[100]*3 - IMAGE_DIRECTION = np.array([[0,0,-1],[1,0,0],[0,-1,0]]) - input_image = itk.Image[itk.F,3].New() + IMAGE_SIZE = [100] * 3 + IMAGE_DIRECTION = np.array([[0, 0, -1], [1, 0, 0], [0, -1, 0]]) + input_image = itk.Image[itk.F, 3].New() input_image.SetRegions(IMAGE_SIZE) input_image.SetDirection(IMAGE_DIRECTION) - assert input_image.TransformIndexToPhysicalPoint([25]*3) == [-25,25,-25] + assert input_image.TransformIndexToPhysicalPoint([25] * 3) == [-25, 25, -25] - REQUESTED_REGION = itk.ImageRegion[3]([25]*3,[15]*3) + REQUESTED_REGION = itk.ImageRegion[3]([25] * 3, [15] * 3) assert input_image.GetLargestPossibleRegion().IsInside(REQUESTED_REGION) - assert np.all(itk_dreg.block.convert.image_to_physical_region(REQUESTED_REGION, input_image) ==\ - np.array([[-39.5, 24.5, -39.5], - [-24.5, 39.5, -24.5]])) + assert np.all( + itk_dreg.block.convert.image_to_physical_region(REQUESTED_REGION, input_image) + == np.array([[-39.5, 24.5, -39.5], [-24.5, 39.5, -24.5]]) + ) # The input physical region cannot be evenly subdivided into a voxel grid with the given spacing, # so allow the grid to extend beyond the input physical region by up to 1 voxel width. @@ -159,20 +174,19 @@ def test_rescale_physical_region_with_direction(): EXPECTED_ORIGIN = [-25, 25, -25] EXPECTED_SPACING = SCALE_FACTORS EXPECTED_SIZE = [8] * 3 - EXPECTED_PHYSICAL_REGION = np.array([ - [-40, 24, -40], - [-24, 40, -24] - ]) + EXPECTED_PHYSICAL_REGION = np.array([[-40, 24, -40], [-24, 40, -24]]) physical_region = itk_dreg.block.convert.image_to_physical_region( - image_region=REQUESTED_REGION, - ref_image=input_image - ) + image_region=REQUESTED_REGION, ref_image=input_image + ) output_image = itk_dreg.block.image.physical_region_to_itk_image( physical_region=physical_region, - spacing = [spacing * scale for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS)], + spacing=[ + spacing * scale + for spacing, scale in zip(itk.spacing(input_image), SCALE_FACTORS) + ], direction=IMAGE_DIRECTION, - extend_beyond=EXTEND_BEYOND + extend_beyond=EXTEND_BEYOND, ) assert itk.size(output_image) == EXPECTED_SIZE @@ -180,5 +194,6 @@ def test_rescale_physical_region_with_direction(): assert itk.origin(output_image) == EXPECTED_ORIGIN assert output_image.GetDirection() == input_image.GetDirection() - assert np.all(itk_dreg.block.image.get_sample_bounds(output_image) == - EXPECTED_PHYSICAL_REGION) + assert np.all( + itk_dreg.block.image.get_sample_bounds(output_image) == EXPECTED_PHYSICAL_REGION + ) diff --git a/src/test/itk_dreg/test_import.py b/src/test/itk_dreg/test_import.py new file mode 100644 index 0000000..a629825 --- /dev/null +++ b/src/test/itk_dreg/test_import.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +# Purpose: Simple pytest to validate that `itk-dreg` modules can be loaded. + +import sys + +sys.path.append("src") + +import itk + +itk.auto_progress(2) + + +def test_loadmodule(): + pass diff --git a/test/itk_dreg/test_scheduler.py b/src/test/itk_dreg/test_scheduler.py similarity index 58% rename from test/itk_dreg/test_scheduler.py rename to src/test/itk_dreg/test_scheduler.py index 381dbc4..55ee14b 100644 --- a/test/itk_dreg/test_scheduler.py +++ b/src/test/itk_dreg/test_scheduler.py @@ -8,7 +8,6 @@ import itk import numpy as np import dask -import dask.array as da import pytest from urllib.request import urlretrieve @@ -31,62 +30,79 @@ # worker_logger.setLevel(logging.DEBUG) PIXEL_TYPE = itk.F -DIMENSION = 3 # 2D is planned but not yet supported (2023.10.20) +DIMENSION = 3 # 2D is planned but not yet supported (2023.10.20) + @pytest.fixture def test_input_dir() -> str: - TEST_INPUT_DIR = 'test/data/input' - os.makedirs(TEST_INPUT_DIR,exist_ok=True) + TEST_INPUT_DIR = "test/data/input" + os.makedirs(TEST_INPUT_DIR, exist_ok=True) yield TEST_INPUT_DIR + @pytest.fixture def test_output_dir() -> str: - TEST_OUTPUT_DIR = 'test/data/output/itk_dreg' + TEST_OUTPUT_DIR = "test/data/output/itk_dreg" os.makedirs(TEST_OUTPUT_DIR, exist_ok=True) return TEST_OUTPUT_DIR + @pytest.fixture def fixed_image_filepath(test_input_dir): # A small 3D sample MRI image depicting a patient's head - FIXED_IMAGE_FILEPATH = os.path.abspath(f'{test_input_dir}/HeadMRVolume_d.mha') - COMPRESSED_FIXED_IMAGE_URL = 'https://data.kitware.com/api/v1/item/65328e0a5be10c8fb6ed4f01/download' + FIXED_IMAGE_FILEPATH = os.path.abspath(f"{test_input_dir}/HeadMRVolume_d.mha") + COMPRESSED_FIXED_IMAGE_URL = ( + "https://data.kitware.com/api/v1/item/65328e0a5be10c8fb6ed4f01/download" + ) if not os.path.exists(FIXED_IMAGE_FILEPATH): with tempfile.TemporaryDirectory(dir=test_input_dir) as tmpdir: - urlretrieve(COMPRESSED_FIXED_IMAGE_URL, f'{tmpdir}/HeadMRVolume.mha') - image = itk.imread(f'{tmpdir}/HeadMRVolume.mha') + urlretrieve(COMPRESSED_FIXED_IMAGE_URL, f"{tmpdir}/HeadMRVolume.mha") + image = itk.imread(f"{tmpdir}/HeadMRVolume.mha") itk.imwrite(image, FIXED_IMAGE_FILEPATH, compression=False) yield FIXED_IMAGE_FILEPATH + @pytest.fixture def moving_image_filepath(test_input_dir) -> str: # The fixed image, but with an arbitrary translation and rotation applied - MOVING_IMAGE_FILEPATH = os.path.abspath(f'{test_input_dir}/HeadMRVolume_rigid_d.mha') - COMPRESSED_MOVING_IMAGE_URL = 'https://data.kitware.com/api/v1/item/65328e0f5be10c8fb6ed4f04/download' + MOVING_IMAGE_FILEPATH = os.path.abspath( + f"{test_input_dir}/HeadMRVolume_rigid_d.mha" + ) + COMPRESSED_MOVING_IMAGE_URL = ( + "https://data.kitware.com/api/v1/item/65328e0f5be10c8fb6ed4f04/download" + ) if not os.path.exists(MOVING_IMAGE_FILEPATH): with tempfile.TemporaryDirectory(dir=test_input_dir) as tmpdir: - urlretrieve(COMPRESSED_MOVING_IMAGE_URL, f'{tmpdir}/HeadMRVolume_rigid.mha') - image = itk.imread(f'{tmpdir}/HeadMRVolume_rigid.mha') + urlretrieve(COMPRESSED_MOVING_IMAGE_URL, f"{tmpdir}/HeadMRVolume_rigid.mha") + image = itk.imread(f"{tmpdir}/HeadMRVolume_rigid.mha") itk.imwrite(image, MOVING_IMAGE_FILEPATH, compression=False) yield MOVING_IMAGE_FILEPATH - -def test_run_singlethreaded(fixed_image_filepath, moving_image_filepath, test_output_dir): - dask.config.set(scheduler='single-threaded') + +def test_run_singlethreaded( + fixed_image_filepath, moving_image_filepath, test_output_dir +): + dask.config.set(scheduler="single-threaded") logging.root.setLevel(logging.INFO) # Methods import functools - fixed_reader_ctor = functools.partial(itk_dreg.itk.make_reader, filepath=fixed_image_filepath) - moving_reader_ctor = functools.partial(itk_dreg.itk.make_reader, filepath=moving_image_filepath) + + fixed_reader_ctor = functools.partial( + itk_dreg.itk.make_reader, filepath=fixed_image_filepath + ) + moving_reader_ctor = functools.partial( + itk_dreg.itk.make_reader, filepath=moving_image_filepath + ) register_method = dreg_mock.CountingBlockPairRegistrationMethod() - logger.warning(f'reg def {register_method.default_result}') + logger.warning(f"reg def {register_method.default_result}") reduce_method = dreg_mock.CountingReduceResultsMethod() - logger.warning(f'reduce def {reduce_method.default_result}') + logger.warning(f"reduce def {reduce_method.default_result}") # Data - fixed_chunk_size=(10,100,100) #TODO investigate failure case (15,15,25) - initial_transform = itk.TranslationTransform[itk.D,3].New() - overlap_factors=[0.1]*3 + fixed_chunk_size = (10, 100, 100) # TODO investigate failure case (15,15,25) + initial_transform = itk.TranslationTransform[itk.D, 3].New() + overlap_factors = [0.1] * 3 registration_graph = itk_dreg.register.register_images( fixed_chunk_size=fixed_chunk_size, @@ -95,36 +111,44 @@ def test_run_singlethreaded(fixed_image_filepath, moving_image_filepath, test_ou fixed_reader_ctor=fixed_reader_ctor, block_registration_method=register_method, reduce_method=reduce_method, - overlap_factors=overlap_factors + overlap_factors=overlap_factors, ) itk.auto_progress(0) registration_result = registration_graph.registration_result.compute() print(registration_result) - assert register_method.num_calls == np.product(registration_graph.fixed_da.numblocks) + assert register_method.num_calls == np.product( + registration_graph.fixed_da.numblocks + ) assert reduce_method.num_calls == 1 assert registration_result.status.shape == registration_graph.fixed_da.numblocks def test_localcluster(fixed_image_filepath, moving_image_filepath): import dask.distributed + cluster = dask.distributed.LocalCluster(n_workers=1, threads_per_worker=1) - client = dask.distributed.Client(cluster) + client = dask.distributed.Client(cluster) # noqa: F841 # Methods import functools - fixed_reader_ctor = functools.partial(itk_dreg.itk.make_reader, filepath=fixed_image_filepath) - moving_reader_ctor = functools.partial(itk_dreg.itk.make_reader, filepath=moving_image_filepath) + + fixed_reader_ctor = functools.partial( + itk_dreg.itk.make_reader, filepath=fixed_image_filepath + ) + moving_reader_ctor = functools.partial( + itk_dreg.itk.make_reader, filepath=moving_image_filepath + ) register_method = dreg_mock.ConstantBlockPairRegistrationMethod() reduce_method = dreg_mock.ConstantReduceResultsMethod() # Data - fixed_chunk_size=(10,100,100) #TODO investigate failure case (15,15,25) - initial_transform = itk.TranslationTransform[itk.D,3].New() - overlap_factors=[0.1]*3 + fixed_chunk_size = (10, 100, 100) # TODO investigate failure case (15,15,25) + initial_transform = itk.TranslationTransform[itk.D, 3].New() + overlap_factors = [0.1] * 3 - #logging.root.setLevel(logging.DEBUG) + # logging.root.setLevel(logging.DEBUG) registration_graph = itk_dreg.register.register_images( fixed_chunk_size=fixed_chunk_size, initial_transform=initial_transform, @@ -132,13 +156,12 @@ def test_localcluster(fixed_image_filepath, moving_image_filepath): fixed_reader_ctor=fixed_reader_ctor, block_registration_method=register_method, reduce_method=reduce_method, - overlap_factors=overlap_factors + overlap_factors=overlap_factors, ) - + itk.auto_progress(0) registration_result = registration_graph.registration_result.compute() print(registration_result) assert registration_result.status.shape == registration_graph.fixed_da.numblocks assert np.all(registration_result.status == BlockRegStatus.SUCCESS) - diff --git a/test/itk_dreg/test_serialize.py b/src/test/itk_dreg/test_serialize.py similarity index 88% rename from test/itk_dreg/test_serialize.py rename to src/test/itk_dreg/test_serialize.py index f56daff..e876b61 100644 --- a/test/itk_dreg/test_serialize.py +++ b/src/test/itk_dreg/test_serialize.py @@ -1,12 +1,13 @@ - import itk -itk.auto_progress(2) import pickle import dask.distributed.protocol import itk_dreg.base.image_block_interface +itk.auto_progress(2) + + def test_serialize_pairwise_result(): failure_result = itk_dreg.base.image_block_interface.BlockPairRegistrationResult( status=itk_dreg.base.image_block_interface.BlockRegStatus.FAILURE @@ -18,12 +19,12 @@ def test_serialize_pairwise_result(): ) assert deserialized_result.status == failure_result.status - transform_domain = itk.Image[itk.F,3].New() - transform_domain.SetRegions([10]*3) + transform_domain = itk.Image[itk.F, 3].New() + transform_domain.SetRegions([10] * 3) success_result = itk_dreg.base.image_block_interface.BlockPairRegistrationResult( status=itk_dreg.base.image_block_interface.BlockRegStatus.SUCCESS, - transform=itk.TranslationTransform[itk.D,3].New(), - transform_domain=transform_domain + transform=itk.TranslationTransform[itk.D, 3].New(), + transform_domain=transform_domain, ) # TODO Unbuffered `itk.Image` is not yet pickleable (ITK v5.4rc2) # ValueError: PyMemoryView_FromBuffer(): info->buf must not be NULL diff --git a/test/reduce_dfield/test_integration.py b/src/test/reduce_dfield/test_integration.py similarity index 63% rename from test/reduce_dfield/test_integration.py rename to src/test/reduce_dfield/test_integration.py index dfbca15..94771b1 100644 --- a/test/reduce_dfield/test_integration.py +++ b/src/test/reduce_dfield/test_integration.py @@ -4,13 +4,10 @@ import tempfile import itk -itk.auto_progress(2) import numpy as np import dask -import dask.array as da -sys.path.append('./src') import itk_dreg.itk import itk_dreg.reduce_dfield.dreg from itk_dreg.register import register_images @@ -18,16 +15,19 @@ sys.path.append("./test") from util import mock as dreg_mock +itk.auto_progress(2) + """ Test the `itk_dreg` registration scheduling framework. """ + def test_run_dreg(): - dask.config.set(scheduler='single-threaded') + dask.config.set(scheduler="single-threaded") + + fixed_arr = np.ones([100] * 3) + moving_arr = np.ones([50] * 3) - fixed_arr = np.ones([100]*3) - moving_arr = np.ones([50]*3) - register_method = dreg_mock.CountingBlockPairRegistrationMethod() reduce_method = itk_dreg.reduce_dfield.dreg.ReduceToDisplacementFieldMethod() @@ -35,31 +35,37 @@ def test_run_dreg(): registration_schedule = None with tempfile.TemporaryDirectory() as testdir: - FIXED_FILEPATH = f'{testdir}/fixed_image.mha' - MOVING_FILEPATH = f'{testdir}/moving_image.mha' + FIXED_FILEPATH = f"{testdir}/fixed_image.mha" + MOVING_FILEPATH = f"{testdir}/moving_image.mha" fixed_image = itk.image_view_from_array(fixed_arr) itk.imwrite(fixed_image, FIXED_FILEPATH, compression=False) - fixed_cb = lambda : itk_dreg.itk.make_reader(FIXED_FILEPATH) + + def fixed_cb(): + return itk_dreg.itk.make_reader(FIXED_FILEPATH) + moving_image = itk.image_view_from_array(moving_arr) - moving_image.SetSpacing([2]*3) + moving_image.SetSpacing([2] * 3) itk.imwrite(moving_image, MOVING_FILEPATH, compression=False) - moving_cb = lambda : itk_dreg.itk.make_reader(MOVING_FILEPATH) + + def moving_cb(): + return itk_dreg.itk.make_reader(MOVING_FILEPATH) registration_schedule = register_images( - fixed_chunk_size=(10,20,100), - initial_transform=itk.TranslationTransform[itk.D,3].New(), + fixed_chunk_size=(10, 20, 100), + initial_transform=itk.TranslationTransform[itk.D, 3].New(), moving_reader_ctor=moving_cb, fixed_reader_ctor=fixed_cb, block_registration_method=register_method, reduce_method=reduce_method, - overlap_factors=[0.1]*3, - displacement_grid_scale_factors=[10.0,10.0,10.0] + overlap_factors=[0.1] * 3, + displacement_grid_scale_factors=[10.0, 10.0, 10.0], ) registration_result = registration_schedule.registration_result.compute() print(registration_result) - assert register_method.num_calls == np.product(registration_schedule.fixed_da.numblocks) + assert register_method.num_calls == np.product( + registration_schedule.fixed_da.numblocks + ) assert registration_result.status.shape == registration_schedule.fixed_da.numblocks - diff --git a/src/test/reduce_dfield/test_reduce_dfield.py b/src/test/reduce_dfield/test_reduce_dfield.py new file mode 100644 index 0000000..7a60fc4 --- /dev/null +++ b/src/test/reduce_dfield/test_reduce_dfield.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +# Purpose: Simple pytest to validate that `itk_dreg.reduce_dfield` modules can be loaded. + +import sys + +sys.path.append("src") + +import itk +import numpy as np + +itk.auto_progress(2) + + +def test_import(): + pass + + +def test_collection_to_deformation_field_transform(): + import itk_dreg.reduce_dfield.transform + import itk_dreg.reduce_dfield.transform_collection + + INPUT_SIZE = [10] * 3 + SCALE_FACTORS = [2] * 3 + EXPECTED_OUTPUT_SIZE = [ + size / scale for size, scale in zip(INPUT_SIZE, SCALE_FACTORS) + ] + + TRANSLATION_COMPONENT = 1 + input_transform = itk.TranslationTransform[itk.D, 3].New() + input_transform.Translate([TRANSLATION_COMPONENT] * 3) + reference_image = itk.Image[itk.F, 3].New() + reference_image.SetRegions([10, 10, 10]) + + transforms = itk_dreg.reduce_dfield.transform_collection.TransformCollection( + transform_and_domain_list=[ + itk_dreg.reduce_dfield.transform_collection.TransformEntry( + input_transform, None + ) + ] + ) + + output_transform = ( + itk_dreg.reduce_dfield.transform.collection_to_deformation_field_transform( + transforms, + reference_image=reference_image, + initial_transform=itk.TranslationTransform[itk.D, 3].New(), + scale_factors=SCALE_FACTORS, + ) + ) + + assert all( + [ + output_size == expected_size + for output_size, expected_size in zip( + itk.size(output_transform.GetDisplacementField()), EXPECTED_OUTPUT_SIZE + ) + ] + ), f"Output has size {itk.size(output_transform.GetDisplacementField())}" + assert np.all( + itk.array_view_from_image(output_transform.GetDisplacementField()) + == TRANSLATION_COMPONENT + ) diff --git a/src/test/reduce_dfield/test_transform_collection.py b/src/test/reduce_dfield/test_transform_collection.py new file mode 100644 index 0000000..dde1375 --- /dev/null +++ b/src/test/reduce_dfield/test_transform_collection.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +import itk +import numpy as np +import pytest + +from itk_dreg.reduce_dfield.transform_collection import ( + TransformEntry, + TransformCollection, +) + +itk.auto_progress(2) + + +def test_unbounded_transform(): + demo_transform = itk.TranslationTransform[itk.D, 3].New() + demo_transform.Translate([1, 1, 1]) + + transforms = [TransformEntry(demo_transform, None)] + transform_collection = TransformCollection( + blend_method=TransformCollection.blend_simple_mean, + transform_and_domain_list=transforms, + ) + + assert len(transform_collection.transforms) == 1 + assert len(transform_collection.domains) == 1 + assert transform_collection.domains[0] is None + + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([0, 0, 0])) + == [1, 1, 1] + ) + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([5, 10, 15])) + == [6, 11, 16] + ) + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([-30, -0.1, -0.2])) + == [-29, 0.9, 0.8] + ) + + +def test_bounded_transform(): + demo_transform = itk.TranslationTransform[itk.D, 3].New() + demo_transform.Translate([1, 1, 1]) + demo_domain = itk.Image[itk.F, 3].New() + demo_domain.SetOrigin([1, 1, 1]) + + r = itk.ImageRegion[3]() + r.SetSize([1, 1, 1]) + demo_domain.SetLargestPossibleRegion(r) + # no allocate -- use itk.Image as metadata container + + transforms = [TransformEntry(demo_transform, demo_domain)] + transform_collection = TransformCollection( + blend_method=TransformCollection.blend_simple_mean, + transform_and_domain_list=transforms, + ) + + assert len(transform_collection.transforms) == 1 + assert len(transform_collection.domains) == 1 + assert transform_collection.domains[0] == demo_domain + + with pytest.raises(Exception): + print(transform_collection.transform_point([0, 0, 0])) + + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([1, 1, 1])) + == [2, 2, 2] + ) + + +def test_two_bounded_transforms(): + transforms = [ + TransformEntry( + itk.TranslationTransform[itk.D, 3].New(), itk.Image[itk.F, 3].New() + ), + TransformEntry( + itk.TranslationTransform[itk.D, 3].New(), itk.Image[itk.F, 3].New() + ), + ] + transforms[0].transform.Translate([1, 1, 1]) + transforms[1].transform.Translate([2, 2, 2]) + transforms[0].domain.SetOrigin([0, 0, 0]) + transforms[0].domain.SetRegions([2, 2, 2]) + transforms[1].domain.SetOrigin([1, 1, 1]) + transforms[1].domain.SetRegions([2, 2, 2]) + + transform_collection = TransformCollection( + blend_method=TransformCollection.blend_simple_mean, + transform_and_domain_list=transforms, + ) + assert len(transform_collection.transforms) == 2 + assert len(transform_collection.domains) == 2 + + # Transform over non-overlapping domain region + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([0, 0, 0])) + == [1, 1, 1] + ) + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([2.4, 2.4, 2.4])) + == [4.4, 4.4, 4.4] + ) + + # Transform over overlapping domain region + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([1, 1, 1])) + == [2.5, 2.5, 2.5] + ) + + # Transform over excluded domain region fails + with pytest.raises(Exception): + print(transform_collection.transform_point([-1, -1, -1])) + + +def test_distance_weighted_blending(): + transforms = [ + TransformEntry( + itk.TranslationTransform[itk.D, 3].New(), itk.Image[itk.F, 3].New() + ), + TransformEntry( + itk.TranslationTransform[itk.D, 3].New(), itk.Image[itk.F, 3].New() + ), + ] + transforms[0].transform.Translate([1, 1, 1]) + transforms[1].transform.Translate([2, 2, 2]) + transforms[0].domain.SetOrigin([0, 0, 0]) + transforms[0].domain.SetRegions([4, 4, 4]) + transforms[1].domain.SetOrigin([1, 1, 1]) + transforms[1].domain.SetRegions([4, 4, 4]) + + transform_collection = TransformCollection( + blend_method=TransformCollection.blend_distance_weighted_mean, + transform_and_domain_list=transforms, + ) + assert len(transform_collection.transforms) == 2 + assert len(transform_collection.domains) == 2 + + # Transform over non-overlapping domain region + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([0, 0, 0])) + == [1, 1, 1] + ) + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([4.4, 4.4, 4.4])) + == [6.4, 6.4, 6.4] + ) + + # Transform over excluded domain region fails + with pytest.raises(Exception): + print(transform_collection.transform_point([-1, -1, -1])) + + # Transform over overlapping domain region weights by distance to domain edge + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([1, 1, 1])) + == [2.25] * 3 + ) + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([2, 2, 2])) + == [3.5] * 3 + ) + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([3, 3, 3])) + == [4.75] * 3 + ) + assert np.all( + transform_collection.transform_point(itk.Point[itk.F, 3]([1, 2, 3])) + == [2.5, 3.5, 4.5] + ) diff --git a/test/util/__init__.py b/src/test/util/__init__.py similarity index 100% rename from test/util/__init__.py rename to src/test/util/__init__.py diff --git a/test/util/mock.py b/src/test/util/mock.py similarity index 57% rename from test/util/mock.py rename to src/test/util/mock.py index 759bdb5..adf8e5b 100644 --- a/test/util/mock.py +++ b/src/test/util/mock.py @@ -1,53 +1,59 @@ - -from unittest.mock import MagicMock -from typing import Optional, Iterable, Iterator +from typing import Iterable, Iterator import itk -from itk_dreg.base.itk_typing import ImageType, TransformType -from itk_dreg.base.image_block_interface import BlockPairRegistrationResult, RegistrationTransformResult, BlockRegStatus, LocatedBlockResult -from itk_dreg.base.registration_interface import BlockPairRegistrationMethod, ReduceResultsMethod +from itk_dreg.base.image_block_interface import ( + BlockPairRegistrationResult, + RegistrationTransformResult, + BlockRegStatus, + LocatedBlockResult, +) +from itk_dreg.base.registration_interface import ( + BlockPairRegistrationMethod, + ReduceResultsMethod, +) -#TODO use unittest.mock +# TODO use unittest.mock class ConstantBlockPairRegistrationMethod(BlockPairRegistrationMethod): """ Return a constant, default registration result for each block. """ - def __init__(self, default_result:BlockPairRegistrationResult=None): - default_transform_domain = itk.Image[itk.F,3].New() - default_transform_domain.SetRegions([10]*3) + + def __init__(self, default_result: BlockPairRegistrationResult = None): + default_transform_domain = itk.Image[itk.F, 3].New() + default_transform_domain.SetRegions([10] * 3) self.default_result = default_result or BlockPairRegistrationResult( - transform=itk.TranslationTransform[itk.D,3].New(), + transform=itk.TranslationTransform[itk.D, 3].New(), transform_domain=default_transform_domain, inv_transform=None, inv_transform_domain=None, - status=BlockRegStatus.SUCCESS + status=BlockRegStatus.SUCCESS, ) def __call__(self, **kwargs): return self.default_result - + + class ConstantReduceResultsMethod(ReduceResultsMethod): """ Return a constant, default transform result. """ - def __init__(self, default_result:RegistrationTransformResult=None): + + def __init__(self, default_result: RegistrationTransformResult = None): self.default_result = default_result or RegistrationTransformResult( - transform=itk.TranslationTransform[itk.D,3].New(), - inv_transform=None + transform=itk.TranslationTransform[itk.D, 3].New(), inv_transform=None ) + def __call__(self, **kwargs): return self.default_result class CountingBlockPairRegistrationMethod(ConstantBlockPairRegistrationMethod): num_calls = 0 - def __call__( - self, - **kwargs - ) -> BlockPairRegistrationResult: + + def __call__(self, **kwargs) -> BlockPairRegistrationResult: self.num_calls += 1 return super().__call__(**kwargs) @@ -55,23 +61,18 @@ def __call__( class CountingReduceResultsMethod(ConstantReduceResultsMethod): num_calls = 0 - def __call__( - self, - **kwargs - ) -> RegistrationTransformResult: + def __call__(self, **kwargs) -> RegistrationTransformResult: self.num_calls += 1 return super().__call__(**kwargs) - + class IteratorBlockPairRegistrationMethod(BlockPairRegistrationMethod): - def __init__(self, default_results:Iterator[BlockPairRegistrationResult]): + def __init__(self, default_results: Iterator[BlockPairRegistrationResult]): self.default_results = default_results def __call__(self, **kwargs): return next(self.default_results) - - class PassthroughReduceResultsMethod(ReduceResultsMethod): """ @@ -79,15 +80,17 @@ class PassthroughReduceResultsMethod(ReduceResultsMethod): May fail if transform inputs are inherently bounded, such as a displacement field. """ - def __init__(self, return_index:int=0): + + def __init__(self, return_index: int = 0): self.return_index = 0 - def __call__(self, block_results: Iterable[LocatedBlockResult],**kwargs): + + def __call__(self, block_results: Iterable[LocatedBlockResult], **kwargs): counter = 0 results_iter = iter(block_results) while counter < self.return_index: next(results_iter) nth_block_result = next(results_iter) return RegistrationTransformResult( - transform=nth_block_result.result.transform, - inv_transform=nth_block_result.result.inv_transform - ) + transform=nth_block_result.result.transform, + inv_transform=nth_block_result.result.inv_transform, + ) diff --git a/test/itk_dreg/test_import.py b/test/itk_dreg/test_import.py deleted file mode 100644 index 0bdba66..0000000 --- a/test/itk_dreg/test_import.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Purpose: Simple pytest to validate that `itk-dreg` modules can be loaded. - -import sys - -sys.path.append("src") - -import itk - -itk.auto_progress(2) - - -def test_loadmodule(): - import itk_dreg - import itk_dreg.itk - import itk_dreg.base.image_block_interface - import itk_dreg.base.itk_typing - import itk_dreg.base.registration_interface - import itk_dreg.block.convert - import itk_dreg.block.image - import itk_dreg.register diff --git a/test/reduce_dfield/test_reduce_dfield.py b/test/reduce_dfield/test_reduce_dfield.py deleted file mode 100644 index b1ad2d6..0000000 --- a/test/reduce_dfield/test_reduce_dfield.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 - -# Purpose: Simple pytest to validate that `itk_dreg.reduce_dfield` modules can be loaded. - -import sys - -sys.path.append("src") - -import itk -import numpy as np - -itk.auto_progress(2) - - -def test_import(): - import itk_dreg.reduce_dfield - import itk_dreg.reduce_dfield.dreg - import itk_dreg.reduce_dfield.matrix_transform - import itk_dreg.reduce_dfield.transform_collection - import itk_dreg.reduce_dfield.transform - -def test_collection_to_deformation_field_transform(): - import itk_dreg.reduce_dfield.transform - import itk_dreg.reduce_dfield.transform_collection - - INPUT_SIZE = [10] * 3 - SCALE_FACTORS = [2] * 3 - EXPECTED_OUTPUT_SIZE = [size / scale for size, scale in zip(INPUT_SIZE, SCALE_FACTORS)] - - TRANSLATION_COMPONENT = 1 - input_transform = itk.TranslationTransform[itk.D,3].New() - input_transform.Translate([TRANSLATION_COMPONENT] * 3) - reference_image = itk.Image[itk.F,3].New() - reference_image.SetRegions([10,10,10]) - - transforms = itk_dreg.reduce_dfield.transform_collection.TransformCollection( - transform_and_domain_list=[itk_dreg.reduce_dfield.transform_collection.TransformEntry(input_transform, None)] - ) - - output_transform = itk_dreg.reduce_dfield.transform.collection_to_deformation_field_transform(transforms, - reference_image=reference_image, - initial_transform=itk.TranslationTransform[itk.D,3].New(), - scale_factors=SCALE_FACTORS) - - assert all([output_size == expected_size - for output_size, expected_size - in zip(itk.size(output_transform.GetDisplacementField()), EXPECTED_OUTPUT_SIZE)]),\ - f'Output has size {itk.size(output_transform.GetDisplacementField())}' - assert np.all(itk.array_view_from_image(output_transform.GetDisplacementField()) == TRANSLATION_COMPONENT) - - diff --git a/test/reduce_dfield/test_transform_collection.py b/test/reduce_dfield/test_transform_collection.py deleted file mode 100644 index 6da4912..0000000 --- a/test/reduce_dfield/test_transform_collection.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -sys.path.append("src") - -import itk -import numpy as np -import pytest - -itk.auto_progress(2) - -from itk_dreg.reduce_dfield.transform_collection import TransformEntry, TransformCollection - -def test_unbounded_transform(): - demo_transform = itk.TranslationTransform[itk.D,3].New() - demo_transform.Translate([1,1,1]) - - transforms = [TransformEntry(demo_transform, None)] - transform_collection = TransformCollection( - blend_method=TransformCollection.blend_simple_mean, - transform_and_domain_list=transforms - ) - - assert len(transform_collection.transforms) == 1 - assert len(transform_collection.domains) == 1 - assert transform_collection.domains[0] is None - - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([0,0,0])) == [1,1,1]) - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([5,10,15])) == [6,11,16]) - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([-30,-0.1,-0.2])) == [-29,0.9,0.8]) - -def test_bounded_transform(): - demo_transform = itk.TranslationTransform[itk.D,3].New() - demo_transform.Translate([1,1,1]) - demo_domain = itk.Image[itk.F,3].New() - demo_domain.SetOrigin([1,1,1]) - - r = itk.ImageRegion[3]() - r.SetSize([1,1,1]) - demo_domain.SetLargestPossibleRegion(r) - # no allocate -- use itk.Image as metadata container - - transforms = [TransformEntry(demo_transform, demo_domain)] - transform_collection = TransformCollection( - blend_method=TransformCollection.blend_simple_mean, - transform_and_domain_list=transforms - ) - - assert len(transform_collection.transforms) == 1 - assert len(transform_collection.domains) == 1 - assert transform_collection.domains[0] == demo_domain - - with pytest.raises(Exception): - print(transform_collection.transform_point([0,0,0])) - - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([1,1,1])) == [2,2,2]) - -def test_two_bounded_transforms(): - transforms = [TransformEntry(itk.TranslationTransform[itk.D,3].New(), itk.Image[itk.F,3].New()), - TransformEntry(itk.TranslationTransform[itk.D,3].New(), itk.Image[itk.F,3].New())] - transforms[0].transform.Translate([1,1,1]) - transforms[1].transform.Translate([2,2,2]) - transforms[0].domain.SetOrigin([0,0,0]) - transforms[0].domain.SetRegions([2,2,2]) - transforms[1].domain.SetOrigin([1,1,1]) - transforms[1].domain.SetRegions([2,2,2]) - - transform_collection = TransformCollection( - blend_method=TransformCollection.blend_simple_mean, - transform_and_domain_list=transforms) - assert len(transform_collection.transforms) == 2 - assert len(transform_collection.domains) == 2 - - # Transform over non-overlapping domain region - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([0,0,0])) == [1,1,1]) - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([2.4,2.4,2.4])) == [4.4,4.4,4.4]) - - # Transform over overlapping domain region - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([1,1,1])) == [2.5,2.5,2.5]) - - # Transform over excluded domain region fails - with pytest.raises(Exception): - print(transform_collection.transform_point([-1,-1,-1])) - -def test_distance_weighted_blending(): - transforms = [TransformEntry(itk.TranslationTransform[itk.D,3].New(), itk.Image[itk.F,3].New()), - TransformEntry(itk.TranslationTransform[itk.D,3].New(), itk.Image[itk.F,3].New())] - transforms[0].transform.Translate([1,1,1]) - transforms[1].transform.Translate([2,2,2]) - transforms[0].domain.SetOrigin([0,0,0]) - transforms[0].domain.SetRegions([4,4,4]) - transforms[1].domain.SetOrigin([1,1,1]) - transforms[1].domain.SetRegions([4,4,4]) - - transform_collection = TransformCollection( - blend_method=TransformCollection.blend_distance_weighted_mean, - transform_and_domain_list=transforms) - assert len(transform_collection.transforms) == 2 - assert len(transform_collection.domains) == 2 - - # Transform over non-overlapping domain region - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([0,0,0])) == [1,1,1]) - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([4.4,4.4,4.4])) == [6.4,6.4,6.4]) - - # Transform over excluded domain region fails - with pytest.raises(Exception): - print(transform_collection.transform_point([-1,-1,-1])) - - # Transform over overlapping domain region weights by distance to domain edge - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([1,1,1])) == [2.25]*3) - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([2,2,2])) == [3.5]*3) - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([3,3,3])) == [4.75]*3) - assert np.all(transform_collection.transform_point(itk.Point[itk.F,3]([1,2,3])) == [2.5,3.5,4.5])