diff --git a/TPTBox/core/bids_constants.py b/TPTBox/core/bids_constants.py index 0041b1c..a6044dd 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: diff --git a/TPTBox/core/nii_wrapper.py b/TPTBox/core/nii_wrapper.py index 28119bb..93cbe33 100755 --- a/TPTBox/core/nii_wrapper.py +++ b/TPTBox/core/nii_wrapper.py @@ -804,9 +804,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() @@ -829,13 +827,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) @@ -940,35 +961,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,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) + 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 diff --git a/TPTBox/core/np_utils.py b/TPTBox/core/np_utils.py index 6a955f5..cc9bd4d 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]) @@ -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: 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) diff --git a/pyproject.toml b/pyproject.toml index 6ee026e..61fd111 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,26 +13,35 @@ packages = [{ include = "TPTBox" }] [tool.poetry.dependencies] python = "^3.9 || ^3.10 || ^3.11 || ^3.12 || ^3.13 || ^3.14" -pathlib = "*" nibabel = "^5.2.0" -numpy = "^1.26.3" typing-extensions = "^4.9.0" -scipy = "^1.12.0" -dataclasses = "*" SimpleITK = "^2.3.1" matplotlib = "^3.8.2" dill = "^0.3.7" -scikit-image = "^0.22.0" fill-voids = "^2.0.6" connected-components-3d = "^3.12.3" tqdm = "*" 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", python = ">=3.11" } +] + [tool.poetry.group.dev.dependencies] pytest = ">=8.1.1" vtk = "*" 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() 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,