From 99d17c1de36b2724296c5b463f2ff29939f14135 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 6 Feb 2024 16:07:14 +0000 Subject: [PATCH 01/10] Start simple test - write some blobs to file --- pyproject.toml | 4 ++++ src/tests/test_registration.py | 38 ++++++++++++++++++++++++++++++++++ tests/test_dummy.py | 6 ------ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 src/tests/test_registration.py delete mode 100644 tests/test_dummy.py diff --git a/pyproject.toml b/pyproject.toml index 1553f5d..ddad5a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,9 @@ urls.homepage = "https://github.com/HiPCTProject/hipct-reg" test = [ "pytest", "pytest-cov", + "numpy~=1.26", + "scikit-image~=0.22", + "tifffile==2024.1.30", ] dev = [ "hipct-reg[test]", @@ -58,6 +61,7 @@ paths.source = [ [tool.mypy] explicit_package_bases = true +strict = true [tool.pytest.ini_options] addopts = """ diff --git a/src/tests/test_registration.py b/src/tests/test_registration.py new file mode 100644 index 0000000..b355f2f --- /dev/null +++ b/src/tests/test_registration.py @@ -0,0 +1,38 @@ +"""An example set of tests.""" + +from pathlib import Path +from typing import Any +import tifffile + +import numpy as np +import pytest +import numpy.typing as npt +from skimage.data import binary_blobs + + +@pytest.fixture() +def rng() -> np.random.Generator: + return np.random.default_rng(seed=1) + + +def write_array_to_stack(array: npt.NDArray[Any], folder: Path) -> None: + """ + Write a 3D array to a stack of images. + """ + assert array.ndim == 3, "Input array must be 3D" + for i, plane in enumerate(array): + tifffile.imwrite(folder / f'{i}.tif', plane, photometric='minisblack') + + +def test_simple_registration(tmp_path: Path, rng: np.random.Generator) -> None: + """ + Test a really simple registration where the common point given is exactly the + correct point, and there is no rotation between the two datasets. + """ + high_res_folder = tmp_path / "high_res" + high_res_folder.mkdir() + high_res_array = binary_blobs( + length=512, n_dim=3, blob_size_fraction=0.01, volume_fraction=0.5, rng=rng + ) + write_array_to_stack(high_res_array, high_res_folder) + assert len(list(high_res_folder.glob("*tif"))) == 512 diff --git a/tests/test_dummy.py b/tests/test_dummy.py deleted file mode 100644 index 0c1c580..0000000 --- a/tests/test_dummy.py +++ /dev/null @@ -1,6 +0,0 @@ -"""An example set of tests.""" - - -def test_stupid_example() -> None: - """Test is merely a placeholder.""" - assert True From 3ea3b233bed2d9a03df4530d655ebb95c770fdc3 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 6 Feb 2024 18:23:42 +0000 Subject: [PATCH 02/10] Add a simple (but failing) test --- pyproject.toml | 11 ++++++--- src/tests/test_registration.py | 44 +++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ddad5a4..771cd28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,11 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ + "dask-image~=2023.08", + "itk~=5.3", + "numpy~=1.26", + "scikit-image~=0.22", + "simpleitk~=2.3", ] description = "Code to register regions of interest with full organ datasets." dynamic = [ @@ -35,8 +40,6 @@ urls.homepage = "https://github.com/HiPCTProject/hipct-reg" test = [ "pytest", "pytest-cov", - "numpy~=1.26", - "scikit-image~=0.22", "tifffile==2024.1.30", ] dev = [ @@ -62,6 +65,8 @@ paths.source = [ [tool.mypy] explicit_package_bases = true strict = true +ignore_missing_imports = true +disallow_untyped_calls = false [tool.pytest.ini_options] addopts = """ @@ -72,7 +77,7 @@ addopts = """ --cov-report=xml """ testpaths = [ - "tests", + "src/hipct_reg/tests", ] [tool.ruff] diff --git a/src/tests/test_registration.py b/src/tests/test_registration.py index b355f2f..9359a5a 100644 --- a/src/tests/test_registration.py +++ b/src/tests/test_registration.py @@ -2,16 +2,22 @@ from pathlib import Path from typing import Any -import tifffile import numpy as np -import pytest import numpy.typing as npt +import pytest +import tifffile from skimage.data import binary_blobs +from skimage.measure import block_reduce + +from hipct_reg.ITK_registration import registration_pipeline @pytest.fixture() def rng() -> np.random.Generator: + """ + Setup the default random number generator. + """ return np.random.default_rng(seed=1) @@ -21,7 +27,7 @@ def write_array_to_stack(array: npt.NDArray[Any], folder: Path) -> None: """ assert array.ndim == 3, "Input array must be 3D" for i, plane in enumerate(array): - tifffile.imwrite(folder / f'{i}.tif', plane, photometric='minisblack') + tifffile.imwrite(folder / f"{i}.tif", plane, photometric="minisblack") def test_simple_registration(tmp_path: Path, rng: np.random.Generator) -> None: @@ -29,10 +35,32 @@ def test_simple_registration(tmp_path: Path, rng: np.random.Generator) -> None: Test a really simple registration where the common point given is exactly the correct point, and there is no rotation between the two datasets. """ - high_res_folder = tmp_path / "high_res" - high_res_folder.mkdir() - high_res_array = binary_blobs( + ground_truth_data = binary_blobs( length=512, n_dim=3, blob_size_fraction=0.01, volume_fraction=0.5, rng=rng + ).astype(np.float32) + + # Downsample to mimic a full organ scan + bin_factor = 4 + full_organ_scan = block_reduce( + ground_truth_data, (bin_factor, bin_factor, bin_factor), np.mean + ) + full_organ_folder = tmp_path / "20.0um_full_organ" + full_organ_folder.mkdir() + write_array_to_stack(full_organ_scan, full_organ_folder) + + # Take a sub-volume to mimic a high resolution region of interest + offset = 128 + size = 64 + roi_scan = ground_truth_data[ + offset : offset + size, offset : offset + size, offset : offset + size + ] + roi_folder = tmp_path / "5.0um_roi" + roi_folder.mkdir() + write_array_to_stack(roi_scan, roi_folder) + + full_organ_point = np.array([offset, offset, offset]) / bin_factor + roi_point = np.array([0, 0, 0]) + + registration_pipeline( + str(full_organ_folder), str(roi_folder), full_organ_point, roi_point ) - write_array_to_stack(high_res_array, high_res_folder) - assert len(list(high_res_folder.glob("*tif"))) == 512 From 92adda86d72f6823beb913917617edba8266d32b Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 12 Feb 2024 17:50:37 +0000 Subject: [PATCH 03/10] Add psutil to reqs --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 771cd28..8f22842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "dask-image~=2023.08", "itk~=5.3", "numpy~=1.26", + "psutil~=5.9", "scikit-image~=0.22", "simpleitk~=2.3", ] From dfe6ef1b45819c46727e64073d96bf91e67bc996 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 12 Feb 2024 17:58:24 +0000 Subject: [PATCH 04/10] Default fiji to False --- src/hipct_reg/ITK_registration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hipct_reg/ITK_registration.py b/src/hipct_reg/ITK_registration.py index 79d5d39..e8d7e15 100644 --- a/src/hipct_reg/ITK_registration.py +++ b/src/hipct_reg/ITK_registration.py @@ -38,7 +38,7 @@ def registration_rot( zrot: float, angle_range: float, angle_step: float, - fiji: bool = True, + fiji: bool = False, ): """ Parameters @@ -309,7 +309,7 @@ def convertSI(fixed_image): def registration_sitk( - fixed_image, moving_image, trans_point, zrot, pt_fixed, fiji=True + fixed_image, moving_image, trans_point, zrot, pt_fixed, fiji=False ): pixel_size_fixed = fixed_image.GetSpacing()[0] pixel_size_moved = moving_image.GetSpacing()[0] @@ -456,7 +456,7 @@ def command_iteration(method, pixel_size, trans_point): def registration_simpleElastix( - fixed_image, moving_image, trans_point, zrot, pt_fixed, fiji=True + fixed_image, moving_image, trans_point, zrot, pt_fixed, fiji=False ): print("\n-----------------") print("\nBeginning of initialization") From 3f20a5e5cda470e0e34f5c2afaf759f17796f6fd Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 12 Feb 2024 18:01:07 +0000 Subject: [PATCH 05/10] Update mypy options --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f22842..8913003 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,9 +65,7 @@ paths.source = [ [tool.mypy] explicit_package_bases = true -strict = true ignore_missing_imports = true -disallow_untyped_calls = false [tool.pytest.ini_options] addopts = """ From 406059aad20ee1981bdf31c50ae6eeb06c943685 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 19 Feb 2024 13:50:51 +0000 Subject: [PATCH 06/10] Force strict typing in tests --- pyproject.toml | 8 +++++++- src/hipct_reg/py.typed | 0 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/hipct_reg/py.typed diff --git a/pyproject.toml b/pyproject.toml index 8913003..f7ce493 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,13 @@ paths.source = [ [tool.mypy] explicit_package_bases = true -ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = [ + "hipct_reg.tests" +] +strict = true +warn_return_any = false [tool.pytest.ini_options] addopts = """ diff --git a/src/hipct_reg/py.typed b/src/hipct_reg/py.typed new file mode 100644 index 0000000..e69de29 From 0e6959ebc95f1320950e7afbd82a0b01b90286a9 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 19 Feb 2024 14:26:37 +0000 Subject: [PATCH 07/10] Rename pt_fixed argument --- src/hipct_reg/ITK_registration.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/hipct_reg/ITK_registration.py b/src/hipct_reg/ITK_registration.py index e8d7e15..bce52ba 100644 --- a/src/hipct_reg/ITK_registration.py +++ b/src/hipct_reg/ITK_registration.py @@ -34,7 +34,7 @@ def registration_rot( fixed_image: sitk.Image, moving_image: sitk.Image, trans_point: npt.NDArray, - pt_fixed: npt.NDArray, + rotation_center_pix: npt.NDArray, zrot: float, angle_range: float, angle_step: float, @@ -47,8 +47,9 @@ def registration_rot( The images being registered. trans_point : Vector from [0, 0, 0] voxel in fixed image to [0, 0, 0] voxel in moving image. - pt_fixed : - Common point in the fixed image. In units of pixels. + rotation_center : + Point in the fixed image about which the moving image will be rotated. + In units of pixels. zrot : Initial rotation for the registration. In units of degrees. angle_range : @@ -79,8 +80,8 @@ def registration_rot( R.SetOptimizerScalesFromPhysicalShift() offset = pixel_size_fixed * trans_point - - rotation_center = pt_fixed * pixel_size_fixed + # Convert from pixels to physical size + rotation_center = rotation_center_pix * pixel_size_fixed theta_x = 0.0 theta_y = 0.0 @@ -129,7 +130,7 @@ def command_iteration( rotation_center, moving_image, fixed_image, - pt_fixed, + rotation_center_pix, ): global metric metric.append(method.GetMetricValue()) @@ -153,7 +154,7 @@ def command_iteration( rotation_center, moving_image, fixed_image, - pt_fixed, + rotation_center_pix, ), ) From 7b999ca9ff120a9923a9c4379d1dfca25013394f Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 19 Feb 2024 14:35:55 +0000 Subject: [PATCH 08/10] Add working test of registration_rot --- .pre-commit-config.yaml | 2 + src/tests/test_registration.py | 81 ++++++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24bbe31..c825f9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,8 @@ repos: rev: v1.8.0 hooks: - id: mypy + additional_dependencies: + - pytest - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: diff --git a/src/tests/test_registration.py b/src/tests/test_registration.py index 9359a5a..1f88f29 100644 --- a/src/tests/test_registration.py +++ b/src/tests/test_registration.py @@ -6,14 +6,21 @@ import numpy as np import numpy.typing as npt import pytest +import SimpleITK as sitk import tifffile from skimage.data import binary_blobs from skimage.measure import block_reduce -from hipct_reg.ITK_registration import registration_pipeline +from hipct_reg.helpers import import_im +from hipct_reg.ITK_registration import registration_rot +PIXEL_SIZE_UM = 5 +BIN_FACTOR = 4 +ROI_OFFSET = 128 +ROI_SIZE = 64 -@pytest.fixture() + +@pytest.fixture def rng() -> np.random.Generator: """ Setup the default random number generator. @@ -30,37 +37,75 @@ def write_array_to_stack(array: npt.NDArray[Any], folder: Path) -> None: tifffile.imwrite(folder / f"{i}.tif", plane, photometric="minisblack") -def test_simple_registration(tmp_path: Path, rng: np.random.Generator) -> None: +@pytest.fixture +def ground_truth(rng: np.random.Generator) -> npt.NDArray[np.float32]: """ - Test a really simple registration where the common point given is exactly the - correct point, and there is no rotation between the two datasets. + Ground truth, high resolution data. """ - ground_truth_data = binary_blobs( + return binary_blobs( length=512, n_dim=3, blob_size_fraction=0.01, volume_fraction=0.5, rng=rng ).astype(np.float32) - # Downsample to mimic a full organ scan - bin_factor = 4 + +@pytest.fixture +def full_organ_scan( + tmp_path: Path, ground_truth: npt.NDArray[np.float32] +) -> sitk.Image: + """ + Downsampled ground truth data, to mimic a full organ scan. + """ full_organ_scan = block_reduce( - ground_truth_data, (bin_factor, bin_factor, bin_factor), np.mean + ground_truth, (BIN_FACTOR, BIN_FACTOR, BIN_FACTOR), np.mean ) full_organ_folder = tmp_path / "20.0um_full_organ" full_organ_folder.mkdir() write_array_to_stack(full_organ_scan, full_organ_folder) - # Take a sub-volume to mimic a high resolution region of interest - offset = 128 - size = 64 - roi_scan = ground_truth_data[ - offset : offset + size, offset : offset + size, offset : offset + size + return import_im( + str(full_organ_folder), pixel_size=PIXEL_SIZE_UM * BIN_FACTOR * 1000 + ) + + +@pytest.fixture +def roi_scan(tmp_path: Path, ground_truth: npt.NDArray[np.float32]) -> sitk.Image: + """ + Sub-volume of ground truth data, to mimic ROI data. + """ + roi_scan = ground_truth[ + ROI_OFFSET : ROI_OFFSET + ROI_SIZE, + ROI_OFFSET : ROI_OFFSET + ROI_SIZE, + ROI_OFFSET : ROI_OFFSET + ROI_SIZE, ] roi_folder = tmp_path / "5.0um_roi" roi_folder.mkdir() write_array_to_stack(roi_scan, roi_folder) - full_organ_point = np.array([offset, offset, offset]) / bin_factor - roi_point = np.array([0, 0, 0]) + return import_im(str(roi_folder), pixel_size=PIXEL_SIZE_UM * 1000) + + +def test_registration_rot(full_organ_scan: sitk.Image, roi_scan: sitk.Image) -> None: + """ + Test a really simple registration where the common point given is exactly the + correct point, and there is no rotation between the two datasets. + """ + zrot = 0 + + trans_point = np.array([ROI_OFFSET, ROI_OFFSET, ROI_OFFSET]) / BIN_FACTOR + rotation_center = ( + trans_point + np.array([ROI_SIZE, ROI_SIZE, ROI_SIZE]) / 2 / BIN_FACTOR + ) - registration_pipeline( - str(full_organ_folder), str(roi_folder), full_organ_point, roi_point + transform = registration_rot( + full_organ_scan, + roi_scan, + trans_point=trans_point, + rotation_center_pix=rotation_center, + zrot=zrot, + angle_range=360, + angle_step=2, ) + + assert isinstance(transform, sitk.Euler3DTransform) + assert transform.GetAngleX() == 0 + assert transform.GetAngleY() == 0 + assert transform.GetAngleZ() == pytest.approx(-0.0349066) From 32fa2568188ce84b1ea774cb47589504bc6ef4bd Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 19 Feb 2024 14:58:18 +0000 Subject: [PATCH 09/10] Use degrees for test --- src/tests/test_registration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/test_registration.py b/src/tests/test_registration.py index 1f88f29..0d2c217 100644 --- a/src/tests/test_registration.py +++ b/src/tests/test_registration.py @@ -108,4 +108,5 @@ def test_registration_rot(full_organ_scan: sitk.Image, roi_scan: sitk.Image) -> assert isinstance(transform, sitk.Euler3DTransform) assert transform.GetAngleX() == 0 assert transform.GetAngleY() == 0 - assert transform.GetAngleZ() == pytest.approx(-0.0349066) + # This value should be close to zero + assert np.rad2deg(transform.GetAngleZ()) == pytest.approx(-2) From ddc4dd4af72554d60d753b2d9fc6237261c1d7ce Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 19 Feb 2024 15:00:38 +0000 Subject: [PATCH 10/10] pre-commit fixes --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f7ce493..44f28cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ paths.source = [ [tool.mypy] explicit_package_bases = true +disallow_untyped_calls = false [[tool.mypy.overrides]] module = [