Skip to content

Commit f70abba

Browse files
committed
Merge branch 'development_robert' of https://github.com/Hendrik-code/TPTBox into development_robert
2 parents e169b4e + a9b6e6e commit f70abba

9 files changed

Lines changed: 126 additions & 33 deletions

File tree

TPTBox/core/bids_constants.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@
163163
"localizer",
164164
"difference",
165165
"labels",
166-
"pet"
166+
"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"]
@@ -222,7 +223,7 @@
222223
"OPT": "Ophthalmic Tomography",
223224
"OPV": "Ophthalmic Visual Field",
224225
"OSS": "Optical Surface Scan",
225-
"OT": "Other ",
226+
"OT": "Other",
226227
"PLAN": "Plan",
227228
"PR": "Presentation State",
228229
"PT": "Positron emission tomography (PET)",

TPTBox/core/bids_files.py

Lines changed: 1 addition & 1 deletion
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]

TPTBox/core/dicom/dicom_extract.py

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,68 @@ def dicom_to_nifti_multiframe(ds, nii_path):
190190
return nii_path
191191

192192

193-
def _convert_to_nifti(dicom_out_path, nii_path):
193+
def _export_pdf_from_dicom(dcm_path, out_pdf):
194+
assert len(dcm_path) == 1, dcm_path
195+
ds = dcm_path[0]
196+
197+
# verify modality / SOP class
198+
if ds.Modality.upper() != "PDF":
199+
raise ValueError("Not a PDF DICOM")
200+
201+
if "EncapsulatedDocument" not in ds:
202+
raise ValueError("No embedded PDF found")
203+
204+
pdf_bytes = ds.EncapsulatedDocument
205+
206+
out_pdf = Path(out_pdf)
207+
out_pdf.write_bytes(pdf_bytes)
208+
209+
210+
def _collect_text(ds, txt_lines: list[str] | None = None):
211+
if txt_lines is None:
212+
txt_lines = []
213+
214+
def _help_collect_text(content_sequence, level: int = 0):
215+
for item in content_sequence:
216+
prefix = " " * level
217+
218+
concept = ""
219+
220+
if hasattr(item, "ConceptNameCodeSequence"):
221+
try:
222+
concept = item.ConceptNameCodeSequence[0].CodeMeaning
223+
except Exception:
224+
pass
225+
226+
value = None
227+
228+
for attr in ["TextValue", "CodeMeaning", "NumericValue"]:
229+
if hasattr(item, attr):
230+
value = getattr(item, attr)
231+
break
232+
233+
if concept or value is not None:
234+
txt_lines.append(f"{prefix}{concept}: {value}")
235+
236+
if hasattr(item, "ContentSequence"):
237+
_help_collect_text(
238+
item.ContentSequence,
239+
level + 1,
240+
)
241+
242+
if hasattr(ds, "ContentSequence"):
243+
_help_collect_text(ds.ContentSequence)
244+
return txt_lines
245+
246+
247+
def _extract_txt_from_dicom(dcm_path, out_txt):
248+
lines = []
249+
for p in dcm_path:
250+
lines = _collect_text(p, lines)
251+
Path(out_txt).write_text("\n".join(lines))
252+
253+
254+
def _extract_nii_from_dicom(dicom_out_path, nii_path):
194255
"""
195256
Convert DICOM files to NIfTI format and handle common conversion errors.
196257
@@ -206,6 +267,7 @@ def _convert_to_nifti(dicom_out_path, nii_path):
206267
FunctionTimedOut: Raised if the DICOM-to-NIfTI conversion times out.
207268
ValueError: Raised for generic validation failures.
208269
"""
270+
209271
try:
210272
if isinstance(dicom_out_path, list):
211273
try:
@@ -217,6 +279,7 @@ def _convert_to_nifti(dicom_out_path, nii_path):
217279
return True
218280
except Exception as e:
219281
logger.on_debug("Multi-Frame DICOM did not work:", e)
282+
## The PDF dicom lands here
220283
convert_dicom.dicom_array_to_nifti(dicom_out_path, nii_path, True)
221284
else:
222285
# func_timeout(10, dicom2nifti.dicom_series_to_nifti, (dicom_out_path, nii_path, True))
@@ -246,6 +309,9 @@ def _convert_to_nifti(dicom_out_path, nii_path):
246309
logger.print_error()
247310

248311
return False
312+
except Exception:
313+
print(nii_path)
314+
249315
return True
250316

251317

