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
3 changes: 2 additions & 1 deletion TPTBox/core/bids_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

formats = [
"ct",
"cta",
"dixon",
"T2c",
"T1c",
Expand Down Expand Up @@ -163,7 +164,7 @@
"labels",
]
# https://bids-specification.readthedocs.io/en/stable/appendices/entity-table.html
formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "cta", "mr", "snapshot", "t1dixon", "dwi"]
formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "mr", "snapshot", "t1dixon", "dwi", "ctb"]
# Recommended writing style: T1c, T2c; This list is not official and can be extended.

modalities = {
Expand Down
10 changes: 7 additions & 3 deletions TPTBox/core/bids_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,10 @@ def bids_format(self):

@property
def mod(self):
return self.mod
mod = self.bids_format
if mod == "msk":
return self.get("mod")
return mod

def get_parent(self, file_type=None):
return self.get_path_decomposed(file_type)[1]
Expand Down Expand Up @@ -985,7 +988,8 @@ def open_nii(self):
except KeyError as e:
raise ValueError(f"nii.gz not present. Found only {self.file.keys()}\t{self.file}\n\n{self}") from e

def get_grid_info(self):
def get_grid_info(self, add_gird_info_to_json=True):
"""returns the Grid info. It looks up if this info is in json. If not it loads the File, computes the Grid and saves it in the json"""
from TPTBox.core.dicom.dicom_extract import _add_grid_info_to_json
from TPTBox.core.nii_poi_abstract import Grid

Expand All @@ -994,7 +998,7 @@ def get_grid_info(self):
return None
if not self.has_json():
self.file["json"] = Path(str(nii_file).split(".")[0] + ".json")
return Grid(**_add_grid_info_to_json(nii_file, self.file["json"])["grid"])
return Grid(**_add_grid_info_to_json(nii_file, self.file["json"], add=add_gird_info_to_json)["grid"])

def get_nii_file(self) -> Path: # type: ignore
for key in _supported_nii_files:
Expand Down
5 changes: 3 additions & 2 deletions TPTBox/core/dicom/dicom2nii_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def test_name_conflict(json_ob, file):
return False


def save_json(json_ob, file, check_exist=False):
def save_json(json_ob, file, check_exist=False, override=True):
"""
recieves a json object and a path and saves the object as a json file
"""
Expand All @@ -257,8 +257,9 @@ def convert(obj):

if check_exist and test_name_conflict(json_ob, file):
raise FileExistsError(file)
if Path(file).exists():
if Path(file).exists() and not override:
return True
Print_Logger().on_save("save json with grid info", file)
with open(file, "w") as file_handel:
json.dump(json_ob, file_handel, indent=4, default=convert)
return False
Expand Down
7 changes: 4 additions & 3 deletions TPTBox/core/dicom/dicom_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,12 @@ def _from_dicom_to_nii(
return nii_path if suc else None


def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_update=False):
nii = NII.load(nii_path, False)
def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_update=False, add=True):
json_dict = load_json(simp_json) if Path(simp_json).exists() else {}
if "grid" in json_dict and not force_update:
return json_dict
print("Read Grid info", Path(simp_json).exists(), "grid" in json_dict)
nii = NII.load(nii_path, False)
gird = {
"shape": nii.shape,
"spacing": nii.spacing,
Expand All @@ -330,7 +331,7 @@ def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_up
"dims": nii.get_num_dims(),
}
json_dict["grid"] = gird
save_json(json_dict, simp_json)
save_json(json_dict, simp_json, override=add)
return json_dict


Expand Down
2 changes: 1 addition & 1 deletion TPTBox/core/dicom/dicom_header_to_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def _get(key, default=None):
keys["acq"] = to_nii(dcm_data_l).get_plane(1)
else:
keys["acq"] = get_plane_dicom(dcm_data_l, 1)
keys["part"] = dixon_mapping.get(_get("ProtocolName", "NO-PART").split("_")[-1], None)
keys["part"] = dixon_mapping.get(_get("ProtocolName", "NO-PART").split("_")[-1])

sequ = _get("SeriesNumber", None)
if sequ is None:
Expand Down
6 changes: 5 additions & 1 deletion TPTBox/core/internal/ants_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from typing import TYPE_CHECKING

import ants
import numpy as np

