From 073aadbb36f69b4f1a0db8ef801c63fef57efd72 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Thu, 7 May 2026 08:13:13 +0000 Subject: [PATCH 01/15] fix bug the fill only produces 1 instead of label. Was binary, instead of uint --- TPTBox/core/np_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TPTBox/core/np_utils.py b/TPTBox/core/np_utils.py index d3507dd..c10ab62 100755 --- a/TPTBox/core/np_utils.py +++ b/TPTBox/core/np_utils.py @@ -1050,7 +1050,7 @@ def np_fill_holes( else: assert 0 <= slice_wise_dim <= arr.ndim - 1, f"slice_wise_dim needs to be in range [0, {arr.ndim - 1}]" filled = np.swapaxes(arr_lc.copy(), 0, slice_wise_dim) - filled = np.stack([_fill(x) for x in filled]) + filled = np.stack([_fill(x).astype(arr.dtype) for x in filled]) filled = np.swapaxes(filled, 0, slice_wise_dim) filled[filled != 0] = l if use_crop: From e8a78a0585aea2a2ca0334c825e238631a3c142a Mon Sep 17 00:00:00 2001 From: Robert Graf Date: Thu, 7 May 2026 16:22:55 +0000 Subject: [PATCH 02/15] negative padding, resample_from_to can now padd instead of resampte if the affine allwos (faster) --- TPTBox/core/nii_wrapper.py | 90 ++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/TPTBox/core/nii_wrapper.py b/TPTBox/core/nii_wrapper.py index 9024057..62bddfb 100755 --- a/TPTBox/core/nii_wrapper.py +++ b/TPTBox/core/nii_wrapper.py @@ -799,9 +799,7 @@ def apply_pad( mode: MODES = "constant", inplace=False, verbose: logging = True - ): - #TODO add other modes - #TODO add testcases and options for modes + ): if padd is None or padd == 0: return self if inplace else self.copy() @@ -824,13 +822,36 @@ def apply_pad( affine = self.affine @ transform + arr = self.get_array() + + # ---- 1. CROPPING (negative padding) ---- + slices = [] + + for i, (before, after) in enumerate(padd[:self.dims]): + start = max(0, -before) + end = arr.shape[i] - max(0, -after) + slices.append(slice(start, end)) + + # keep non-spatial dims unchanged + slices += [slice(None)] * (arr.ndim - self.dims) + + arr = arr[tuple(slices)] + + # ---- 2. PADDING (positive only) ---- + padd_positive = tuple( + (max(0, b), max(0, a)) for b, a in padd + ) + args = {} if mode == "constant": args["constant_values"] = self.get_c_val() + if mode == "nearest": + mode = "edge" + log.print(f"Padd {padd}; {mode=}, {args}", verbose=verbose) - arr = np.pad(self.get_array(), padd, mode=mode, **args) + arr = np.pad(arr, padd_positive, mode=mode, **args) nii = (arr, affine, self.header) @@ -935,35 +956,38 @@ def resample_from_to(self, to_vox_map:Image_Reference|Has_Grid|tuple[SHAPE,AFFIN mapping = to_vox_map.to_gird() else: mapping = to_vox_map if isinstance(to_vox_map, tuple) else to_nii_optional(to_vox_map, seg=self.seg, default=to_vox_map) - if isinstance(mapping,Has_Grid) and mapping.assert_affine(self,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0): - log.print(f"resample_from_to skipped; already in space: {self}",verbose=verbose) - return self if inplace else self.copy() - - #m1 = mapping.make_empty_POI().reorient(self.orientation) - #if m1.assert_affine(self,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0): - # log.print(f"resample_from_to only need reorientation; {self.orientation}",verbose=verbose) - # return self.reorient(mapping.orientation,inplace=inplace) - #if self.orientation == mapping.orientation and self.zoom == mapping.zoom: - # shift = (np.array(self.origin) - np.array(m1.origin)) / np.array(m1.zoom) - # if np.allclose(shift, np.round(shift), atol=1e-6): - # self = self.reorient(mapping.orientation,inplace=inplace) # noqa: PLW0642 - # shift = (np.array(self.origin) - np.array(mapping.origin)) / np.array(mapping.zoom) - # shift = np.round(shift).astype(int) - # src_shape = np.array(mapping.shape) - # dst_shape = np.array(self.shape) - # # padding before = how much dst starts before src - # pad_before = np.maximum(-shift, 0) - # - # # where src ends inside dst - # src_end_in_dst = shift + src_shape - # # padding after = remaining dst size after src - # pad_after = np.maximum(dst_shape - src_end_in_dst, 0) - # pad = tuple((int(b), int(a)) for b, a in zip(pad_before, pad_after)) - # ret = self.apply_pad(pad, mode=mode) - # - # log.print(f"resample_from_to only needs padding/cropping {pad}, ",verbose=verbose,) - # ret.assert_affine(mapping,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0) - # return ret + if isinstance(mapping,Has_Grid): + if mapping.assert_affine(self,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0): + log.print(f"resample_from_to skipped; already in space: {self}",verbose=verbose) + return self if inplace else self.copy() + + m1 = mapping if mapping.orientation == self.orientation else mapping.make_empty_POI().reorient(self.orientation) + if m1.assert_affine(self,raise_error=False,origin_tolerance=0.00001,error_tolerance=0.00001,shape_tolerance=0): + log.print(f"resample_from_to only need reorientation; {self.orientation}",verbose=verbose) + ret = self.reorient(mapping.orientation,inplace=inplace) + ret.affine = mapping.affine #remove floating point error + return ret + if self.orientation == mapping.orientation and np.allclose(self.zoom , mapping.zoom, atol=1e-6): + shift = (np.array(self.origin) - np.array(m1.origin)) / np.array(m1.zoom) + if np.allclose(shift, np.round(shift), atol=1e-6): + s = self.reorient(mapping.orientation,inplace=inplace) # noqa: PLW0642 + shift = (np.array(self.origin) - np.array(mapping.origin)) / np.array(mapping.zoom) + shift = np.round(shift).astype(int) + dst_shape = np.array(mapping.shape) + src_shape = np.array(s.shape) + # padding before = how much dst starts before src + pad_before = shift + # padding after = remaining dst size after src + pad_after = dst_shape-shift-src_shape + pad = tuple((int(b), int(a)) for b, a in zip(pad_before, pad_after)) + ret = s.apply_pad(pad, mode=mode,) + + #TODO SET raise_error=False before committing + valid = ret.assert_affine(mapping,raise_error=True,origin_tolerance=0.0001,error_tolerance=0.0001,shape_tolerance=0) + if valid: + log.print(f"resample_from_to only needs padding/cropping {pad}",verbose=verbose) + ret.affine = mapping.affine #remove floating point error + return ret assert mapping is not None From 94a1dee603a875213c8aeed3af185f9ab7a3339b Mon Sep 17 00:00:00 2001 From: Robert Graf Date: Thu, 7 May 2026 16:23:07 +0000 Subject: [PATCH 03/15] bump up numpy version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6ee026e..e454884 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ packages = [{ include = "TPTBox" }] python = "^3.9 || ^3.10 || ^3.11 || ^3.12 || ^3.13 || ^3.14" pathlib = "*" nibabel = "^5.2.0" -numpy = "^1.26.3" +numpy = "^1.26.3 || ^2.0.0" typing-extensions = "^4.9.0" scipy = "^1.12.0" dataclasses = "*" From b3131a1938aaaac3767c4c3cda3904e3aa1f5298 Mon Sep 17 00:00:00 2001 From: Robert Graf Date: Thu, 7 May 2026 16:31:37 +0000 Subject: [PATCH 04/15] bump up scipy --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e454884..0dd862c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,9 +15,9 @@ packages = [{ include = "TPTBox" }] python = "^3.9 || ^3.10 || ^3.11 || ^3.12 || ^3.13 || ^3.14" pathlib = "*" nibabel = "^5.2.0" -numpy = "^1.26.3 || ^2.0.0" +numpy = "^2.0.0" typing-extensions = "^4.9.0" -scipy = "^1.12.0" +scipy = "^1.17.0" dataclasses = "*" SimpleITK = "^2.3.1" matplotlib = "^3.8.2" From 79787762ae14b407a08d419c84809eaeae462d8a Mon Sep 17 00:00:00 2001 From: Robert Graf Date: Thu, 7 May 2026 16:34:32 +0000 Subject: [PATCH 05/15] x --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0dd862c..19ca3fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ pathlib = "*" nibabel = "^5.2.0" numpy = "^2.0.0" typing-extensions = "^4.9.0" -scipy = "^1.17.0" +scipy = "^1.17.1" dataclasses = "*" SimpleITK = "^2.3.1" matplotlib = "^3.8.2" From 77346b5c68cbf054e3ccf77f184f1b5fd349d782 Mon Sep 17 00:00:00 2001 From: Robert Graf Date: Thu, 7 May 2026 16:38:38 +0000 Subject: [PATCH 06/15] change versions --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19ca3fb..a295f69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,12 +17,12 @@ pathlib = "*" nibabel = "^5.2.0" numpy = "^2.0.0" typing-extensions = "^4.9.0" -scipy = "^1.17.1" +scipy = "^1.13.1" dataclasses = "*" SimpleITK = "^2.3.1" matplotlib = "^3.8.2" dill = "^0.3.7" -scikit-image = "^0.22.0" +scikit-image = "^0.26.0" fill-voids = "^2.0.6" connected-components-3d = "^3.12.3" tqdm = "*" From badbca8c6fffd3f36c23da8922518a41be0af042 Mon Sep 17 00:00:00 2001 From: Robert Graf Date: Thu, 7 May 2026 16:50:44 +0000 Subject: [PATCH 07/15] numpy 2.0 support for 3.11 and above --- pyproject.toml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a295f69..93e529a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,14 +15,11 @@ packages = [{ include = "TPTBox" }] python = "^3.9 || ^3.10 || ^3.11 || ^3.12 || ^3.13 || ^3.14" pathlib = "*" nibabel = "^5.2.0" -numpy = "^2.0.0" typing-extensions = "^4.9.0" -scipy = "^1.13.1" dataclasses = "*" SimpleITK = "^2.3.1" matplotlib = "^3.8.2" dill = "^0.3.7" -scikit-image = "^0.26.0" fill-voids = "^2.0.6" connected-components-3d = "^3.12.3" tqdm = "*" @@ -30,9 +27,24 @@ joblib = "*" scikit-learn = "*" antspyx = "0.4.2" pynrrd = "*" -#hf-deepali = "*" requests = "*" +# --- OLD STACK (Python < 3.11) +numpy = [ + { version = ">=1.26.3,<2.0", python = "<3.11" }, + { version = ">=2.0,<3.0", python = ">=3.11" } +] + +scipy = [ + { version = ">=1.11,<1.13", python = "<3.11" }, + { version = ">=1.13", python = ">=3.11" } +] + +scikit-image = [ + { version = ">=0.22,<0.23", python = "<3.11" }, + { version = ">=0.24,<0.27", python = ">=3.11" } +] + [tool.poetry.group.dev.dependencies] pytest = ">=8.1.1" vtk = "*" From 206306db4b4c1ad93f8e94f00b4063b22f367566 Mon Sep 17 00:00:00 2001 From: Robert Graf Date: Thu, 7 May 2026 16:58:47 +0000 Subject: [PATCH 08/15] no upper limit for scikit-image --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 93e529a..e52e1c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ scipy = [ scikit-image = [ { version = ">=0.22,<0.23", python = "<3.11" }, - { version = ">=0.24,<0.27", python = ">=3.11" } + { version = ">=0.24", python = ">=3.11" } ] [tool.poetry.group.dev.dependencies] From dc607be87bc34010f07e24284a6b8466ea39affb Mon Sep 17 00:00:00 2001 From: ga84mun Date: Mon, 11 May 2026 15:30:09 +0000 Subject: [PATCH 09/15] Major seep-up, by prevent copying. --- TPTBox/mesh3D/snapshot3D.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/TPTBox/mesh3D/snapshot3D.py b/TPTBox/mesh3D/snapshot3D.py index 12f985e..f90f46e 100644 --- a/TPTBox/mesh3D/snapshot3D.py +++ b/TPTBox/mesh3D/snapshot3D.py @@ -243,7 +243,7 @@ def _set_input( return vtk_object -def _contour_from_roi_smooth(data, affine=None, color: np.ndarray | list = _red, opacity=1, smoothing=0): +def _contour_from_roi_smooth(data: np.ndarray, affine=None, color: np.ndarray | list = _red, opacity=1, smoothing=0): """Generates surface actor from a binary ROI. Code from dipy, but added awesome smoothing! @@ -274,10 +274,8 @@ def _contour_from_roi_smooth(data, affine=None, color: np.ndarray | list = _red, else: nb_components = 1 - data = (data > 0) * 1 - vol = np.interp(data, xp=[data.min(), data.max()], fp=[0, 255]) - vol = vol.astype("uint8") - + vol = data.astype("uint8") * 255 + assert data.max() <= 1, np.unique(data) im = vtk.vtkImageData() if major_version <= 5: im.SetScalarTypeToUnsignedChar() # type: ignore @@ -291,12 +289,10 @@ def _contour_from_roi_smooth(data, affine=None, color: np.ndarray | list = _red, im.SetNumberOfScalarComponents(nb_components) # type: ignore else: im.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, nb_components) - - # copy data vol = np.swapaxes(vol, 0, 2) - vol = np.ascontiguousarray(vol) + # vol = np.ascontiguousarray(vol) # already is - vol = vol.ravel() if nb_components == 1 else np.reshape(vol, [np.prod(vol.shape[:3]), vol.shape[3]]) + vol = vol.reshape(-1) if nb_components == 1 else np.reshape(vol, [np.prod(vol.shape[:3]), vol.shape[3]]) uchar_array = numpy_support.numpy_to_vtk(vol, deep=0) im.GetPointData().SetScalars(uchar_array) From d2269cffc99dea5b0c3e138f884c95af56303f10 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 07:08:11 +0000 Subject: [PATCH 10/15] remove python build-ins from toml --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e52e1c2..baee17f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,10 +13,8 @@ packages = [{ include = "TPTBox" }] [tool.poetry.dependencies] python = "^3.9 || ^3.10 || ^3.11 || ^3.12 || ^3.13 || ^3.14" -pathlib = "*" nibabel = "^5.2.0" typing-extensions = "^4.9.0" -dataclasses = "*" SimpleITK = "^2.3.1" matplotlib = "^3.8.2" dill = "^0.3.7" From 93e08c2bd147cf002d64b8dbd4b75fffa80c5ddb Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 07:16:16 +0000 Subject: [PATCH 11/15] remove ants (now optional) --- pyproject.toml | 1 - unit_tests/test_nrrd.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index baee17f..61fd111 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ connected-components-3d = "^3.12.3" tqdm = "*" joblib = "*" scikit-learn = "*" -antspyx = "0.4.2" pynrrd = "*" requests = "*" diff --git a/unit_tests/test_nrrd.py b/unit_tests/test_nrrd.py index 6c2c4a1..2d121a5 100644 --- a/unit_tests/test_nrrd.py +++ b/unit_tests/test_nrrd.py @@ -19,7 +19,7 @@ class TestAnts(unittest.TestCase): - @unittest.skipIf(not has_ants, "requires spineps to be installed") + @unittest.skipIf(not has_ants, "requires ants to be installed") def test_segmentation_CT(self): """Test round-trip for Segmentation.seg.nrrd.""" ct, subreg, vert = get_nii_paths_ct() From 8194e1cbc145fbce57178f891535237a3d0d36ed Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 09:56:34 +0200 Subject: [PATCH 12/15] prevent error when spineps does not find a Dense --- TPTBox/core/np_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TPTBox/core/np_utils.py b/TPTBox/core/np_utils.py index c10ab62..c0c37ed 100755 --- a/TPTBox/core/np_utils.py +++ b/TPTBox/core/np_utils.py @@ -944,7 +944,7 @@ def np_get_connected_components_center_of_mass( connectivity=connectivity, label_ref=label, ) - coms = list(np_center_of_mass(subreg_cc[label]).values()) + coms = list(np_center_of_mass(subreg_cc[label]).values()) if label in subreg_cc else None if sort_by_axis is not None: coms.sort(key=lambda a: a[sort_by_axis]) From a4d7902c3246bf774d0752cc68ae7c3a33023618 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 09:56:55 +0200 Subject: [PATCH 13/15] added PET export --- TPTBox/core/bids_constants.py | 1 + TPTBox/core/dicom/dicom_header_to_keys.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/TPTBox/core/bids_constants.py b/TPTBox/core/bids_constants.py index f726074..4a5e230 100755 --- a/TPTBox/core/bids_constants.py +++ b/TPTBox/core/bids_constants.py @@ -163,6 +163,7 @@ "localizer", "difference", "labels", + "pet" ] # https://bids-specification.readthedocs.io/en/stable/appendices/entity-table.html formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "mr", "snapshot", "t1dixon", "dwi", "ctb"] diff --git a/TPTBox/core/dicom/dicom_header_to_keys.py b/TPTBox/core/dicom/dicom_header_to_keys.py index c9aabff..885a625 100644 --- a/TPTBox/core/dicom/dicom_header_to_keys.py +++ b/TPTBox/core/dicom/dicom_header_to_keys.py @@ -268,6 +268,8 @@ def _get(key, default=None): found = False if modality == "ct": mri_format = "ct" + elif modality.lower() == "pt": + mri_format = "pet" elif modality == "xa": # Angiography biplane = False if "BIPLANE A" in image_type or "SINGLE A" in image_type: From 585760ca5a4405386a073a54f7754a3b3ecc1244 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 08:28:35 +0000 Subject: [PATCH 14/15] exclude bias_field test, because pyants is now optional --- unit_tests/test_stiching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unit_tests/test_stiching.py b/unit_tests/test_stiching.py index 44494ea..75f5aaf 100755 --- a/unit_tests/test_stiching.py +++ b/unit_tests/test_stiching.py @@ -78,7 +78,7 @@ def test_stitching( store_ramp=False, verbose=False, min_value=0, - bias_field=True, + bias_field=False, crop_to_bias_field=False, crop_empty=False, histogram=None, From 0f647d94f225cdad586f7733f80db1224d6f7aac Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 13:26:21 +0000 Subject: [PATCH 15/15] forgot inplace boolean --- TPTBox/core/nii_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TPTBox/core/nii_wrapper.py b/TPTBox/core/nii_wrapper.py index aa14694..93cbe33 100755 --- a/TPTBox/core/nii_wrapper.py +++ b/TPTBox/core/nii_wrapper.py @@ -985,7 +985,7 @@ def resample_from_to(self, to_vox_map:Image_Reference|Has_Grid|tuple[SHAPE,AFFIN # padding after = remaining dst size after src pad_after = dst_shape-shift-src_shape pad = tuple((int(b), int(a)) for b, a in zip(pad_before, pad_after)) - ret = s.apply_pad(pad, mode=mode,) + ret = s.apply_pad(pad, mode=mode,inplace=inplace) #TODO SET raise_error=False before committing valid = ret.assert_affine(mapping,raise_error=True,origin_tolerance=0.0001,error_tolerance=0.0001,shape_tolerance=0)