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 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: 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..e52e1c2 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 = "^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 = "*" @@ -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", python = ">=3.11" } +] + [tool.poetry.group.dev.dependencies] pytest = ">=8.1.1" vtk = "*"