Skip to content

Commit a9b6e6e

Browse files
committed
Merge branch 'development_robert' of github.com:Hendrik-code/TPTBox into development_robert
2 parents de3a52a + 6d6654d commit a9b6e6e

16 files changed

Lines changed: 200 additions & 82 deletions

TPTBox/core/bids_constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"MP2RAG",
3535
"MPM",
3636
"MT",
37-
"MT",
37+
"MTS",
3838
"T1map",
3939
"T2map",
4040
"T2starmap",
@@ -164,6 +164,7 @@
164164
"difference",
165165
"labels",
166166
"report",
167+
"pet",
167168
]
168169
# https://bids-specification.readthedocs.io/en/stable/appendices/entity-table.html
169170
formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "mr", "snapshot", "t1dixon", "dwi", "ctb"]

TPTBox/core/bids_files.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ def get_changed_bids(
687687
auto_add_run_id=False,
688688
additional_folder: str | None = None,
689689
dataset_path: str | None = None,
690-
make_parent=True,
690+
make_parent=False,
691691
non_strict_mode=False,
692692
):
693693
ds = dataset_path if dataset_path is not None else self.get_path_decomposed()[0]
@@ -1108,6 +1108,22 @@ def get_frame_of_reference_uid(self, default=None):
11081108
base36 = chars[i] + base36
11091109
return base36[:length]
11101110

1111+
def get_identifier(self, sequence_splitting_keys: list[str]) -> str:
1112+
"""Generates an identifier for the BIDS_FILE based on subject and splitting keys
1113+
1114+
Args:
1115+
sequence_splitting_keys (list[str]): list of keys to use for splitting
1116+
"""
1117+
if "sub" not in self.info:
1118+
print(f"family_id, no sub-key, got {self.info}")
1119+
identifier = "sub-404"
1120+
else:
1121+
identifier = "sub-" + self.info["sub"]
1122+
for s in self.info.keys():
1123+
if s in sequence_splitting_keys:
1124+
identifier += "_" + s + "-" + self.info[s]
1125+
return identifier
1126+
11111127

11121128
class Searchquery:
11131129
def __init__(self, subj: Subject_Container, flatten=False) -> None:
@@ -1468,15 +1484,16 @@ def __lt__(self, other):
14681484

14691485
def get_identifier(self):
14701486
first_e = self.data_dict[next(iter(self.data_dict.keys()))][0]
1471-
if "sub" not in first_e.info:
1472-
print(f"family_id, no sub-key, got {first_e.info} and data_dict {list(self.data_dict.keys())}")
1473-
identifier = "sub-404"
1474-
else:
1475-
identifier = "sub-" + first_e.info["sub"]
1476-
for s in first_e.info.keys():
1477-
if s in self.sequence_splitting_keys:
1478-
identifier += "_" + s + "-" + first_e.info[s]
1479-
return identifier
1487+
return first_e.get_identifier(self.sequence_splitting_keys)
1488+
# if "sub" not in first_e.info:
1489+
# print(f"family_id, no sub-key, got {first_e.info} and data_dict {list(self.data_dict.keys())}")
1490+
# identifier = "sub-404"
1491+
# else:
1492+
# identifier = "sub-" + first_e.info["sub"]
1493+
# for s in first_e.info.keys():
1494+
# if s in self.sequence_splitting_keys:
1495+
# identifier += "_" + s + "-" + first_e.info[s]
1496+
# return identifier
14801497

14811498
def items(self):
14821499
return self.data_dict.items()

TPTBox/core/dicom/dicom_header_to_keys.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ def _get(key, default=None):
279279
found = False
280280
if modality == "ct":
281281
mri_format = "ct"
282+
elif modality.lower() == "pt":
283+
mri_format = "pet"
282284
elif modality == "xa": # Angiography
283285
biplane = False
284286
if "BIPLANE A" in image_type or "SINGLE A" in image_type:

TPTBox/core/nii_poi_abstract.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,12 @@ def assert_affine(
305305

306306
# Print errors
307307
for err in found_errors:
308-
log.print(err, ltype=Log_Type.FAIL, verbose=verbose)
308+
text2 = f"{text}; {err}" if text else f"{err}"
309+
log.print(f"{text2}", ltype=Log_Type.FAIL, verbose=verbose)
309310
# Final conclusion and possible raising of AssertionError
310311
has_errors = len(found_errors) > 0
311312
if raise_error and has_errors:
312-
raise AssertionError(f"{text}; assert_affine failed with {found_errors}")
313+
raise AssertionError(f"{text} assert_affine failed with {found_errors}")
313314

314315
return not has_errors
315316

TPTBox/core/nii_wrapper.py

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,9 @@ def dtype(self)->type:
413413
if self.__unpacked:
414414
return self._arr.dtype # type: ignore
415415
return self.nii.dataobj.dtype #type: ignore
416+
@dtype.setter
417+
def dtype(self, dtype:type):
418+
self.set_dtype_(dtype)
416419
@property
417420
def header(self) -> Nifti1Header:
418421
if self.__unpacked:
@@ -433,6 +436,9 @@ def affine(self,affine:np.ndarray):
433436
def orientation(self) -> AX_CODES:
434437
ort = nio.io_orientation(self.affine)
435438
return nio.ornt2axcodes(ort) # type: ignore
439+
@orientation.setter
440+
def orientation(self, value: AX_CODES):
441+
self.reorient_(value, verbose=False)
436442
@property
437443
def dims(self)->int:
438444
self._unpack()
@@ -448,6 +454,9 @@ def zoom(self) -> ZOOMS:
448454
z = z[:n]
449455
#assert len(z) == 3,z
450456
return z # type: ignore
457+
@zoom.setter
458+
def zoom(self, value: tuple[float, float, float]):
459+
self.rescale_(value, verbose=False)
451460

452461
@property
453462
def origin(self) -> tuple[float, float, float]:
@@ -482,10 +491,6 @@ def direction_itk(self) -> list:
482491
a[:len(a)//3*2]*=-1
483492
return a.tolist()
484493

485-
@orientation.setter
486-
def orientation(self, value: AX_CODES):
487-
self.reorient_(value, verbose=False)
488-
489494

490495
def split_4D_image_to_3D(self):
491496
assert self.get_num_dims() == 4,self.get_num_dims()
@@ -799,9 +804,7 @@ def apply_pad(
799804
mode: MODES = "constant",
800805
inplace=False,
801806
verbose: logging = True
802-
):
803-
#TODO add other modes
804-
#TODO add testcases and options for modes
807+
):
805808
if padd is None or padd == 0:
806809
return self if inplace else self.copy()
807810

@@ -824,13 +827,36 @@ def apply_pad(
824827

825828
affine = self.affine @ transform
826829

830+
arr = self.get_array()
831+
832+
# ---- 1. CROPPING (negative padding) ----
833+
slices = []
834+
835+
for i, (before, after) in enumerate(padd[:self.dims]):
836+
start = max(0, -before)
837+
end = arr.shape[i] - max(0, -after)
838+
slices.append(slice(start, end))
839+
840+
# keep non-spatial dims unchanged
841+
slices += [slice(None)] * (arr.ndim - self.dims)
842+
843+
arr = arr[tuple(slices)]
844+
845+
# ---- 2. PADDING (positive only) ----
846+
padd_positive = tuple(
847+
(max(0, b), max(0, a)) for b, a in padd
848+
)
849+
827850
args = {}
828851
if mode == "constant":
829852
args["constant_values"] = self.get_c_val()
830853

854+
if mode == "nearest":
855+
mode = "edge"
856+
831857
log.print(f"Padd {padd}; {mode=}, {args}", verbose=verbose)
832858

833-
arr = np.pad(self.get_array(), padd, mode=mode, **args)
859+
arr = np.pad(arr, padd_positive, mode=mode, **args)
834860

835861
nii = (arr, affine, self.header)
836862

@@ -935,35 +961,38 @@ def resample_from_to(self, to_vox_map:Image_Reference|Has_Grid|tuple[SHAPE,AFFIN
935961
mapping = to_vox_map.to_gird()
936962
else:
937963
mapping = to_vox_map if isinstance(to_vox_map, tuple) else to_nii_optional(to_vox_map, seg=self.seg, default=to_vox_map)
938-
if isinstance(mapping,Has_Grid) and mapping.assert_affine(self,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0):
939-
log.print(f"resample_from_to skipped; already in space: {self}",verbose=verbose)
940-
return self if inplace else self.copy()
941-
942-
#m1 = mapping.make_empty_POI().reorient(self.orientation)
943-
#if m1.assert_affine(self,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0):
944-
# log.print(f"resample_from_to only need reorientation; {self.orientation}",verbose=verbose)
945-
# return self.reorient(mapping.orientation,inplace=inplace)
946-
#if self.orientation == mapping.orientation and self.zoom == mapping.zoom:
947-
# shift = (np.array(self.origin) - np.array(m1.origin)) / np.array(m1.zoom)
948-
# if np.allclose(shift, np.round(shift), atol=1e-6):
949-
# self = self.reorient(mapping.orientation,inplace=inplace) # noqa: PLW0642
950-
# shift = (np.array(self.origin) - np.array(mapping.origin)) / np.array(mapping.zoom)
951-
# shift = np.round(shift).astype(int)
952-
# src_shape = np.array(mapping.shape)
953-
# dst_shape = np.array(self.shape)
954-
# # padding before = how much dst starts before src
955-
# pad_before = np.maximum(-shift, 0)
956-
#
957-
# # where src ends inside dst
958-
# src_end_in_dst = shift + src_shape
959-
# # padding after = remaining dst size after src
960-
# pad_after = np.maximum(dst_shape - src_end_in_dst, 0)
961-
# pad = tuple((int(b), int(a)) for b, a in zip(pad_before, pad_after))
962-
# ret = self.apply_pad(pad, mode=mode)
963-
#
964-
# log.print(f"resample_from_to only needs padding/cropping {pad}, ",verbose=verbose,)
965-
# ret.assert_affine(mapping,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0)
966-
# return ret
964+
if isinstance(mapping,Has_Grid):
965+
if mapping.assert_affine(self,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0):
966+
log.print(f"resample_from_to skipped; already in space: {self}",verbose=verbose)
967+
return self if inplace else self.copy()
968+
969+
m1 = mapping if mapping.orientation == self.orientation else mapping.make_empty_POI().reorient(self.orientation)
970+
if m1.assert_affine(self,raise_error=False,origin_tolerance=0.00001,error_tolerance=0.00001,shape_tolerance=0):
971+
log.print(f"resample_from_to only need reorientation; {self.orientation}",verbose=verbose)
972+
ret = self.reorient(mapping.orientation,inplace=inplace)
973+
ret.affine = mapping.affine #remove floating point error
974+
return ret
975+
if self.orientation == mapping.orientation and np.allclose(self.zoom , mapping.zoom, atol=1e-6):
976+
shift = (np.array(self.origin) - np.array(m1.origin)) / np.array(m1.zoom)
977+
if np.allclose(shift, np.round(shift), atol=1e-6):
978+
s = self.reorient(mapping.orientation,inplace=inplace) # noqa: PLW0642
979+
shift = (np.array(self.origin) - np.array(mapping.origin)) / np.array(mapping.zoom)
980+
shift = np.round(shift).astype(int)
981+
dst_shape = np.array(mapping.shape)
982+
src_shape = np.array(s.shape)
983+
# padding before = how much dst starts before src
984+
pad_before = shift
985+
# padding after = remaining dst size after src
986+
pad_after = dst_shape-shift-src_shape
987+
pad = tuple((int(b), int(a)) for b, a in zip(pad_before, pad_after))
988+
ret = s.apply_pad(pad, mode=mode,inplace=inplace,verbose=verbose)
989+
990+
#TODO SET raise_error=False before committing
991+
valid = ret.assert_affine(mapping,raise_error=True,origin_tolerance=0.0001,error_tolerance=0.0001,shape_tolerance=0)
992+
if valid:
993+
log.print(f"resample_from_to only needs padding/cropping {pad}",verbose=verbose)
994+
ret.affine = mapping.affine #remove floating point error
995+
return ret
967996

968997

969998
assert mapping is not None
@@ -1772,7 +1801,7 @@ def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|s
17721801
"""
17731802
self.assert_affine(reference_mask)
17741803
if _do_crop:
1775-
crop = reference_mask.compute_crop(0,5)
1804+
crop = reference_mask.compute_crop(0,5,raise_error=False)
17761805
s = self.apply_crop(crop)
17771806
reference_mask = reference_mask.apply_crop(crop)
17781807
else:

TPTBox/core/np_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ def np_calc_crop_around_centerpoint(
563563
arr: np.ndarray,
564564
cutout_size: tuple[int, ...],
565565
pad_to_size: Sequence[int] | np.ndarray | int = 0,
566-
) -> tuple[np.ndarray, tuple, tuple]:
566+
) -> tuple[np.ndarray, tuple[slice, slice, slice], tuple]:
567567
"""
568568
569569
Args:
@@ -944,7 +944,7 @@ def np_get_connected_components_center_of_mass(
944944
connectivity=connectivity,
945945
label_ref=label,
946946
)
947-
coms = list(np_center_of_mass(subreg_cc[label]).values())
947+
coms = list(np_center_of_mass(subreg_cc[label]).values()) if label in subreg_cc else None
948948

949949
if sort_by_axis is not None:
950950
coms.sort(key=lambda a: a[sort_by_axis])
@@ -1050,7 +1050,7 @@ def np_fill_holes(
10501050
else:
10511051
assert 0 <= slice_wise_dim <= arr.ndim - 1, f"slice_wise_dim needs to be in range [0, {arr.ndim - 1}]"
10521052
filled = np.swapaxes(arr_lc.copy(), 0, slice_wise_dim)
1053-
filled = np.stack([_fill(x) for x in filled])
1053+
filled = np.stack([_fill(x).astype(arr.dtype) for x in filled])
10541054
filled = np.swapaxes(filled, 0, slice_wise_dim)
10551055
filled[filled != 0] = l
10561056
if use_crop:

TPTBox/core/poi.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -945,10 +945,10 @@ def calc_poi_from_subreg_vert(
945945
save_buffer_file=False, # used by wrapper # noqa: ARG001
946946
decimals=2,
947947
subreg_id: int | Abstract_lvl | Sequence[int | Abstract_lvl] | Sequence[Abstract_lvl] | Sequence[int] = 50,
948-
verbose: logging = True,
948+
verbose: logging = False,
949949
extend_to: POI | None = None,
950950
# use_vertebra_special_action=True,
951-
_vert_ids=None,
951+
_vert_ids: list[int] | None = None,
952952
_print_phases=False,
953953
_orientation_version=0,
954954
) -> POI:
@@ -1088,6 +1088,7 @@ def calc_poi_from_subreg_vert(
10881088
subreg_msk,
10891089
_vert_ids=_vert_ids,
10901090
log=log,
1091+
verbose=verbose,
10911092
_orientation_version=_orientation_version,
10921093
)
10931094
extend_to.apply_crop_reverse(crop, org_shape, inplace=True)

TPTBox/core/poi_fun/poi_abstract.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def map_labels_(
441441

442442
def sort(self, inplace=True, order_dict: dict | None = None) -> Self:
443443
"""Sort vertebra dictionary by sorting_list"""
444-
if self.level_one_info is not None:
444+
if self.level_one_info is not None and self.level_one_info != Any:
445445
order_dict = self.level_one_info.order_dict()
446446
poi = self.centroids._sort(inplace=inplace, order_dict=order_dict)
447447
if inplace:

0 commit comments

Comments
 (0)