Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 57 additions & 33 deletions TPTBox/core/nii_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion TPTBox/core/np_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 5 additions & 9 deletions TPTBox/mesh3D/snapshot3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
20 changes: 16 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,36 @@ 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 = "*"
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 = "*"
Expand Down
Loading