Skip to content

Commit f639bec

Browse files
authored
Merge pull request #104 from Hendrik-code/development_robert
Development robert
2 parents e51a488 + a2e3c54 commit f639bec

23 files changed

Lines changed: 984 additions & 116 deletions

TPTBox/core/bids_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"FLASH",
6565
"VF",
6666
"defacemas",
67+
"fluroscopy",
6768
"dw",
6869
"TB1TFL",
6970
"TB1RFM",

TPTBox/core/bids_files.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,16 +632,20 @@ def rename_files(self, path: Path | str, ending=".nii.gz"):
632632
p = Path(path + "." + key)
633633
value.rename(p)
634634

635-
def symlink_files(self, path: Path | str, ending=".nii.gz"):
635+
def symlink_files(self, path: Path | str, ending=".nii.gz", exist_ok=False):
636636
ending = ending if ending[0] == "." else "." + ending
637637
path = str(path)
638638
assert path.endswith(ending), f"set 'ending' to the part after the '.'\n {path} does not end with {ending}"
639639
path = path.replace(ending, "")
640640
for key, value in self.file.items():
641641
p = Path(path + "." + key)
642+
642643
if os.path.islink(p):
643644
assert Path(os.readlink(p)) == value, f"{p} exists"
644645
continue
646+
if exist_ok and p.exists():
647+
continue
648+
645649
os.symlink(value, p)
646650

647651
def get_path_decomposed(self, file_type=None) -> tuple[Path, str, str, str]:

TPTBox/core/dicom/dicom2nii_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ def test_name_conflict(json_ob, file):
239239
if Path(file).exists():
240240
with open(file) as f:
241241
js = json.load(f)
242+
if "grid" in js:
243+
del js["grid"]
242244
return js != json_ob
243245
return False
244246

