From 073aadbb36f69b4f1a0db8ef801c63fef57efd72 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Thu, 7 May 2026 08:13:13 +0000 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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 ab064718d4dfd49686d895ff12c7f342aa3f898c Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 09:59:39 +0200 Subject: [PATCH 14/23] x --- 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 c0c37ed..3f9351c 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()) if label in subreg_cc else None + coms = list(np_center_of_mass(subreg_cc[label]).values()) if label in subreg_cc else [] if sort_by_axis is not None: coms.sort(key=lambda a: a[sort_by_axis]) From 585760ca5a4405386a073a54f7754a3b3ecc1244 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 08:28:35 +0000 Subject: [PATCH 15/23] 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 16/23] 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) From f39b443273d7fd90850e7bd7a1f25581e68be255 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 13 May 2026 19:55:32 +0200 Subject: [PATCH 17/23] add get_outpaths_spineps to cannonical imports --- TPTBox/segmentation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TPTBox/segmentation/__init__.py b/TPTBox/segmentation/__init__.py index 0601e1f..81244ab 100644 --- a/TPTBox/segmentation/__init__.py +++ b/TPTBox/segmentation/__init__.py @@ -1,5 +1,5 @@ from __future__ import annotations -from TPTBox.segmentation.spineps import _run_spineps_all, run_spineps +from TPTBox.segmentation.spineps import _run_spineps_all, run_spineps,get_outpaths_spineps from TPTBox.segmentation.VibeSeg.vibeseg import extract_vertebra_bodies_from_VibeSeg, run_inference_on_file, run_nnunet, run_vibeseg from TPTBox.segmentation.VibeSeg.vibeseg import run_vibeseg as run_totalvibeseg # TODO deprecate From 6d6654d2f59807cc437d2109c1402443c4646bb9 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 20 May 2026 13:36:54 +0000 Subject: [PATCH 18/23] minor bug fixes --- TPTBox/core/bids_files.py | 2 +- TPTBox/core/nii_wrapper.py | 4 ++-- TPTBox/segmentation/VibeSeg/inference_nnunet.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TPTBox/core/bids_files.py b/TPTBox/core/bids_files.py index e71efb3..d4daa34 100755 --- a/TPTBox/core/bids_files.py +++ b/TPTBox/core/bids_files.py @@ -687,7 +687,7 @@ def get_changed_bids( auto_add_run_id=False, additional_folder: str | None = None, dataset_path: str | None = None, - make_parent=True, + make_parent=False, non_strict_mode=False, ): ds = dataset_path if dataset_path is not None else self.get_path_decomposed()[0] diff --git a/TPTBox/core/nii_wrapper.py b/TPTBox/core/nii_wrapper.py index 93cbe33..f23717d 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,inplace=inplace) + ret = s.apply_pad(pad, mode=mode,inplace=inplace,verbose=verbose) #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) @@ -1801,7 +1801,7 @@ def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|s """ self.assert_affine(reference_mask) if _do_crop: - crop = reference_mask.compute_crop(0,5) + crop = reference_mask.compute_crop(0,5,raise_error=False) s = self.apply_crop(crop) reference_mask = reference_mask.apply_crop(crop) else: diff --git a/TPTBox/segmentation/VibeSeg/inference_nnunet.py b/TPTBox/segmentation/VibeSeg/inference_nnunet.py index bfa8736..34979a2 100644 --- a/TPTBox/segmentation/VibeSeg/inference_nnunet.py +++ b/TPTBox/segmentation/VibeSeg/inference_nnunet.py @@ -227,7 +227,7 @@ def to_int(a: str, k: None | int = None): try: return enum_[a].value except Exception: - print("no ", enum_) + pass if k is not None and k not in unknown_strings.values(): return k unknown_strings[a] = unknown_strings["max"] @@ -247,7 +247,7 @@ def to_int(a: str, k: None | int = None): logger.print(f"{mapping=}") seg_nii.map_labels_(mapping) if out_file is not None and (not Path(out_file).exists() or override): - seg_nii.save(out_file) + seg_nii.set_dtype("smallest_uint").save(out_file) del nnunet torch.cuda.empty_cache() From 9025061c8cfd12ced03c787c78f622ef32fbb8f8 Mon Sep 17 00:00:00 2001 From: robert Date: Wed, 20 May 2026 15:42:01 +0200 Subject: [PATCH 19/23] add PDF export support --- TPTBox/core/bids_constants.py | 3 +- TPTBox/core/dicom/dicom_extract.py | 84 +++++++++++++++++++++-- TPTBox/core/dicom/dicom_header_to_keys.py | 52 +++++++++----- 3 files changed, 114 insertions(+), 25 deletions(-) diff --git a/TPTBox/core/bids_constants.py b/TPTBox/core/bids_constants.py index f726074..67dde05 100755 --- a/TPTBox/core/bids_constants.py +++ b/TPTBox/core/bids_constants.py @@ -163,6 +163,7 @@ "localizer", "difference", "labels", + "report", ] # https://bids-specification.readthedocs.io/en/stable/appendices/entity-table.html formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "mr", "snapshot", "t1dixon", "dwi", "ctb"] @@ -221,7 +222,7 @@ "OPT": "Ophthalmic Tomography", "OPV": "Ophthalmic Visual Field", "OSS": "Optical Surface Scan", - "OT": "Other ", + "OT": "Other", "PLAN": "Plan", "PR": "Presentation State", "PT": "Positron emission tomography (PET)", diff --git a/TPTBox/core/dicom/dicom_extract.py b/TPTBox/core/dicom/dicom_extract.py index 0d63226..b4d14bd 100644 --- a/TPTBox/core/dicom/dicom_extract.py +++ b/TPTBox/core/dicom/dicom_extract.py @@ -190,7 +190,68 @@ def dicom_to_nifti_multiframe(ds, nii_path): return nii_path -def _convert_to_nifti(dicom_out_path, nii_path): +def _export_pdf_from_dicom(dcm_path, out_pdf): + assert len(dcm_path) == 1, dcm_path + ds = dcm_path[0] + + # verify modality / SOP class + if ds.Modality.upper() != "PDF": + raise ValueError("Not a PDF DICOM") + + if "EncapsulatedDocument" not in ds: + raise ValueError("No embedded PDF found") + + pdf_bytes = ds.EncapsulatedDocument + + out_pdf = Path(out_pdf) + out_pdf.write_bytes(pdf_bytes) + + +def _collect_text(ds, txt_lines: list[str] | None = None): + if txt_lines is None: + txt_lines = [] + + def _help_collect_text(content_sequence, level: int = 0): + for item in content_sequence: + prefix = " " * level + + concept = "" + + if hasattr(item, "ConceptNameCodeSequence"): + try: + concept = item.ConceptNameCodeSequence[0].CodeMeaning + except Exception: + pass + + value = None + + for attr in ["TextValue", "CodeMeaning", "NumericValue"]: + if hasattr(item, attr): + value = getattr(item, attr) + break + + if concept or value is not None: + txt_lines.append(f"{prefix}{concept}: {value}") + + if hasattr(item, "ContentSequence"): + _help_collect_text( + item.ContentSequence, + level + 1, + ) + + if hasattr(ds, "ContentSequence"): + _help_collect_text(ds.ContentSequence) + return txt_lines + + +def _extract_txt_from_dicom(dcm_path, out_txt): + lines = [] + for p in dcm_path: + lines = _collect_text(p, lines) + Path(out_txt).write_text("\n".join(lines)) + + +def _extract_nii_from_dicom(dicom_out_path, nii_path): """ Convert DICOM files to NIfTI format and handle common conversion errors. @@ -206,6 +267,7 @@ def _convert_to_nifti(dicom_out_path, nii_path): FunctionTimedOut: Raised if the DICOM-to-NIfTI conversion times out. ValueError: Raised for generic validation failures. """ + try: if isinstance(dicom_out_path, list): try: @@ -217,6 +279,7 @@ def _convert_to_nifti(dicom_out_path, nii_path): return True except Exception as e: logger.on_debug("Multi-Frame DICOM did not work:", e) + ## The PDF dicom lands here convert_dicom.dicom_array_to_nifti(dicom_out_path, nii_path, True) else: # func_timeout(10, dicom2nifti.dicom_series_to_nifti, (dicom_out_path, nii_path, True)) @@ -246,6 +309,9 @@ def _convert_to_nifti(dicom_out_path, nii_path): logger.print_error() return False + except Exception: + print(nii_path) + return True @@ -264,7 +330,7 @@ def _get_paths( ): if keys is None: keys = {} - (mri_format, keys) = extract_keys_from_json( + (mri_format, keys, ending) = extract_keys_from_json( simp_json, dcm_data_l, use_session, @@ -277,7 +343,7 @@ def _get_paths( json_file_name, json_bids_name = _generate_bids_path( dataset_nifti_dir, keys, mri_format, simp_json, make_subject_chunks=make_subject_chunks, parent=parent ) - nii_path = str(json_file_name).replace(".json", "") + ".nii.gz" + nii_path = str(json_file_name).replace(".json", "") + ending return json_file_name, json_bids_name, nii_path @@ -364,11 +430,17 @@ def _from_dicom_to_nii( if exist and Path(nii_path).exists(): logger.print("already exists:", json_file_name, ltype=Log_Type.STRANGE, verbose=verbose) return nii_path - suc = _convert_to_nifti(dcm_data_l, nii_path) + add_grid = False + if nii_path.endswith(".pdf"): + _export_pdf_from_dicom(dcm_data_l, nii_path) + elif nii_path.endswith(".txt"): + _extract_txt_from_dicom(dcm_data_l, nii_path) + else: + add_grid = _extract_nii_from_dicom(dcm_data_l, nii_path) - if suc: + if add_grid: _add_grid_info_to_json(nii_path, json_file_name) - return nii_path if suc else None + return nii_path if add_grid else None def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_update=False, add=True): diff --git a/TPTBox/core/dicom/dicom_header_to_keys.py b/TPTBox/core/dicom/dicom_header_to_keys.py index c9aabff..5ff0200 100644 --- a/TPTBox/core/dicom/dicom_header_to_keys.py +++ b/TPTBox/core/dicom/dicom_header_to_keys.py @@ -165,7 +165,15 @@ def extract_keys_from_json( # noqa: C901 def _get(key, default=None): if key not in simp_json: return keys.get(key, default) - return str(simp_json[key]).replace("_", "-").replace(" ", "-").replace(".", "-") + value = str(simp_json[key]).replace("_", "-").replace(" ", "-").replace(".", "-") + # remove invalid filename characters + value = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "", value) + # collapse repeated dashes + value = re.sub(r"-+", "-", value) + # strip leading/trailing dots and dashes + value = value.strip(".-") + + return value """Extract keys from JSON based on study and series descriptions.""" #### NAKO FIXED #### @@ -174,25 +182,28 @@ def _get(key, default=None): series_description = _get("SeriesDescription", "unnamed") """Determine the MRI format based on the series description.""" if "T2_TSE" in series_description: - return "T2w", {"acq": "sag", "chunk": series_description.split("_")[-1], "sequ": simp_json["SeriesNumber"], **keys} + return "T2w", {"acq": "sag", "chunk": series_description.split("_")[-1], "sequ": simp_json["SeriesNumber"], **keys}, ".nii.gz" elif "3D_GRE_TRA" in series_description: - return "vibe", { - "acq": "ax", - "part": dixon_mapping[series_description.split("_")[-1].lower()], - "chunk": _get("ProtocolName", "unnamed").split("_")[-1], - **keys, - } + return ( + "vibe", + { + "acq": "ax", + "part": dixon_mapping[series_description.split("_")[-1].lower()], + "chunk": _get("ProtocolName", "unnamed").split("_")[-1], + **keys, + }, + ".nii.gz", + ) elif "ME_vibe" in series_description: - return "mevibe", { - "acq": "ax", - "part": dixon_mapping[series_description.split("_")[-1].lower()], - "sequ": simp_json["SeriesNumber"], - **keys, - } + return ( + "mevibe", + {"acq": "ax", "part": dixon_mapping[series_description.split("_")[-1].lower()], "sequ": simp_json["SeriesNumber"], **keys}, + ".nii.gz", + ) elif "PD" in series_description: - return "pd", {"acq": "iso", **keys} + return "pd", {"acq": "iso", **keys}, ".nii.gz" elif "T2_HASTE" in series_description: - return "T2haste", {"acq": "ax", **keys} + return "T2haste", {"acq": "ax", **keys}, ".nii.gz" else: raise NotImplementedError(series_description) # GENERAL @@ -319,9 +330,14 @@ def _get(key, default=None): " km " in series_description.lower() or series_description.startswith("km") or series_description.endswith("km") ) and keys.get("ce") is None: keys["ce"] = "ContrastAgent" + elif modality.lower() == "pdf": + return "report", keys, ".pdf" + elif modality.lower() == "sr": + keys["desc"] = _get("SeriesDescription", None) + return "report", keys, ".txt" else: - raise NotImplementedError(f"modality='{modality.upper()}', ({modalities.get(modality.upper())})") + raise NotImplementedError(f"modality='{modality}', ({modalities.get(modality.upper(), 'Non Standard Modality key')})") # ".*sub.*t1.*": "subtraktion", # "subtraktion.*t1.*": "subtraktion", - return mri_format, keys + return mri_format, keys, ".nii.gz" From de3a52a0dc211dd880b94e3b7c32e8eec24875da Mon Sep 17 00:00:00 2001 From: robert Date: Wed, 20 May 2026 15:42:39 +0200 Subject: [PATCH 20/23] add option to set ramp path in stitching. --- TPTBox/stitching/stitching.py | 4 +++- TPTBox/stitching/stitching_tools.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/TPTBox/stitching/stitching.py b/TPTBox/stitching/stitching.py index 11a75a5..02b8e2e 100755 --- a/TPTBox/stitching/stitching.py +++ b/TPTBox/stitching/stitching.py @@ -324,6 +324,7 @@ def main( # noqa: C901 is_segmentation: bool = False, dtype: type | str = float, save=True, + ramp_path=None, ): np.set_printoptions(precision=2, floatmode="fixed") if is_segmentation: @@ -529,7 +530,8 @@ def main( # noqa: C901 occupancy_arr = occupancy_arr[ex_slice] assert output is not None nii_occ = set_array(nii_out, occupancy_arr) - output = output.replace(".nii.gz", "_ramps.nii.gz") + nii_occ.set_data_dtype(np.int8) + output = output.replace(".nii.gz", "_ramps.nii.gz").replace("_msk_", "_") if ramp_path is None else ramp_path if save: nib.save(nii_occ, output) # type: ignore print("Saved ", output) if verbose else None diff --git a/TPTBox/stitching/stitching_tools.py b/TPTBox/stitching/stitching_tools.py index c1d9e66..bbcb4be 100755 --- a/TPTBox/stitching/stitching_tools.py +++ b/TPTBox/stitching/stitching_tools.py @@ -22,6 +22,7 @@ def stitching( dtype: type = float, match_histogram=False, store_ramp=False, + ramp_path=None, ): out = str(out.file["nii.gz"]) if isinstance(out, BIDS_FILE) else str(out) files = [to_nii(bf).nii for bf in bids_files] @@ -37,6 +38,7 @@ def stitching( kick_out_fully_integrated_images=kick_out_fully_integrated_images, is_segmentation=is_seg, dtype=dtype, + ramp_path=ramp_path, ) From e169b4e41f90195e5e3784be8d43ba64a0d430a0 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 20 May 2026 15:44:16 +0200 Subject: [PATCH 21/23] add print outs --- TPTBox/core/dicom/dicom_extract.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/TPTBox/core/dicom/dicom_extract.py b/TPTBox/core/dicom/dicom_extract.py index 0d63226..c9100b5 100644 --- a/TPTBox/core/dicom/dicom_extract.py +++ b/TPTBox/core/dicom/dicom_extract.py @@ -390,7 +390,7 @@ def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_up return json_dict -def _find_all_files(dcm_dirs: Path | list[Path]): +def _find_all_files(dcm_dirs: Path | list[Path],verbose=False): """ Recursively find all DICOM directories or files in the given paths. @@ -400,6 +400,9 @@ def _find_all_files(dcm_dirs: Path | list[Path]): Yields: Path: Paths to directories or individual DICOM files found during the search. """ + if verbose: + logger.on_neutral("Start file searching") + i = 0 yield dcm_dirs dcm_dirs = dcm_dirs if isinstance(dcm_dirs, list) else [dcm_dirs] for dcm_dir in dcm_dirs: @@ -408,9 +411,15 @@ def _find_all_files(dcm_dirs: Path | list[Path]): file = "" for file in files: if Path(file).is_file(): # str(file).endswith(".dcm") or str(file).endswith(".ima") + if verbose: + logger.on_neutral("File ",i,end="\r") + i += 1 yield Path(root, file).absolute().parent break else: + if verbose: + logger.on_neutral("File ",i,end="\r") + i += 1 yield Path(root, file) # if "." not in str(file): # yield Path(root, file).absolute().parent @@ -606,7 +615,7 @@ def extract_dicom_folder( convert_dicom.settings.disable_validate_slice_increment() outs = {} - for p in _find_all_files(dicom_folder): + for p in _find_all_files(dicom_folder,verbose=verbose): dicom_path = p if str(dicom_path).endswith(".pkl"): @@ -672,6 +681,7 @@ def process_series(key, files, parts): p, Path("/media/data/robert/datasets", "dataset-Durchleuchtung222"), False, False, validate_slice_increment=False ) + sys.exit() # s = "/home/robert/Downloads/bein/dataset-oberschenkel/rawdata/sub-1-3-46-670589-11-2889201787-2305829596-303261238-2367429497/mr/sub-1-3-46-670589-11-2889201787-2305829596-303261238-2367429497_sequ-406_mr.nii.gz" # nii2 = NII.load(s, False) From 877d733df6b8b7bd9dfdd53f4ffb2e49ff1e0424 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 20 May 2026 13:48:12 +0000 Subject: [PATCH 22/23] ruff --- TPTBox/segmentation/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/TPTBox/segmentation/__init__.py b/TPTBox/segmentation/__init__.py index 0601e1f..4b19d48 100644 --- a/TPTBox/segmentation/__init__.py +++ b/TPTBox/segmentation/__init__.py @@ -2,4 +2,3 @@ from TPTBox.segmentation.spineps import _run_spineps_all, run_spineps from TPTBox.segmentation.VibeSeg.vibeseg import extract_vertebra_bodies_from_VibeSeg, run_inference_on_file, run_nnunet, run_vibeseg -from TPTBox.segmentation.VibeSeg.vibeseg import run_vibeseg as run_totalvibeseg # TODO deprecate From b5fad178bf4a0a53e34c6ba8fc56abc1f19babe3 Mon Sep 17 00:00:00 2001 From: ga84mun Date: Wed, 20 May 2026 13:48:34 +0000 Subject: [PATCH 23/23] ruff --- TPTBox/segmentation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TPTBox/segmentation/__init__.py b/TPTBox/segmentation/__init__.py index 138275b..0070a83 100644 --- a/TPTBox/segmentation/__init__.py +++ b/TPTBox/segmentation/__init__.py @@ -1,4 +1,4 @@ from __future__ import annotations -from TPTBox.segmentation.spineps import _run_spineps_all, run_spineps,get_outpaths_spineps +from TPTBox.segmentation.spineps import _run_spineps_all, get_outpaths_spineps, run_spineps from TPTBox.segmentation.VibeSeg.vibeseg import extract_vertebra_bodies_from_VibeSeg, run_inference_on_file, run_nnunet, run_vibeseg