Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ba8f9f1
Merge branch 'development_robert' of github.com:Hendrik-code/TPTBox i…
robert-graf Jul 24, 2025
56ddf76
improved defaults
robert-graf Jul 31, 2025
24a9cd7
add options to some functions (raise error, 2d infect, better defautls)
robert-graf Sep 1, 2025
cd20aa8
update constance Full Body
robert-graf Sep 1, 2025
f45386c
3.9 comp
robert-graf Sep 1, 2025
34483e9
snapshot3d can now update the resolution correctly
robert-graf Sep 1, 2025
e94f4e7
update max_histroy for early stoping
robert-graf Sep 1, 2025
0307165
add wrapper for file names
robert-graf Sep 1, 2025
a7e1864
update inference
robert-graf Sep 1, 2025
1d3b8e3
Merge branch 'development_robert' of github.com:Hendrik-code/TPTBox i…
robert-graf Sep 1, 2025
dd83121
add better logging
robert-graf Sep 11, 2025
87429c5
add option
robert-graf Sep 11, 2025
d45c49d
Merge branch 'main' into development_robert
robert-graf Sep 11, 2025
0ba8bdc
small changes
robert-graf Oct 1, 2025
d7b4a47
prevent error for unreadable data, like object data
robert-graf Oct 1, 2025
2c72af7
Merge branch 'development_robert' of github.com:Hendrik-code/TPTBox i…
robert-graf Oct 1, 2025
389b305
small bugfixes
robert-graf Oct 14, 2025
c9a198b
Merge branch 'development_robert' of github.com:Hendrik-code/TPTBox i…
robert-graf Oct 14, 2025
532f560
Merge branch 'development_robert' of github.com:Hendrik-code/TPTBox i…
robert-graf Oct 14, 2025
8711979
add 3D dicom supprot
robert-graf Oct 14, 2025
584efea
prevent error when cuda is not installed.
robert-graf Oct 17, 2025
11e4e8a
remove total from name
robert-graf Oct 17, 2025
01a36db
remove oar segmentation. Use CATS instead
robert-graf Oct 17, 2025
2976ba8
add __all__
robert-graf Oct 17, 2025
9cf81df
Merge branch 'main' into development_robert
robert-graf Oct 17, 2025
3332ea6
x
robert-graf Oct 17, 2025
c77ffb2
update tests
robert-graf Oct 17, 2025
889432f
small bug with new name
robert-graf Oct 30, 2025
82a58a4
added new features
robert-graf Nov 28, 2025
a9a3aa7
fix test with updated semantic meaning
robert-graf Nov 29, 2025
d5e1bc9
add 3.9 to fast test
robert-graf Nov 29, 2025
acea6bb
add 3.9 to merge test
robert-graf Nov 29, 2025
37c74e7
remove "total"
robert-graf Nov 29, 2025
67f4798
add init
robert-graf Nov 29, 2025
346e9db
ruff: remove Optional
robert-graf Nov 29, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests_mr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.10"]
python-version: ["3.9","3.12"]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,6 @@ tutorials/tutorial_data_processing/*
tutorials/*PixelPandemonium/*
tutorials/dataset-PixelPandemonium/*
*.html
_*.py
#_*.py
dicom_select
examples
91 changes: 91 additions & 0 deletions TPTBox/core/bids_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,15 @@
"recon",
"reformat",
"subtraction",
"DSA",
"DSA3D",
"3DRA",
"XA",
"RI", # Raw input
"tmax",
"cbv",
"mtt",
"cbf",
"stat",
"snp",
"log",
Expand All @@ -158,6 +166,88 @@
formats_relaxed = [*formats, "t2", "t1", "t2c", "t1c", "cta", "mr", "snapshot", "t1dixon", "dwi"]
# Recommended writing style: T1c, T2c; This list is not official and can be extended.

modalities = {
"AR": "Autorefraction",
"AS": "Angioscopy (Retired)",
"ASMT": "Content Assessment Results",
"AU": "Audio",
"BDUS": "Bone Densitometry (ultrasound)",
"BI": "Biomagnetic imaging",
"BMD": "Bone Densitometry (X-Ray)",
"CD": "Color flow Doppler (Retired)",
"CF": "Cinefluorography (Retired)",
"CP": "Colposcopy (Retired)",
"CR": "Computed Radiography",
"CS": "Cystoscopy (Retired)",
"CT": "Computed Tomography",
"DD": "Duplex Doppler (Retired)",
"DF": "Digital fluoroscopy (Retired)",
"DG": "Diaphanography",
"DM": "Digital microscopy (Retired)",
"DOC": "Document",
"DS": "Digital Subtraction Angiography (Retired)",
"DX": "Digital Radiography",
"EC": "Echocardiography (Retired)",
"ECG": "Electrocardiography",
"EPS": "Cardiac Electrophysiology",
"ES": "Endoscopy",
"FA": "Fluorescein angiography (Retired)",
"FID": "Fiducials",
"FS": "Fundoscopy (Retired)",
"GM": "General Microscopy ",
"HC": "Hard Copy",
"HD": "Hemodynamic Waveform",
"IO": "Intra-Oral Radiography",
"IOL": "Intraocular Lens Data",
"IVOCT": "Intravascular Optical Coherence Tomography",
"IVUS": "Intravascular Ultrasound",
"KER": "Keratometry",
"KO": "Key Object Selection",
"LEN": "Lensometry",
"LP": "Laparoscopy (Retired)",
"LS": "Laser surface scan",
"MA": "Magnetic resonance angiography (Retired)",
"MG": "Mammography",
"MR": "Magnetic Resonance",
"MS": "Magnetic resonance spectroscopy (Retired)",
"NM": "Nuclear Medicine",
"OAM": "Ophthalmic Axial Measurements",
"OCT": "Optical Coherence Tomography (non-Ophthalmic)",
"OP": "Ophthalmic Photography",
"OPM": "Ophthalmic Mapping",
"OPR": "Ophthalmic Refraction (Retired)",
"OPT": "Ophthalmic Tomography",
"OPV": "Ophthalmic Visual Field",
"OSS": "Optical Surface Scan",
"OT": "Other ",
"PLAN": "Plan",
"PR": "Presentation State",
"PT": "Positron emission tomography (PET)",
"PX": "Panoramic X-Ray",
"REG": "Registration",
"RESP": "Respiratory Waveform",
"RF": "Radio Fluoroscopy",
"RG": "Radiographic imaging (conventional film/screen)",
"RTDOSE": "Radiotherapy Dose",
"RTIMAGE": "Radiotherapy Image",
"RTPLAN": "Radiotherapy Plan",
"RTRECORD": "RT Treatment Record",
"RTSTRUCT": "Radiotherapy Structure Set",
"RWV": "Real World Value Map",
"SEG": "Segmentation",
"SM": "Slide Microscopy",
"SMR": "Stereometric Relationship",
"SR": "SR Document",
"SRF": "Subjective Refraction",
"ST": "Single-photon emission computed tomography (SPECT) (Retired)",
"STAIN": "Automated Slide Stainer",
"TG": "Thermography",
"US": "Ultrasound",
"VA": "Visual Acuity",
"VF": "Videofluorography (Retired)",
"XA": "X-Ray Angiography",
"XC": "External-camera Photography",
}

# Actual official final folder
# func (task based and resting state functional MRI)
Expand Down Expand Up @@ -246,6 +336,7 @@
# Others (never used)
"Split": "split",
"Density": "den",
"version": "version",
"Description": "desc",
"nameconflict": "nameconflict",
}
Expand Down
3 changes: 2 additions & 1 deletion TPTBox/core/bids_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,8 @@ def get_changed_path( # noqa: C901
if key in same_info:
continue
if value is not None:
assert validate_entities(key, value, f"..._{key}-{value}_...", True)
if not non_strict_mode:
assert validate_entities(key, value, f"..._{key}-{value}_...", True), f"..._{key}-{value}_..."
final_info[key] = value
# file_name += f"{key}-{value}_"
# sort by order
Expand Down
48 changes: 48 additions & 0 deletions TPTBox/core/dicom/dicom2nii_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import pickle
from copy import deepcopy
from datetime import date
from pathlib import Path

import numpy as np
Expand Down Expand Up @@ -152,9 +153,56 @@ def clean_dicom_data(dcm_data) -> dict:
for tag in ["00291010", "00291020"]:
if tag in py_dict and "InlineBinary" in py_dict[tag]:
del py_dict[tag]["InlineBinary"]
py_dict = replace_birthdate_with_age(py_dict)
return py_dict


def replace_birthdate_with_age(d):
try:
# DICOM tags
BIRTH_TAG = "00100030" # PatientBirthDate
STUDY_DATE_TAG = "00080020" # StudyDate
AGE_TAG = "00101010" # PatientAge

birth_str = d.get(BIRTH_TAG, {}).get("Value", [None])[0]
study_str = d.get(STUDY_DATE_TAG, {}).get("Value", [None])[0]

if not birth_str:
return d # no birth date, nothing to do

# Parse birth date safely
try:
year = int(birth_str[:4])
month = int(birth_str[4:6]) if len(birth_str) >= 6 and birth_str[4:6] != "00" else 6
day = int(birth_str[6:8]) if len(birth_str) == 8 and birth_str[6:8] != "00" else 15
birth_date = date(year, month, day)
except Exception:
return d # invalid date format, skip

# Reference date (study date or today)
try:
ref_date = date(
int(study_str[:4]),
int(study_str[4:6]) if study_str[4:6] != "00" else 6,
int(study_str[6:8]) if study_str[6:8] != "00" else 15,
)
except Exception:
ref_date = date.today()

# Compute integer age
age = ref_date.year - birth_date.year - ((ref_date.month, ref_date.day) < (birth_date.month, birth_date.day))

# Replace PatientBirthDate with PatientAge
d.pop(BIRTH_TAG, None)
d[AGE_TAG] = {
"vr": "AS", # Age String
"Value": [f"{age:03d}Y"], # DICOM age format (e.g. '034Y')
}
except Exception:
pass
return d


def get_json_from_dicom(data: list[pydicom.FileDataset] | pydicom.FileDataset):
if isinstance(data, list):
data = data[0]
Expand Down
94 changes: 90 additions & 4 deletions TPTBox/core/dicom/dicom_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import dicom2nifti
import dicom2nifti.exceptions
import nibabel as nib
import numpy as np
import pydicom
from dicom2nifti import convert_dicom
Expand Down Expand Up @@ -87,6 +88,74 @@ def _generate_bids_path(
return fname.file["json"], fname


def dicom_to_nifti_multiframe(ds, nii_path):
pixel_array = ds.pixel_array
if len(pixel_array.shape) != 3 and len(pixel_array.shape) != 4:
raise ValueError(f"Expected a shape with 3 colums not {len(pixel_array.shape)}; {pixel_array.shape=}")
n_frames = pixel_array.shape[0]

# Pixel spacing (mm)
if hasattr(ds, "PixelSpacing"):
dy, dx = (float(v) for v in ds.PixelSpacing)
# Image orientation (row and column direction cosines)
orientation = [float(v) for v in ds.ImageOrientationPatient]
row_cosines = np.array(orientation[0:3])
col_cosines = np.array(orientation[3:6])
# Normal vector (slice direction)
slice_cosines = np.cross(row_cosines, col_cosines)

# Image position (origin of first slice)
origin = np.array([float(v) for v in ds.ImagePositionPatient])
# Slice spacing - robust: Abstand zwischen Slice 0 und 1
if n_frames > 1:
pos1 = np.array([float(v) for v in ds.PerFrameFunctionalGroupsSequence[0].PlanePositionSequence[0].ImagePositionPatient])
pos2 = np.array([float(v) for v in ds.PerFrameFunctionalGroupsSequence[1].PlanePositionSequence[0].ImagePositionPatient])
dz = np.linalg.norm(pos2 - pos1)
else:
dz = float(getattr(ds, "SpacingBetweenSlices", ds.SliceThickness))

# Affine bauen
affine = np.eye(4)
affine[0:3, 0] = row_cosines * dx
affine[0:3, 1] = col_cosines * dy
affine[0:3, 2] = slice_cosines * dz
affine[0:3, 3] = origin
nii = nib.Nifti1Image(np.transpose(pixel_array, (2, 1, 0)), affine)

elif hasattr(ds, "ImagerPixelSpacing"):
dy, dx = (float(v) for v in ds.ImagerPixelSpacing)
# Einfaches affine (nur 2D + Zeit, keine Lage im Patientenraum)
affine = np.eye(4)
affine[0, 0] = -dx
affine[1, 1] = -dy
nii = nib.Nifti1Image(np.transpose(pixel_array, (2, 1, 0)), affine)

else:
if hasattr(ds, "RelatedSeriesSequence"):
raise NotImplementedError("RelatedSeriesSequence Affine lookup not implemented")
raise NotImplementedError("No spatial metadata found")
### Some could be solved by looking up the "RelatedSeriesSequence"
# "RelatedSeriesSequence": [
# {
# "StudyInstanceUID": "1.2.276.0.38.1.1.1.7712.20250929100319.54200288",
# "SeriesInstanceUID": "1.3.46.670589.7.8.1.6.1403526999.1.9608.1759142950287.2",
# "PurposeOfReferenceCodeSequence": []
# }
# ],
# --- No geometry info (e.g. RGB screen captures or video frames) ---
print("⚠️ No spatial metadata found — assuming pixel size = 1mm and identity orientation.")
affine = np.eye(4)
affine[0, 0] = 1.0
affine[1, 1] = 1.0
affine[2, 2] = 1.0
nii = nib.Nifti1Image(pixel_array, affine)

# Reihenfolge anpassen: Nibabel erwartet (X,Y,Z)
nib.save(nii, nii_path)

return nii_path


def _convert_to_nifti(dicom_out_path, nii_path):
"""
Convert DICOM files to NIfTI format and handle common conversion errors.
Expand All @@ -105,15 +174,25 @@ def _convert_to_nifti(dicom_out_path, nii_path):
"""
try:
if isinstance(dicom_out_path, list):
try:
if len(dicom_out_path) == 1:
ds = dicom_out_path[0]
if hasattr(ds, "pixel_array") and len(ds.pixel_array.shape) >= 2:
dicom_to_nifti_multiframe(ds, nii_path)

return True
except Exception as e:
logger.on_debug("Multi-Frame DICOM did not work:", e)
convert_dicom.dicom_array_to_nifti(dicom_out_path, nii_path, True)
else:
# func_timeout(10, dicom2nifti.dicom_series_to_nifti, (dicom_out_path, nii_path, True))
dicom2nifti.dicom_series_to_nifti(dicom_out_path, nii_path, True)
logger.print("Save ", nii_path, Log_Type.SAVE)
except dicom2nifti.exceptions.ConversionValidationError as e:
if e.args[0] in ["NON_IMAGING_DICOM_FILES"]:
s = f"dicom_array_to_nifti len={len(dicom_out_path)}" if isinstance(dicom_out_path, list) else "dicom_series_to_nifti"
Path(str(nii_path).replace(".nii.gz", ".json")).unlink(missing_ok=True)
logger.on_debug(f"Not exportable '{Path(nii_path).name}':", e.args[0])
logger.on_debug(f"Not exportable '{Path(nii_path).name}':", e.args[0], s)
return False
for key, reason in [
("validate_orthogonal", "NON_CUBICAL_IMAGE/GANTRY_TILT"),
Expand Down Expand Up @@ -311,7 +390,7 @@ def _read_dicom_files(dicom_out_path: Path) -> tuple[dict[str, list[FileDataset]
path = Path(_paths)
if path.is_file():
try:
dcm_data = pydicom.dcmread(path, defer_size="1 KB", force=True)
dcm_data = pydicom.dcmread(path, defer_size="1 KB", force=True) # , stop_before_pixels=True
try:
typ = (
str(dcm_data.get_item((0x0008, 0x0008)).value)
Expand All @@ -324,7 +403,6 @@ def _read_dicom_files(dicom_out_path: Path) -> tuple[dict[str, list[FileDataset]
except Exception:
typ = ""
key1 = str(dcm_data.SeriesInstanceUID)

key = f"{key1}_{typ}"
if not hasattr(dcm_data, "ImageOrientationPatient"):
key += "_" + dcm_data.get("SOPInstanceUID", 0)
Expand Down Expand Up @@ -437,6 +515,7 @@ def extract_dicom_folder(
validate_slicecount=True,
validate_orientation=True,
validate_orthogonal=False,
validate_slice_increment=True,
n_cpu: int | None = 1,
override_subject_name: Callable[[dict, Path], str] | None = None,
skip_localizer=True,
Expand All @@ -463,7 +542,8 @@ def extract_dicom_folder(
convert_dicom.settings.disable_validate_orientation()
if not validate_orthogonal:
convert_dicom.settings.disable_validate_orthogonal()

if not validate_slice_increment:
convert_dicom.settings.disable_validate_slice_increment()
outs = {}

for p in _find_all_files(dicom_folder):
Expand Down Expand Up @@ -512,6 +592,8 @@ def process_series(key, files, parts):
try:
key2, out = process_series(key, files, parts)
outs[key2] = out
except NotImplementedError as e:
logger.on_warning("NotImplementedError:", e)
except Exception:
logger.print_error()

Expand All @@ -523,6 +605,10 @@ def process_series(key, files, parts):


if __name__ == "__main__":
for p in Path("/DATA/NAS/datasets_source/brain/dsa").iterdir():
extract_dicom_folder(p, Path("/DATA/NAS/datasets_source/brain/", "dataset-DSA"), False, False, validate_slice_increment=False)

sys.exit()
# 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"
# nii2 = NII.load(s, False)
# print(nii2.affine, nii2.orientation)
Expand Down
Loading