Skip to content

Commit 7d54443

Browse files
authored
Merge pull request #100 from Hendrik-code/development_robert
Development robert
2 parents be2e950 + 64ab93b commit 7d54443

61 files changed

Lines changed: 1576 additions & 302 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

TPTBox/core/bids_constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
formats = [
66
"ct",
7+
"cta",
78
"dixon",
89
"T2c",
910
"T1c",
@@ -163,7 +164,7 @@
163164
"labels",
164165
]
165166
# https://bids-specification.readthedocs.io/en/stable/appendices/entity-table.html
166-
formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "cta", "mr", "snapshot", "t1dixon", "dwi"]
167+
formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "mr", "snapshot", "t1dixon", "dwi", "ctb"]
167168
# Recommended writing style: T1c, T2c; This list is not official and can be extended.
168169

169170
modalities = {

TPTBox/core/bids_files.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,10 @@ def bids_format(self):
664664

665665
@property
666666
def mod(self):
667-
return self.mod
667+
mod = self.bids_format
668+
if mod == "msk":
669+
return self.get("mod")
670+
return mod
668671

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

988-
def get_grid_info(self):
991+
def get_grid_info(self, add_grid_info_to_json=True):
992+
"""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"""
989993
from TPTBox.core.dicom.dicom_extract import _add_grid_info_to_json
990994
from TPTBox.core.nii_poi_abstract import Grid
991995

@@ -994,7 +998,7 @@ def get_grid_info(self):
994998
return None
995999
if not self.has_json():
9961000
self.file["json"] = Path(str(nii_file).split(".")[0] + ".json")
997-
return Grid(**_add_grid_info_to_json(nii_file, self.file["json"])["grid"])
1001+
return Grid(**_add_grid_info_to_json(nii_file, self.file["json"], add=add_grid_info_to_json)["grid"])
9981002

9991003
def get_nii_file(self) -> Path: # type: ignore
10001004
for key in _supported_nii_files:

TPTBox/core/dicom/dicom2nii_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def test_name_conflict(json_ob, file):
243243
return False
244244

245245

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

258258
if check_exist and test_name_conflict(json_ob, file):
259259
raise FileExistsError(file)
260-
if Path(file).exists():
260+
if Path(file).exists() and not override:
261261
return True
262+
Print_Logger().on_save("save json with grid info", file)
262263
with open(file, "w") as file_handel:
263264
json.dump(json_ob, file_handel, indent=4, default=convert)
264265
return False

TPTBox/core/dicom/dicom_extract.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,12 @@ def _from_dicom_to_nii(
316316
return nii_path if suc else None
317317

318318

319-
def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_update=False):
320-
nii = NII.load(nii_path, False)
319+
def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_update=False, add=True):
321320
json_dict = load_json(simp_json) if Path(simp_json).exists() else {}
322321
if "grid" in json_dict and not force_update:
323322
return json_dict
323+
print("Read Grid info", Path(simp_json).exists(), "grid" in json_dict)
324+
nii = NII.load(nii_path, False)
324325
gird = {
325326
"shape": nii.shape,
326327
"spacing": nii.spacing,
@@ -330,7 +331,7 @@ def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_up
330331
"dims": nii.get_num_dims(),
331332
}
332333
json_dict["grid"] = gird
333-
save_json(json_dict, simp_json)
334+
save_json(json_dict, simp_json, override=add)
334335
return json_dict
335336

336337

TPTBox/core/dicom/dicom_header_to_keys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def _get(key, default=None):
222222
keys["acq"] = to_nii(dcm_data_l).get_plane(1)
223223
else:
224224
keys["acq"] = get_plane_dicom(dcm_data_l, 1)
225-
keys["part"] = dixon_mapping.get(_get("ProtocolName", "NO-PART").split("_")[-1], None)
225+
keys["part"] = dixon_mapping.get(_get("ProtocolName", "NO-PART").split("_")[-1])
226226

227227
sequ = _get("SeriesNumber", None)
228228
if sequ is None:

TPTBox/core/internal/ants_load.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from typing import TYPE_CHECKING
44

5-
import ants
65
import numpy as np
76

87
if TYPE_CHECKING:
@@ -23,6 +22,8 @@ def nifti_to_ants(nib_image: Nifti1Image, **args):
2322
ants_image : ants.ANTsImage
2423
The converted ANTs image.
2524
"""
25+
import ants
26+
2627
try:
2728
return ants.utils.from_nibabel_nifti(nib_image, **args)
2829
except Exception:
@@ -111,6 +112,8 @@ def ants_to_nifti(img, header=None) -> Nifti1Image:
111112
img : Nifti1Image
112113
The converted Nifti image.
113114
"""
115+
import ants
116+
114117
try:
115118
return ants.utils.to_nibabel_nifti(img, header=header)
116119
except Exception:
@@ -131,6 +134,7 @@ def ants_to_nifti(img, header=None) -> Nifti1Image:
131134
to_nibabel = ants_to_nifti
132135