@@ -264,7 +330,7 @@ def _get_paths(
264330
):
265331
if keys is None:
266332
keys = {}
267-
(mri_format, keys) = extract_keys_from_json(
333+
(mri_format, keys, ending) = extract_keys_from_json(
268334
simp_json,
269335
dcm_data_l,
270336
use_session,
@@ -277,7 +343,7 @@ def _get_paths(
277343
json_file_name, json_bids_name = _generate_bids_path(
278344
dataset_nifti_dir, keys, mri_format, simp_json, make_subject_chunks=make_subject_chunks, parent=parent
279345
)
280-
nii_path = str(json_file_name).replace(".json", "") + ".nii.gz"
346+
nii_path = str(json_file_name).replace(".json", "") + ending
281347
return json_file_name, json_bids_name, nii_path
282348

283349

@@ -364,11 +430,17 @@ def _from_dicom_to_nii(
364430
if exist and Path(nii_path).exists():
365431
logger.print("already exists:", json_file_name, ltype=Log_Type.STRANGE, verbose=verbose)
366432
return nii_path
367-
suc = _convert_to_nifti(dcm_data_l, nii_path)
433+
add_grid = False
434+
if nii_path.endswith(".pdf"):
435+
_export_pdf_from_dicom(dcm_data_l, nii_path)
436+
elif nii_path.endswith(".txt"):
437+
_extract_txt_from_dicom(dcm_data_l, nii_path)
438+
else:
439+
add_grid = _extract_nii_from_dicom(dcm_data_l, nii_path)
368440

369-
if suc:
441+
if add_grid:
370442
_add_grid_info_to_json(nii_path, json_file_name)
371-
return nii_path if suc else None
443+
return nii_path if add_grid else None
372444

373445

374446
def _add_grid_info_to_json(nii_path: Path | str, simp_json: Path | str, force_update=False, add=True):

TPTBox/core/dicom/dicom_header_to_keys.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,15 @@ def extract_keys_from_json( # noqa: C901
165165
def _get(key, default=None):
166166
if key not in simp_json:
167167
return keys.get(key, default)
168-
return str(simp_json[key]).replace("_", "-").replace(" ", "-").replace(".", "-")
168+
value = str(simp_json[key]).replace("_", "-").replace(" ", "-").replace(".", "-")
169+
# remove invalid filename characters
170+
value = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "", value)
171+
# collapse repeated dashes
172+
value = re.sub(r"-+", "-", value)
173+
# strip leading/trailing dots and dashes
174+
value = value.strip(".-")
175+
176+
return value
169177

170178
"""Extract keys from JSON based on study and series descriptions."""
171179
#### NAKO FIXED ####
@@ -174,25 +182,28 @@ def _get(key, default=None):
174182
series_description = _get("SeriesDescription", "unnamed")
175183
"""Determine the MRI format based on the series description."""
176184
if "T2_TSE" in series_description:
177-
return "T2w", {"acq": "sag", "chunk": series_description.split("_")[-1], "sequ": simp_json["SeriesNumber"], **keys}
185+
return "T2w", {"acq": "sag", "chunk": series_description.split("_")[-1], "sequ": simp_json["SeriesNumber"], **keys}, ".nii.gz"
178186
elif "3D_GRE_TRA" in series_description:
179-
return "vibe", {
180-
"acq": "ax",
181-
"part": dixon_mapping[series_description.split("_")[-1].lower()],
182-
"chunk": _get("ProtocolName", "unnamed").split("_")[-1],
183-
**keys,
184-
}
187+
return (
188+
"vibe",
189+
{
190+
"acq": "ax",
191+
"part": dixon_mapping[series_description.split("_")[-1].lower()],
192+
"chunk": _get("ProtocolName", "unnamed").split("_")[-1],
193+
**keys,
194+
},
195+
".nii.gz",
196+
)
185197
elif "ME_vibe" in series_description:
186-
return "mevibe", {
187-
"acq": "ax",
188-
"part": dixon_mapping[series_description.split("_")[-1].lower()],
189-
"sequ": simp_json["SeriesNumber"],
190-
**keys,
191-
}
198+
return (
199+
"mevibe",
200+
{"acq": "ax", "part": dixon_mapping[series_description.split("_")[-1].lower()], "sequ": simp_json["SeriesNumber"], **keys},
201+
".nii.gz",
202+
)
192203
elif "PD" in series_description:
193-
return "pd", {"acq": "iso", **keys}
204+
return "pd", {"acq": "iso", **keys}, ".nii.gz"
194205
elif "T2_HASTE" in series_description:
195-
return "T2haste", {"acq": "ax", **keys}
206+
return "T2haste", {"acq": "ax", **keys}, ".nii.gz"
196207
else:
197208
raise NotImplementedError(series_description)
198209
# GENERAL
@@ -321,9 +332,14 @@ def _get(key, default=None):
321332
" km " in series_description.lower() or series_description.startswith("km") or series_description.endswith("km")
322333
) and keys.get("ce") is None:
323334
keys["ce"] = "ContrastAgent"
335+
elif modality.lower() == "pdf":
336+
return "report", keys, ".pdf"
337+
elif modality.lower() == "sr":
338+
keys["desc"] = _get("SeriesDescription", None)
339+
return "report", keys, ".txt"
324340
else:
325-
raise NotImplementedError(f"modality='{modality.upper()}', ({modalities.get(modality.upper())})")
341+
raise NotImplementedError(f"modality='{modality}', ({modalities.get(modality.upper(), 'Non Standard Modality key')})")
326342

327343
# ".*sub.*t1.*": "subtraktion",
328344
# "subtraktion.*t1.*": "subtraktion",
329-
return mri_format, keys
345+
return mri_format, keys, ".nii.gz"

TPTBox/core/nii_wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ def resample_from_to(self, to_vox_map:Image_Reference|Has_Grid|tuple[SHAPE,AFFIN
985985
# padding after = remaining dst size after src
986986
pad_after = dst_shape-shift-src_shape
987987
pad = tuple((int(b), int(a)) for b, a in zip(pad_before, pad_after))
988-
ret = s.apply_pad(pad, mode=mode,)
988+
ret = s.apply_pad(pad, mode=mode,inplace=inplace,verbose=verbose)
989989

990990
#TODO SET raise_error=False before committing
991991
valid = ret.assert_affine(mapping,raise_error=True,origin_tolerance=0.0001,error_tolerance=0.0001,shape_tolerance=0)
@@ -1801,7 +1801,7 @@ def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|s
18011801
"""
18021802
self.assert_affine(reference_mask)
18031803
if _do_crop:
1804-
crop = reference_mask.compute_crop(0,5)
1804+
crop = reference_mask.compute_crop(0,5,raise_error=False)
18051805
s = self.apply_crop(crop)
18061806
reference_mask = reference_mask.apply_crop(crop)
18071807
else:

TPTBox/segmentation/VibeSeg/inference_nnunet.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def to_int(a: str, k: None | int = None):
227227
try:
228228
return enum_[a].value
229229
except Exception:
230-
print("no ", enum_)
230+
pass
231231
if k is not None and k not in unknown_strings.values():
232232
return k
233233
unknown_strings[a] = unknown_strings["max"]
@@ -247,7 +247,7 @@ def to_int(a: str, k: None | int = None):
247247
logger.print(f"{mapping=}")
248248
seg_nii.map_labels_(mapping)
249249
if out_file is not None and (not Path(out_file).exists() or override):
250-
seg_nii.save(out_file)
250+
seg_nii.set_dtype("smallest_uint").save(out_file)
251251
del nnunet
252252

253253
torch.cuda.empty_cache()

TPTBox/stitching/stitching.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ def main( # noqa: C901
324324
is_segmentation: bool = False,
325325
dtype: type | str = float,
326326
save=True,
327+
ramp_path=None,
327328
):
328329
np.set_printoptions(precision=2, floatmode="fixed")
329330
if is_segmentation:
@@ -529,7 +530,8 @@ def main( # noqa: C901
529530
occupancy_arr = occupancy_arr[ex_slice]
530531
assert output is not None
531532
nii_occ = set_array(nii_out, occupancy_arr)
532-
output = output.replace(".nii.gz", "_ramps.nii.gz")
533+
nii_occ.set_data_dtype(np.int8)
534+
output = output.replace(".nii.gz", "_ramps.nii.gz").replace("_msk_", "_") if ramp_path is None else ramp_path
533535
if save:
534536
nib.save(nii_occ, output) # type: ignore
535537
print("Saved ", output) if verbose else None

TPTBox/stitching/stitching_tools.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def stitching(
2222
dtype: type = float,
2323
match_histogram=False,
2424
store_ramp=False,
25+
ramp_path=None,
2526
):
2627
out = str(out.file["nii.gz"]) if isinstance(out, BIDS_FILE) else str(out)
2728
files = [to_nii(bf).nii for bf in bids_files]
@@ -37,6 +38,7 @@ def stitching(
3738
kick_out_fully_integrated_images=kick_out_fully_integrated_images,
3839
is_segmentation=is_seg,
3940
dtype=dtype,
41+
ramp_path=ramp_path,
4042
)
4143

4244

unit_tests/test_stiching.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_stitching(
7878
store_ramp=False,
7979
verbose=False,
8080
min_value=0,
81-
bias_field=True,
81+
bias_field=False,
8282
crop_to_bias_field=False,
8383
crop_empty=False,
8484
histogram=None,

0 commit comments

Comments
 (0)