TPTBox/core/dicom/dicom_extract.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,42 @@ def _generate_bids_path(
8888
return fname.file["json"], fname
8989

9090

91+
def dicom_to_nifti_multiframe_2d(ds, nii_path, pixel_array):
92+
if hasattr(ds, "PixelSpacing"):
93+
dy, dx = map(float, ds.PixelSpacing)
94+
affine = np.eye(4)
95+
96+
if hasattr(ds, "ImageOrientationPatient"):
97+
orientation = list(map(float, ds.ImageOrientationPatient))
98+
row_cosines = np.array(orientation[:3])
99+
col_cosines = np.array(orientation[3:])
100+
affine[:3, 0] = row_cosines * dx
101+
affine[:3, 1] = col_cosines * dy
102+
else:
103+
affine[0, 0] = dx
104+
affine[1, 1] = dy
105+
106+
if hasattr(ds, "ImagePositionPatient"):
107+
affine[:3, 3] = np.array(list(map(float, ds.ImagePositionPatient)))
108+
109+
elif hasattr(ds, "ImagerPixelSpacing"):
110+
dy, dx = map(float, ds.ImagerPixelSpacing)
111+
affine = np.diag([-dx, -dy, 1, 1])
112+
113+
else:
114+
affine = np.eye(4)
115+
116+
nii = nib.Nifti1Image(pixel_array.T[:, :, None], affine)
117+
logger.on_log("Save 2D", nii_path)
118+
nib.save(nii, nii_path)
119+
return nii_path
120+
121+
91122
def dicom_to_nifti_multiframe(ds, nii_path):
92123
pixel_array = ds.pixel_array
124+
if len(pixel_array.shape) == 2:
125+
return dicom_to_nifti_multiframe_2d(ds, nii_path, pixel_array)
126+
93127
if len(pixel_array.shape) != 3 and len(pixel_array.shape) != 4:
94128
raise ValueError(f"Expected a shape with 3 colums not {len(pixel_array.shape)}; {pixel_array.shape=}")
95129
n_frames = pixel_array.shape[0]
@@ -265,7 +299,22 @@ def _from_dicom_to_nii(
265299
override_subject_name: Callable[[dict, Path], str] | None = None,
266300
chunk=None,
267301
skip_localizer=False,
302+
parent="rawdata",
303+
censor_list=None,
268304
):
305+
if censor_list is None:
306+
censor_list = [
307+
"StudyDate",
308+
"SeriesDate",
309+
"AcquisitionDate",
310+
"ContentDate",
311+
"StudyTime",
312+
"SeriesTime",
313+
"AcquisitionTime",
314+
"ContentTime",
315+
"InstanceCreationDate",
316+
"InstanceCreationTime",
317+
]
269318
if chunk is None:
270319
splitted_dcm_data_l = _classic_get_grouped_dicoms(dcm_data_l)
271320
if len(splitted_dcm_data_l) != 1:
@@ -282,6 +331,7 @@ def _from_dicom_to_nii(
282331
override_subject_name=override_subject_name,
283332
chunk=i,
284333
skip_localizer=skip_localizer,
334+
parent=parent,
285335
)
286336
outs.append(o)
287337
return outs
@@ -291,6 +341,9 @@ def _from_dicom_to_nii(
291341
return None
292342

293343
simp_json = get_json_from_dicom(dcm_data_l)
344+
for censor_key in censor_list:
345+
if censor_key in simp_json:
346+
del simp_json[censor_key]
294347
json_file_name, json_bids, nii_path = _get_paths(
295348
simp_json,
296349
dcm_data_l,
@@ -301,11 +354,13 @@ def _from_dicom_to_nii(
301354
map_series_description_to_file_format,
302355
override_subject_name,
303356
chunk=chunk,
357+
parent=parent,
304358
)
305359
if skip_localizer and json_bids.bids_format == "localizer":
306360
return
307361
logger.print(json_file_name, Log_Type.NEUTRAL, verbose=verbose)
308-
exist = save_json(simp_json, json_file_name)
362+
exist = save_json(simp_json, json_file_name, override=False)
363+
# logger.on_debug(exist, Path(nii_path).exists(), nii_path)
309364
if exist and Path(nii_path).exists():
310365
logger.print("already exists:", json_file_name, ltype=Log_Type.STRANGE, verbose=verbose)
311366
return nii_path
@@ -520,6 +575,8 @@ def extract_dicom_folder(
520575
n_cpu: int | None = 1,
521576
override_subject_name: Callable[[dict, Path], str] | None = None,
522577
skip_localizer=True,
578+
parent="rawdata",
579+
censor_list: list | None = None,
523580
):
524581
"""
525582
Extract DICOM files from a directory or list of directories, convert them to NIfTI format, and store the output.
@@ -537,6 +594,8 @@ def extract_dicom_folder(
537594
Returns:
538595
dict: A dictionary with keys representing DICOM series and values as paths to the generated NIfTI files.
539596
"""
597+
if censor_list is None:
598+
censor_list = []
540599
if not validate_slicecount:
541600
convert_dicom.settings.disable_validate_slicecount()
542601
if not validate_orientation:
@@ -576,6 +635,8 @@ def process_series(key, files, parts):
576635
map_series_description_to_file_format=map_series_description_to_file_format,
577636
override_subject_name=override_subject_name,
578637
skip_localizer=skip_localizer,
638+
parent=parent,
639+
censor_list=censor_list,
579640
)
580641

581642
# Process in parallel or sequentially based on n_cpu
@@ -606,8 +667,10 @@ def process_series(key, files, parts):
606667

607668

608669
if __name__ == "__main__":
609-
for p in Path("/DATA/NAS/datasets_source/brain/dsa").iterdir():
610-
extract_dicom_folder(p, Path("/DATA/NAS/datasets_source/brain/", "dataset-DSA"), False, False, validate_slice_increment=False)
670+
for p in Path("/media/robert/STORE N GO/DSA_Daten/").iterdir():
671+
extract_dicom_folder(
672+
p, Path("/media/data/robert/datasets", "dataset-Durchleuchtung222"), False, False, validate_slice_increment=False
673+
)
611674

612675
sys.exit()
613676
# s = "/home/robert/Downloads/bein/dataset-oberschenkel/rawdata/sub-1-3-46-670589-11-2889201787-2305829596-303261238-2367429497/mr/sub-1-3-46-670589-11-2889201787-2305829596-303261238-2367429497_sequ-406_mr.nii.gz"

TPTBox/core/dicom/dicom_header_to_keys.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
"t2w?_fse.*": "T2w",
4747
".*t1w?_tse.*": "T1w",
4848
".*t1w?_vibe_tra.*": "vibe",
49+
".*Durchleuchtung.*": "fluroscopy",
50+
".*fluroscopy.*": "fluroscopy",
4951
".*scout": "localizer",
5052
"localizer": "localizer",
5153
".*pilot.*": "localizer",
@@ -267,21 +269,33 @@ def _get(key, default=None):
267269
if modality == "ct":
268270
mri_format = "ct"
269271
elif modality == "xa": # Angiography
272+
biplane = False
270273
if "BIPLANE A" in image_type or "SINGLE A" in image_type:
271274
keys["acq"] = "A"
275+
biplane = True
272276
elif "BIPLANE B" in image_type or "SINGLE B" in image_type:
273277
keys["acq"] = "B"
278+
biplane = True
279+
derived = "DERIVED" in image_type
280+
series_description = _get("SeriesDescription", " ").lower() # "SeriesDescription": "Durchleuchtung - gespeichert",
274281
monitor = _get("PositionerMotion", " ").lower()
275282
# ftv = _get("FrameTimeVector", None).lower()
276283
monitor = _get("PositionerMotion", " ").lower()
277284
tag = _get("DerivationDescription", " ").lower()
285+
# "ImagerPixelSpacing"
286+
# FrameTimeVector = _get("DerivationDescription", [])
278287
# ftv is not None
279-
if tag == "subtraction":
288+
if "durchleuchtung" in series_description or "fluroscopy" in series_description:
289+
mri_format = "fluroscopy"
290+
elif tag == "subtraction":
280291
mri_format = "DSA" if monitor == "static" and "VOLUME" not in image_type and "RECON" not in image_type else "subtraction"
281292
elif "3DRA_PROP" in image_type:
282293
mri_format = "3DRA"
283294
elif monitor == "dynamic" or "VOLUME" in image_type or "RECON" in image_type or "3DRA_PROP" in image_type:
284295
mri_format = "DSA3D"
296+
elif biplane and derived and "VOLUME" not in image_type and "RECON" not in image_type:
297+
##len(FrameTimeVector) >= 1 and (monitor == "static" and "VOLUME" not in image_type and "RECON" not in image_type)
298+
mri_format = "DSA"
285299
else:
286300
mri_format = "XA"
287301
elif modality == "mr":
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import time
2+
3+
import elasticdeform
4+
import numpy as np
5+
from numpy.typing import NDArray
6+
7+
from TPTBox import NII
8+
9+
10+
def deformed_nii(
11+
nii_dic: dict[str, NII],
12+
sigma: float | None = None,
13+
points=None,
14+
deform_factor=1.0,
15+
deform_padding=10,
16+
normalize=True,
17+
joint_normalize=False,
18+
) -> dict[str, NII]:
19+
"""
20+
Deform a dictionary of NII objects using random grid deformation. Requires elasticdeform. 'pip install elasticdeform'
21+
22+
IMPORTANT: Normalize your image data to 0,1. The .seg property of NII shows if this is a segmentation. (NII is form our TPTBox and is a wrapper for nibable)
23+
24+
This function takes a dictionary of NII objects and applies random grid deformation to each object
25+
using specified deformation parameters or, if not provided, random parameters generated based on
26+
the `deform_factor`. The deformed objects are returned as a dictionary.
27+
28+
Args:
29+
arr_dic (dict[str, NII]): A dictionary containing NII objects to be deformed.
30+
sigma (float, optional): The standard deviation of the deformation field. If not provided,
31+
it will be generated based on the `deform_factor`.
32+
points (int, optional): The number of control points for the deformation grid. If not provided,
33+
it will be generated based on the `deform_factor`.
34+
deform_factor (float, optional): A factor used to determine the deformation parameters if
35+
`sigma` and `points` are not specified. Larger values result in stronger deformations.
36+
deform_padding (int, optional): The padding added to the deformed objects to avoid edge artifacts.
37+
verbose (bool, optional): If True, enable verbose logging. Default is True.
38+
39+
Returns:
40+
dict[str, NII]: A dictionary where keys correspond to the input dictionary keys, and values
41+
correspond to the deformed NII objects.
42+
43+
Example:
44+
# Deform a dictionary of NII objects using default deformation parameters
45+
deformed_data = deformed_NII(arr_dic)
46+
47+
# Deform a dictionary of NII objects with specific deformation parameters
48+
sigma = 1.0
49+
points = 20
50+
deformed_data = deformed_NII(arr_dic, sigma=sigma, points=points)
51+
"""
52+
if sigma is None or points is None:
53+
sigma, points = get_random_deform_parameter(deform_factor=deform_factor)
54+
55+
print("deformation parameter sigma = ", round(sigma, 4), "; n_points = ", points)
56+
t = time.time()
57+
values = list(nii_dic.values())
58+
# Deform
59+
if joint_normalize:
60+
max_v = max([img.max() for img in nii_dic.values() if not img.seg])
61+
nii_dic = {k: img if img.seg else img.set_dtype(np.float32) / max_v for k, img in nii_dic.items()}
62+
elif normalize:
63+
nii_dic = {k: img if img.seg else img.set_dtype(np.float32).normalize() for k, img in nii_dic.items()}
64+
else:
65+
nii_dic = {k: img if img.seg else img.set_dtype(np.float32) for k, img in nii_dic.items()}
66+
assert sigma is not None
67+
p = deform_padding
68+
out: list[NDArray] = elasticdeform.deform_random_grid(
69+
[pad(v.get_array(), p=p) for v in values],
70+
sigma=sigma, # type: ignore
71+
points=points,
72+
order=[0 if v.seg else 3 for v in values], # type: ignore
73+
)
74+
out2: dict[str, NII] = {}
75+
for (k, nii), arr in zip(nii_dic.items(), out, strict=True):
76+
out2[k] = nii.set_array(arr[p:-p, p:-p, p:-p])
77+
print("Deformation took", round(time.time() - t, 1), "Seconds")
78+
return out2
79+
80+
81+
def pad(arr, p=10):
82+
return np.pad(arr, p, mode="reflect")
83+
84+
85+
def get_random_deform_parameter(deform_factor: float = 1):
86+
"""
87+
Generate random deformation parameters for use in 3D deformation.
88+
89+
This function generates random values for the deformation parameters, including 'sigma' and 'points',
90+
based on the specified deformation factor. These parameters are used for 3D deformation operations.
91+
92+
Args:
93+
deform_factor (float, optional): A factor to control the strength of deformation. Default is 1.
94+
95+
Returns:
96+
tuple[float, int]: A tuple containing the generated 'sigma' (float) and 'points' (int) parameters.
97+
98+
Example:
99+
# Generate random deformation parameters with a deformation factor of 1
100+
sigma, points = get_random_deform_parameter()
101+
102+
# Generate random deformation parameters with a deformation factor of 2
103+
sigma, points = get_random_deform_parameter(deform_factor=2)
104+
"""
105+
sigma = 2 + np.random.uniform() * 2.5 # 1,5 - 4.5
106+
min_points = 3
107+
max_points = 17
108+
if sigma < 2:
109+
max_points = 17
110+
elif sigma < 1.7:
111+
max_points = 16
112+
elif sigma < 2.1:
113+
max_points = 15
114+
elif sigma < 2.3:
115+
max_points = 14
116+
elif sigma < 2.5:
117+
max_points = 13
118+
elif sigma < 2.6:
119+
max_points = 12
120+
elif sigma < 2.7:
121+
max_points = 11
122+
elif sigma < 2.8:
123+
max_points = 10
124+
elif sigma < 3:
125+
max_points = 9
126+
elif sigma < 3.5:
127+
max_points = 8
128+
elif sigma < 4.0:
129+
max_points = 7
130+
elif sigma < 4.3:
131+
max_points = 6
132+
else:
133+
max_points = 5
134+
points = np.random.randint(max_points - min_points + 1) + min_points
135+
# Stronger
136+
sigma *= deform_factor
137+
# points *= deform_factor
138+
points = max(round(points), 1)
139+
return (sigma, points)

0 commit comments

Comments
 (0)