133136
if __name__ == "__main__":
137+
import ants
134138
import nibabel as nib
135139

136140
fn = ants.get_ants_data("mni")

TPTBox/core/internal/slicer_nrrd.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,38 @@ def _write_segmentation(file, segmentation, compression_level=9, index_order=Non
399399

400400
# Get list of segment IDs (needed if we need to generate new ID)
401401
segment_ids = set()
402-
for _, segment in enumerate(segmentation["segments"]):
402+
segment_ids_num = set()
403+
for _, segment in enumerate(segmentation.get("segments", [])):
403404
if "id" in segment:
404405
segment_ids.add(segment["id"])
406+
segment_ids_num.add(segment["labelValue"])
407+
from TPTBox.core.np_utils import np_unique_withoutzero
408+
from TPTBox.mesh3D.mesh_colors import get_color_by_label
409+
410+
if "segments" not in segmentation:
411+
segmentation["segments"] = []
412+
for i in np_unique_withoutzero(voxels):
413+
if i in segment_ids_num:
414+
continue
415+
l: list = segmentation["segments"]
416+
segment_ids.add(f"Segment_{i}")
417+
l.append(
418+
{
419+
"id": f"Segment_{i}",
420+
"color": get_color_by_label(i).rgb / 255.0,
421+
"colorAutoGenerated": False,
422+
"labelValue": i,
423+
"layer": 0,
424+
"name": f"Segment_{i}",
425+
"nameAutoGenerated": True,
426+
"terminology": {
427+
"contextName": "TPTBox colors",
428+
"category": ["SCT", "85756007", "Tissue"],
429+
"type": ["SCT", "85756007", "Tissue"],
430+
"anatomicContextName": "Undefined",
431+
},
432+
}
433+
)
405434

406435
for output_segment_index, segment in enumerate(segmentation["segments"]):
407436
# Copy all segment fields corresponding to this segment
@@ -694,7 +723,7 @@ def save_slicer_nrrd(nii: NII, file: str | Path, make_parents=True, verbose: log
694723
**info,
695724
}
696725
if nii.seg:
697-
segmentation["containedRepresentationNames"] = info.get("containedRepresentationNames", ["Binary labelmap"])
726+
segmentation["containedRepresentationNames"] = info.get("containedRepresentationNames", ["Binary labelmap", "Closed surface"])
698727
segmentation["masterRepresentation"] = info.get("masterRepresentation", "Binary labelmap")
699728
segmentation["referenceImageExtentOffset"] = info.get("referenceImageExtentOffset", [0, 0, 0])
700729
remove_not_supported_values(segmentation)

TPTBox/core/nii_poi_abstract.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def to_gird(self) -> Grid:
5757

5858
@property
5959
def shape_int(self):
60-
assert self.shape is not None, "need shape information"
60+
if self.shape is None:
61+
return None
6162
return tuple(np.rint(list(self.shape)).astype(int).tolist())
6263

6364
@property

TPTBox/core/nii_wrapper.py

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
MODES,
5959
SHAPE,
6060
ZOOMS,
61-
Location,
6261
_same_direction,
6362
log,
6463
logging,
@@ -309,13 +308,22 @@ def _unpack(self):
309308
try:
310309
if self.__unpacked:
311310
return
312-
if not self._checked_dtype or self.seg:
313-
dtype = _check_if_nifty_is_lying_about_its_dtype(self)
314-
#print("unpack-nii",f"{self.seg=}",dtype)
315-
self._checked_dtype = True
316-
self._arr = np.asanyarray(self.nii.dataobj, dtype=dtype).copy()
317-
else:
318-
self._arr = np.asanyarray(self.nii.dataobj, dtype=self.nii.dataobj.dtype).copy() #type: ignore
311+
try:
312+
#if arr.dtype.fields is not None: # structured dtype (RGB)
313+
# arr = np.stack([arr[name] for name in arr.dtype.names], axis=-1)
314+
if not self._checked_dtype or self.seg:
315+
dtype = _check_if_nifty_is_lying_about_its_dtype(self)
316+
#print("unpack-nii",f"{self.seg=}",dtype)
317+
self._checked_dtype = True
318+
self._arr = np.asanyarray(self.nii.dataobj, dtype=dtype).copy()
319+
else:
320+
self._arr = np.asanyarray(self.nii.dataobj, dtype=self.nii.dataobj.dtype).copy() #type: ignore
321+
except np.exceptions.DTypePromotionError:
322+
arr = np.asarray(self.nii.dataobj)
323+
if arr.dtype.fields is not None: # structured dtype (RGB)
324+
self._arr = np.stack([arr[name] for name in arr.dtype.names], axis=-1)
325+
else:
326+
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
319327

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

782-
def apply_pad(self,padd:Sequence[tuple[int|None,int]],mode:MODES="constant",inplace = False,verbose:logging=True):
790+
def apply_pad(self,padd:Sequence[tuple[int|None,int]]|None,mode:MODES="constant",inplace = False,verbose:logging=True):
783791
#TODO add other modes
784792
#TODO add testcases and options for modes
793+
if padd is None:
794+
return self if inplace else self.copy()
785795
transform = np.eye(self.dims+1, dtype=int)
786796
assert len(padd) == self.dims
787797
for i, (before,_) in enumerate(padd):
@@ -1518,7 +1528,9 @@ def is_segmentation_in_border(self,minimum=0, voxel_tolerance: int = 2,use_mm=Fa
15181528
Returns:
15191529
- bool: True if the segmentation is within the defined voxel tolerance of the border, False otherwise.
15201530
"""
1521-
slices = self.compute_crop(minimum,dist=0,use_mm=use_mm)
1531+
slices = self.compute_crop(minimum,dist=0,use_mm=use_mm,raise_error=False)
1532+
if slices is None:
1533+
return False
15221534
shp = self.shape
15231535
seg_at_border = False
15241536
for d in range(3):
@@ -1567,7 +1579,7 @@ def truncate_labels_beyond_reference_(
15671579
if len(threshold[axis_]) == 0:
15681580
return self if inplace else self.copy()
15691581
flip_up = flip
1570-
if inclusion:
1582+
if not inclusion:
15711583
flip_up = not flip_up
15721584
# Determine the lowest index along the axis
15731585
limit = threshold[axis_].min() if flip_up else threshold[axis_].max()
@@ -1593,11 +1605,12 @@ def truncate_labels_beyond_reference(
15931605
not_beyond: int | list[int] = 1,
15941606
fill: int = 0,
15951607
axis: DIRECTIONS = "S",
1596-
inclusion: bool = False
1608+
inclusion: bool = False,
1609+
inplace=False
15971610
):
1598-
return self.truncate_labels_beyond_reference_(idx,not_beyond,fill,axis,inclusion)
1611+
return self.truncate_labels_beyond_reference_(idx,not_beyond,fill,axis,inclusion,inplace=inplace)
15991612

1600-
def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|str|None=None):
1613+
def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|str|None=None,max_depth=None):
16011614
"""
16021615
Expands labels from self_mask into regions of reference_mask == 1 via breadth-first diffusion.
16031616
@@ -1633,7 +1646,7 @@ def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|s
16331646

16341647
search = []
16351648
coords = np.where(self_mask != 0)
1636-
def _add_idx(x,y,z,v):
1649+
def _add_idx(x,y,z,v,d):
16371650
for x1,y1,z1 in kernel:
16381651
a = x+x1
16391652
b = y+y1
@@ -1644,27 +1657,29 @@ def _add_idx(x,y,z,v):
16441657
continue
16451658
#try:
16461659
if searched[a,b,c] == 0 and ref_mask[a,b,c] == 1:
1647-
search.append((a,b,c,v))
1660+
search.append((a,b,c,v,d))
16481661
#except Exception:
16491662
# pass
1650-
def _infect(a,b,c,v):
1663+
def _infect(a,b,c,v,d):
1664+
if d-1 == max_depth:
1665+
return
16511666
if searched[a,b,c] != 0:
16521667
return
16531668
if ref_mask[a,b,c] == 0:
16541669
return
16551670
#print(a,b,c)
16561671
searched[a,b,c] = 1
16571672
self_mask[a,b,c] = v
1658-
_add_idx(x,y,z,v)
1673+
_add_idx(x,y,z,v,d)
16591674

16601675
from tqdm import tqdm
16611676
for x,y,z in tqdm(zip(coords[0],coords[1],coords[2]),total=len(coords[0]),disable=not verbose,desc="Collecting Surface"):
1662-
_add_idx(x,y,z,self_mask[x,y,z])
1677+
_add_idx(x,y,z,self_mask[x,y,z],0)
16631678
while len(search) != 0:
16641679
search2 = search
16651680
search = []
1666-
for x,y,z,v in tqdm(search2,disable=not verbose,desc="infect"):
1667-
_infect(x,y,z,v)
1681+
for x,y,z,v,d in tqdm(search2,disable=not verbose,desc="infect"):
1682+
_infect(x,y,z,v,d+1)
16681683
self_mask[self_mask == 0] = self_mask_org[self_mask == 0]
16691684
return self.set_array(self_mask,inplace=inplace)
16701685

@@ -1897,9 +1912,9 @@ def extract_label(self,label:int|Enum|Sequence[int]|Sequence[Enum]|None, keep_la
18971912
if keep_label:
18981913
seg_arr = seg_arr * self.get_seg_array()
18991914
return self.set_array(seg_arr,inplace=inplace)
1900-
def extract_label_(self,label:int|Location|Sequence[int]|Sequence[Location], keep_label=False):
1915+
def extract_label_(self,label:int|Enum|Sequence[int]|Sequence[Enum], keep_label=False):
19011916
return self.extract_label(label,keep_label,inplace=True)
1902-
def remove_labels(self,label:int|Location|Sequence[int]|Sequence[Location], inplace=False, verbose:logging=True, removed_to_label=0):
1917+
def remove_labels(self,label:int|Enum|Sequence[int]|Sequence[Enum], inplace=False, verbose:logging=True, removed_to_label=0):
19031918
'''If this NII is a segmentation you can single out one label.'''
19041919
assert label != 0, 'Zero label does not make sens. This is the background'
19051920
seg_arr = self.get_seg_array()
@@ -1912,18 +1927,25 @@ def remove_labels(self,label:int|Location|Sequence[int]|Sequence[Location], inpl
19121927
else:
19131928
seg_arr[seg_arr == l] = removed_to_label
19141929
return self.set_array(seg_arr,inplace=inplace, verbose=verbose)
1915-
def remove_labels_(self,label:int|Location|Sequence[int]|Sequence[Location], verbose:logging=True):
1916-
return self.remove_labels(label,inplace=True,verbose=verbose)
1930+
def remove_labels_(self,label:int|Enum|Sequence[int]|Sequence[Enum],removed_to_label=0, verbose:logging=True):
1931+
return self.remove_labels(label,inplace=True,removed_to_label=removed_to_label,verbose=verbose)
19171932
def apply_mask(self,mask:Self, inplace=False):
19181933
assert mask.shape == self.shape, f"[def apply_mask] Mask and Shape are not equal: \nMask - {mask},\nSelf - {self})"
19191934
seg_arr = mask.get_seg_array()
19201935
seg_arr[seg_arr != 0] = 1
19211936
arr = self.get_array()
19221937
return self.set_array(arr*seg_arr,inplace=inplace)
19231938

1924-
def unique(self,verbose:logging=False):
1939+
def unique(self,verbose:logging=False,crop=False):
19251940
'''Returns all integer labels WITHOUT 0. Must be performed only on a segmentation nii'''
1926-
out = np_unique_withoutzero(self.get_seg_array())
1941+
1942+
arr = self.get_seg_array()
1943+
if crop:
1944+
try:
1945+
arr = arr[np_bbox_binary(arr)]
1946+
except Exception:
1947+
pass
1948+
out = np_unique_withoutzero(arr)
19271949
log.print(out,verbose=verbose)
19281950
return out
19291951
def voxel_volume(self):

TPTBox/core/nii_wrapper_math.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,15 @@ def mean(self,axis = None,keepdims=False,where = np._NoValue, **qargs)->float:
184184
where=where.get_array().astype(bool)
185185

186186
return np.mean(self.get_array(),axis=axis,keepdims=keepdims,where=where,**qargs)
187-
def median(self,axis = None,keepdims=False,where = np._NoValue, **qargs)->float: # type: ignore
187+
def median(self, axis=None, keepdims=False, **qargs): # type: ignore
188+
arr = self.get_array()
189+
return np.median(arr, axis=axis, keepdims=keepdims, **qargs)
190+
191+
def std(self,axis = None,keepdims=False,where = np._NoValue, **qargs)->float: # type: ignore
188192
if hasattr(where,"get_array"):
189193
where=where.get_array().astype(bool)
190194

191-
return np.median(self.get_array(),axis=axis,keepdims=keepdims,where=where,**qargs)
195+
return np.std(self.get_array(),axis=axis,keepdims=keepdims,where=where,**qargs)
192196
def threshold(self,threshold=0.5, inplace=False):
193197
arr = self.get_array()
194198
arr2 = arr.copy()

0 commit comments

Comments
 (0)