Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
12 changes: 2 additions & 10 deletions TPTBox/core/dicom/dicom2nii_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from tqdm import tqdm

from TPTBox import BIDS_FILE, NII, BIDS_Global_info, Print_Logger
from TPTBox.core.internal.nii_help import save_json as secure_save_json

# source_folder = Path("/DATA/NAS/datasets_source/epi/NAKO/NAKO-732_Nachlieferung_20_25/")
source_folder = Path("/DATA/NAS/datasets_source/epi/NAKO/NAKO_2D_issue/")
Expand Down Expand Up @@ -309,21 +310,12 @@ def save_json(json_ob: dict, file: str | Path, check_exist: bool = False, overri
FileExistsError: When *check_exist* is ``True`` and the existing file
contains different content.
"""

def convert(obj):
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
return float(obj)
raise TypeError(type(obj))

if check_exist and test_name_conflict(json_ob, file):
raise FileExistsError(file)
if Path(file).exists() and not override:
return True
Print_Logger().on_save("save json with grid info", file)
with open(file, "w") as file_handel:
json.dump(json_ob, file_handel, indent=4, default=convert)
secure_save_json(file, json_ob, indent=4)
return False


Expand Down
51 changes: 38 additions & 13 deletions TPTBox/core/dicom/dicom_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,51 @@

sys.path.append(str(Path(__file__).parent))

import string

from TPTBox.core.dicom.dicom2nii_utils import get_json_from_dicom, load_json, save_json, test_name_conflict

logger = Print_Logger()


def _inc_key(keys: dict, inc: int = 1) -> None:
"""Increment the sequence key inside *keys* by *inc*."""
k = "sequ"
def _next_letter_suffix(s: str, inc: int = 1) -> str:
"""Increment a letter suffix: a -> b, z -> aa, aa -> ab."""
alphabet = string.ascii_lowercase
# Convert to a number (base 26, 1-indexed)
n = 0
for c in s:
n = n * 26 + (ord(c) - ord("a") + 1)
n += inc
# Convert back to letters
result = []
while n > 0:
n -= 1
result.append(alphabet[n % 26])
n //= 26
return "".join(reversed(result))


def _inc_key(keys: dict, inc: int = 1, k="sequ") -> None:
"""Increment the sequence key inside *keys* by appending letter suffixes."""
if k not in keys:
keys[k] = 0
keys[k] = "0"
value = str(keys[k])
try:
v = int(keys[k])
keys[k] = str(v + int(inc))
except Exception:
try:
a, b = str(keys[k]).rsplit("-", maxsplit=2)
except Exception:
a = keys[k]
b = 0
keys[k] = a + "-" + str(int(b) + int(inc))
# Pure number: 100 -> 100-a
int(value)
keys[k] = f"{value}-a"
return # noqa: TRY300
except ValueError:
pass

try:
base, suffix = value.rsplit("-", maxsplit=1)
if suffix.isalpha():
keys[k] = f"{base}-{_next_letter_suffix(suffix, inc)}"
else:
keys[k] = f"{base}-a"
except ValueError:
keys[k] = f"{value}-a"


def _generate_bids_path(
Expand Down
17 changes: 13 additions & 4 deletions TPTBox/core/internal/elastic_deform.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import time

import elasticdeform
# pip install elasticdeform
import elasticdeform # See https://github.com/gvtulder/elasticdeform/issues/24 to install this for >2.x
import numpy as np
from numpy.typing import NDArray

Expand Down Expand Up @@ -49,19 +50,23 @@ def deformed_nii(
deformed_data = deformed_NII(arr_dic, sigma=sigma, points=points)
"""
if sigma is None or points is None:
np.random.seed(None)
sigma, points = get_random_deform_parameter(deform_factor=deform_factor)

print("deformation parameter sigma = ", round(sigma, 4), "; n_points = ", points)
t = time.time()
values = list(nii_dic.values())

# Deform
max_v = None
if joint_normalize:
max_v = max([img.max() for img in nii_dic.values() if not img.seg])
nii_dic = {k: img if img.seg else img.set_dtype(np.float32) / max_v for k, img in nii_dic.items()}
elif normalize:
nii_dic = {k: img if img.seg else img.set_dtype(np.float32).normalize() for k, img in nii_dic.items()}
max_v = {k: None if img.seg else (float(max(img.max() - img.min(), 1)), float(img.min())) for k, img in nii_dic.items()}
nii_dic = {k: img if img.seg else (img.set_dtype(np.float32) - max_v[k][1]) / max_v[k][0] for k, img in nii_dic.items()}
else:
nii_dic = {k: img if img.seg else img.set_dtype(np.float32) for k, img in nii_dic.items()}

values = list(nii_dic.values())
assert sigma is not None
p = deform_padding
out: list[NDArray] = elasticdeform.deform_random_grid(
Expand All @@ -74,6 +79,10 @@ def deformed_nii(
for (k, nii), arr in zip(nii_dic.items(), out, strict=True):
out2[k] = nii.set_array(arr[p:-p, p:-p, p:-p])
print("Deformation took", round(time.time() - t, 1), "Seconds")
if joint_normalize:
out2 = {k: img if img.seg else img.set_dtype(np.float32) * max_v for k, img in out2.items()}
elif normalize:
out2 = {k: img if img.seg else ((img.set_dtype(np.float32) * max_v[k][0]) + max_v[k][1]) for k, img in out2.items()}
return out2


Expand Down
84 changes: 82 additions & 2 deletions TPTBox/core/internal/nii_help.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
import shutil
from collections.abc import Callable
from functools import wraps
Expand All @@ -14,10 +15,11 @@
if TYPE_CHECKING:
from TPTBox.core.nii_poi_abstract import Has_Grid
from TPTBox.core.nii_wrapper import NII

from TPTBox.core.vert_constants import AFFINE, MODES, SHAPE, ZOOMS, Sentinel, _supported_img_files


def secure_save(func) -> Callable:
def secure_save(func, *, file_types=tuple(_supported_img_files)) -> Callable:
"""Decorator that writes to a `.backup` file first and restores it if saving fails.

Steps: (1) back up existing file, (2) call the wrapped save function, (3) delete backup on
Expand Down Expand Up @@ -48,7 +50,7 @@ def save_to_file(self, file: Path, data: Any):
@wraps(func)
def wrapper(self, file: str | Path | bids_files.BIDS_FILE, *args, **kwargs):
if isinstance(file, bids_files.BIDS_FILE):
for file_type in _supported_img_files:
for file_type in file_types:
if file_type in file.file:
file = file.file[file_type]
break
Expand Down Expand Up @@ -81,6 +83,84 @@ def wrapper(self, file: str | Path | bids_files.BIDS_FILE, *args, **kwargs):
return wrapper


def _convert(obj):
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
if isinstance(obj, Path):
return str(obj.absolute())
raise TypeError(type(obj))


def _save_json(data, filepath: str | Path | bids_files.BIDS_FILE, indent=4, convert=_convert):

if isinstance(filepath, bids_files.BIDS_FILE):
if "json" in filepath.file:
filepath = filepath.file["json"]
else:
nf = filepath.get_nii_file()
if nf is not None:
filepath = (nf.parent) / (nf.name.split(".")[0] + ".json")
else:
nf = next(iter(filepath.file.values()))
filepath = (nf.parent) / (nf.name.rsplit(".", maxsplit=1)[0] + ".json")
# print(markups[-1].get("display"))
with open(filepath, "w") as f:
json.dump(data, f, indent=indent, default=convert)


def save_json(filepath: str | Path | bids_files.BIDS_FILE, data, indent=4, convert=_convert) -> None:
"""Safely save a Python object as a JSON file with automatic backup protection.

This function writes JSON data to disk using a safe save mechanism:
if the target file already exists, it is first moved to a `.backup`
file. If writing succeeds, the backup is removed. If writing fails,
the original file is restored.

The function supports flexible input types for the target path:
- str or Path: written directly to disk
- bids_files.BIDS_FILE: resolved to an appropriate `.json` path

Non-JSON-serializable types are handled via a custom converter
that supports:
- numpy integers → int
- numpy floats → float
- numpy arrays → list
- pathlib.Path → absolute string path

Args:
filepath (str | Path | bids_files.BIDS_FILE):
Target file path or BIDS file container.
data (Any):
Python object to serialize into JSON.
indent (int, optional):
Pretty-print indentation level. Default is 4.
convert (callable, optional):
Custom serialization function for unsupported types.
Defaults to `_convert`.

Returns:
None

Notes:
- Uses `secure_save` to ensure atomic write semantics with backup/restore.
- If `filepath` is a `BIDS_FILE`, the `.json` path is inferred from:
1. explicit "json" entry in the file map
2. associated NIfTI file path
3. fallback to any available file in the container
- This function is intended for structured metadata and annotation storage.

Raises:
Exception:
Propagates any error raised during serialization or file writing,
after attempting automatic recovery of the original file.
"""
return secure_save(_save_json, file_types=["json"])(data, filepath, indent=indent, convert=convert)


def _resample_from_to(
from_img: NII,
to_img: tuple[SHAPE, AFFINE, ZOOMS] | Has_Grid,
Expand Down
Empty file.
Loading
Loading