if TYPE_CHECKING:
Expand All @@ -23,6 +22,8 @@ def nifti_to_ants(nib_image: Nifti1Image, **args):
ants_image : ants.ANTsImage
The converted ANTs image.
"""
import ants

try:
return ants.utils.from_nibabel_nifti(nib_image, **args)
except Exception:
Expand Down Expand Up @@ -111,6 +112,8 @@ def ants_to_nifti(img, header=None) -> Nifti1Image:
img : Nifti1Image
The converted Nifti image.
"""
import ants

try:
return ants.utils.to_nibabel_nifti(img, header=header)
except Exception:
Expand All @@ -131,6 +134,7 @@ def ants_to_nifti(img, header=None) -> Nifti1Image:
to_nibabel = ants_to_nifti

if __name__ == "__main__":
import ants
import nibabel as nib

fn = ants.get_ants_data("mni")
Expand Down
33 changes: 31 additions & 2 deletions TPTBox/core/internal/slicer_nrrd.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,38 @@ def _write_segmentation(file, segmentation, compression_level=9, index_order=Non

# Get list of segment IDs (needed if we need to generate new ID)
segment_ids = set()
for _, segment in enumerate(segmentation["segments"]):
segment_ids_num = set()
for _, segment in enumerate(segmentation.get("segments", [])):
if "id" in segment:
segment_ids.add(segment["id"])
segment_ids_num.add(segment["labelValue"])
from TPTBox.core.np_utils import np_unique_withoutzero
from TPTBox.mesh3D.mesh_colors import get_color_by_label

if "segments" not in segmentation:
segmentation["segments"] = []
for i in np_unique_withoutzero(voxels):
if i in segment_ids_num:
continue
l: list = segmentation["segments"]
segment_ids.add(f"Segment_{i}")
l.append(
{
"id": f"Segment_{i}",
"color": get_color_by_label(i).rgb / 255.0,
"colorAutoGenerated": False,
"labelValue": i,
"layer": 0,
"name": f"Segment_{i}",
"nameAutoGenerated": True,
"terminology": {
"contextName": "TPTBox colors",
"category": ["SCT", "85756007", "Tissue"],
"type": ["SCT", "85756007", "Tissue"],
"anatomicContextName": "Undefined",
},
}
)

for output_segment_index, segment in enumerate(segmentation["segments"]):
# Copy all segment fields corresponding to this segment
Expand Down Expand Up @@ -694,7 +723,7 @@ def save_slicer_nrrd(nii: NII, file: str | Path, make_parents=True, verbose: log
**info,
}
if nii.seg:
segmentation["containedRepresentationNames"] = info.get("containedRepresentationNames", ["Binary labelmap"])
segmentation["containedRepresentationNames"] = info.get("containedRepresentationNames", ["Binary labelmap", "Closed surface"])
segmentation["masterRepresentation"] = info.get("masterRepresentation", "Binary labelmap")
segmentation["referenceImageExtentOffset"] = info.get("referenceImageExtentOffset", [0, 0, 0])
remove_not_supported_values(segmentation)
Expand Down
3 changes: 2 additions & 1 deletion TPTBox/core/nii_poi_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def to_gird(self) -> Grid:

@property
def shape_int(self):
assert self.shape is not None, "need shape information"
if self.shape is None:
return None
return tuple(np.rint(list(self.shape)).astype(int).tolist())

@property
Expand Down
70 changes: 45 additions & 25 deletions TPTBox/core/nii_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
MODES,
SHAPE,
ZOOMS,
Location,
_same_direction,
log,
logging,
Expand Down Expand Up @@ -309,13 +308,22 @@ def _unpack(self):
try:
if self.__unpacked:
return
if not self._checked_dtype or self.seg:
dtype = _check_if_nifty_is_lying_about_its_dtype(self)
#print("unpack-nii",f"{self.seg=}",dtype)
self._checked_dtype = True
self._arr = np.asanyarray(self.nii.dataobj, dtype=dtype).copy()
else:
self._arr = np.asanyarray(self.nii.dataobj, dtype=self.nii.dataobj.dtype).copy() #type: ignore
try:
#if arr.dtype.fields is not None: # structured dtype (RGB)
# arr = np.stack([arr[name] for name in arr.dtype.names], axis=-1)
if not self._checked_dtype or self.seg:
dtype = _check_if_nifty_is_lying_about_its_dtype(self)
#print("unpack-nii",f"{self.seg=}",dtype)
self._checked_dtype = True
self._arr = np.asanyarray(self.nii.dataobj, dtype=dtype).copy()
else:
self._arr = np.asanyarray(self.nii.dataobj, dtype=self.nii.dataobj.dtype).copy() #type: ignore
except np.exceptions.DTypePromotionError:
arr = np.asarray(self.nii.dataobj)
if arr.dtype.fields is not None: # structured dtype (RGB)
self._arr = np.stack([arr[name] for name in arr.dtype.names], axis=-1)
else:
raise np.exceptions.DTypePromotionError(f"The DTypes <class '{self.nii.dataobj.dtype}'> do not have a common numerical DType. {np.asarray(self.nii.dataobj)}") from None

self._aff = self.nii.affine
self._header:Nifti1Header = self.nii.header # type: ignore
Expand Down Expand Up @@ -779,9 +787,11 @@ def pad_to(self,target_shape:list[int]|tuple[int,int,int] | Self, mode:MODES="co
s = s.apply_crop(tuple(crop),inplace=inplace)
return s.apply_pad(padding,inplace=inplace,mode=mode)

def apply_pad(self,padd:Sequence[tuple[int|None,int]],mode:MODES="constant",inplace = False,verbose:logging=True):
def apply_pad(self,padd:Sequence[tuple[int|None,int]]|None,mode:MODES="constant",inplace = False,verbose:logging=True):
#TODO add other modes
#TODO add testcases and options for modes
if padd is None:
return self if inplace else self.copy()
transform = np.eye(self.dims+1, dtype=int)
assert len(padd) == self.dims
for i, (before,_) in enumerate(padd):
Expand Down Expand Up @@ -1567,7 +1577,7 @@ def truncate_labels_beyond_reference_(
if len(threshold[axis_]) == 0:
return self if inplace else self.copy()
flip_up = flip
if inclusion:
if not inclusion:
flip_up = not flip_up
# Determine the lowest index along the axis
limit = threshold[axis_].min() if flip_up else threshold[axis_].max()
Expand All @@ -1593,11 +1603,12 @@ def truncate_labels_beyond_reference(
not_beyond: int | list[int] = 1,
fill: int = 0,
axis: DIRECTIONS = "S",
inclusion: bool = False
inclusion: bool = False,
inplace=False
):
return self.truncate_labels_beyond_reference_(idx,not_beyond,fill,axis,inclusion)
return self.truncate_labels_beyond_reference_(idx,not_beyond,fill,axis,inclusion,inplace=inplace)

def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|str|None=None):
def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|str|None=None,max_depth=None):
"""
Expands labels from self_mask into regions of reference_mask == 1 via breadth-first diffusion.

Expand Down Expand Up @@ -1633,7 +1644,7 @@ def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|s

search = []
coords = np.where(self_mask != 0)
def _add_idx(x,y,z,v):
def _add_idx(x,y,z,v,d):
for x1,y1,z1 in kernel:
a = x+x1
b = y+y1
Expand All @@ -1644,27 +1655,29 @@ def _add_idx(x,y,z,v):
continue
#try:
if searched[a,b,c] == 0 and ref_mask[a,b,c] == 1:
search.append((a,b,c,v))
search.append((a,b,c,v,d))
#except Exception:
# pass
def _infect(a,b,c,v):
def _infect(a,b,c,v,d):
if d-1 == max_depth:
return
if searched[a,b,c] != 0:
return
if ref_mask[a,b,c] == 0:
return
#print(a,b,c)
searched[a,b,c] = 1
self_mask[a,b,c] = v
_add_idx(x,y,z,v)
_add_idx(x,y,z,v,d)

from tqdm import tqdm
for x,y,z in tqdm(zip(coords[0],coords[1],coords[2]),total=len(coords[0]),disable=not verbose,desc="Collecting Surface"):
_add_idx(x,y,z,self_mask[x,y,z])
_add_idx(x,y,z,self_mask[x,y,z],0)
while len(search) != 0:
search2 = search
search = []
for x,y,z,v in tqdm(search2,disable=not verbose,desc="infect"):
_infect(x,y,z,v)
for x,y,z,v,d in tqdm(search2,disable=not verbose,desc="infect"):
_infect(x,y,z,v,d+1)
self_mask[self_mask == 0] = self_mask_org[self_mask == 0]
return self.set_array(self_mask,inplace=inplace)

Expand Down Expand Up @@ -1897,9 +1910,9 @@ def extract_label(self,label:int|Enum|Sequence[int]|Sequence[Enum]|None, keep_la
if keep_label:
seg_arr = seg_arr * self.get_seg_array()
return self.set_array(seg_arr,inplace=inplace)
def extract_label_(self,label:int|Location|Sequence[int]|Sequence[Location], keep_label=False):
def extract_label_(self,label:int|Enum|Sequence[int]|Sequence[Enum], keep_label=False):
return self.extract_label(label,keep_label,inplace=True)
def remove_labels(self,label:int|Location|Sequence[int]|Sequence[Location], inplace=False, verbose:logging=True, removed_to_label=0):
def remove_labels(self,label:int|Enum|Sequence[int]|Sequence[Enum], inplace=False, verbose:logging=True, removed_to_label=0):
'''If this NII is a segmentation you can single out one label.'''
assert label != 0, 'Zero label does not make sens. This is the background'
seg_arr = self.get_seg_array()
Expand All @@ -1912,7 +1925,7 @@ def remove_labels(self,label:int|Location|Sequence[int]|Sequence[Location], inpl
else:
seg_arr[seg_arr == l] = removed_to_label
return self.set_array(seg_arr,inplace=inplace, verbose=verbose)
def remove_labels_(self,label:int|Location|Sequence[int]|Sequence[Location], verbose:logging=True):
def remove_labels_(self,label:int|Enum|Sequence[int]|Sequence[Enum], verbose:logging=True):
return self.remove_labels(label,inplace=True,verbose=verbose)
def apply_mask(self,mask:Self, inplace=False):
assert mask.shape == self.shape, f"[def apply_mask] Mask and Shape are not equal: \nMask - {mask},\nSelf - {self})"
Expand All @@ -1921,9 +1934,16 @@ def apply_mask(self,mask:Self, inplace=False):
arr = self.get_array()
return self.set_array(arr*seg_arr,inplace=inplace)

def unique(self,verbose:logging=False):
def unique(self,verbose:logging=False,crop=False):
'''Returns all integer labels WITHOUT 0. Must be performed only on a segmentation nii'''
out = np_unique_withoutzero(self.get_seg_array())

arr = self.get_seg_array()
if crop:
try:
arr = arr[np_bbox_binary(arr)]
except Exception:
pass
out = np_unique_withoutzero(arr)
log.print(out,verbose=verbose)
return out
def voxel_volume(self):
Expand Down
8 changes: 6 additions & 2 deletions TPTBox/core/nii_wrapper_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,15 @@ def mean(self,axis = None,keepdims=False,where = np._NoValue, **qargs)->float:
where=where.get_array().astype(bool)

return np.mean(self.get_array(),axis=axis,keepdims=keepdims,where=where,**qargs)
def median(self,axis = None,keepdims=False,where = np._NoValue, **qargs)->float: # type: ignore
def median(self, axis=None, keepdims=False, **qargs): # type: ignore
arr = self.get_array()
return np.median(arr, axis=axis, keepdims=keepdims, **qargs)

def std(self,axis = None,keepdims=False,where = np._NoValue, **qargs)->float: # type: ignore
if hasattr(where,"get_array"):
where=where.get_array().astype(bool)

return np.median(self.get_array(),axis=axis,keepdims=keepdims,where=where,**qargs)
return np.std(self.get_array(),axis=axis,keepdims=keepdims,where=where,**qargs)
def threshold(self,threshold=0.5, inplace=False):
arr = self.get_array()
arr2 = arr.copy()
Expand Down
2 changes: 2 additions & 0 deletions TPTBox/core/poi.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ def load(cls, poi: POI_Reference, reference: Has_Grid | None = None, allow_globa
if isinstance(poi_obj, POI_Global):
poi_obj = poi_obj.resample_from_to(reference)
else:
if poi_obj.orientation == ("U", "U", "U"):
poi_obj.orientation = reference.orientation
if poi_obj.spacing is None:
poi_obj.spacing = reference.spacing
if poi_obj.rotation is None:
Expand Down
Loading