Skip to content
297 changes: 99 additions & 198 deletions README.md

Large diffs are not rendered by default.

82 changes: 26 additions & 56 deletions TPTBox/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,25 @@
The `core` subpackage is the foundation of TPTBox. It provides the three primary abstractions —
`NII`, `POI`, and `BIDS_FILE` — along with helper utilities for array operations and anatomical constants.

## Key Classes and Functions

### `nii_wrapper.py` — NIfTI image wrapper
## The three pillars -- NII, POI, BIDS

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing one Read-Me for Visualization.


| Symbol | Description |
|---|---|
| `NII` | Wraps `nibabel.Nifti1Image`; the central image type throughout TPTBox |
| `NII.load(path, seg)` | Load a NIfTI file from disk (classmethod) |
| `NII.from_numpy(arr, affine, seg)` | Construct from a numpy array and affine matrix |
| `NII.reorient(axcodes_to)` | Reorient to a canonical axis code (e.g. `("R","A","S")`) |
| `NII.rescale(voxel_spacing)` | Resample to new voxel spacing in mm |
| `NII.resample_from_to(other)` | Resample to match the grid of another `NII` |
| `NII.apply_mask(mask)` | Zero-out voxels outside a binary/label mask |
| `NII.map_labels(label_map)` | Remap integer labels |
| `NII.save(path)` | Save to disk as `.nii` or `.nii.gz` |
| `NII.get_array()` | Return a copy of the underlying numpy array |
| `NII.get_seg_array()` | Same as `get_array()` but asserts `seg=True` |
| `Image_Reference` | Type alias: `BIDS_FILE | Nifti1Image | Path | str | NII` |

### `bids_files.py` — BIDS dataset navigation
### <a href=TPTBox/core/README_NII.md>NII: nii_wrapper.py -- NIfTI image wrapper </a>
Comment thread
Hendrik-code marked this conversation as resolved.
Outdated
This is the core of image handling, this takes care of loading images and segmentations, and any data processing

| Symbol | Description |
|---|---|
| `BIDS_Global_info` | Scans a dataset root and indexes all BIDS files |
| `BIDS_Global_info.enumerate_subjects()` | Iterate over subjects as `(subject_id, Subject_Container)` |
| `Subject_Container` | Per-subject file index; entry point for queries |
| `Subject_Container.new_query()` | Returns a `Searchquery` for this subject |
| `BIDS_FILE` | One file parsed into BIDS entities (sub, ses, format, …) |
| `BIDS_FILE.open_nii()` | Load this file's NIfTI |
| `BIDS_FILE.get_changed_path(...)` | Derive a new path with changed BIDS entities |
| `Searchquery` | Fluent query builder: `.filter()`, `.loop_dict()`, `.first()` |
| `BIDS_Family` | `dict[str, list[BIDS_FILE]]` grouping files by format |

### `poi.py` — Points of Interest
### <a href=TPTBox/core/README_POI.md>POI: poi.py -- Points of Interests </a>
This is the core of handling 2D/3D coordinates in any defined space. Center of mass locations can be computed in this format, and other landmarks. Similar to Niftis, this contains an affine matrix so it is aware of its global space relation, voxel spacing, ...

### <a href=TPTBox/core/README_BIDS.md>BIDS: bids_file.py -- Dataset Handling </a>
Comment thread
Hendrik-code marked this conversation as resolved.
Outdated
This is the core of handling datasets that are BIDS-compliant. Easily search through your datasets and find all images following your constraints, such as every CT that also has a specific segmentation available.

| Symbol | Description |
|---|---|
| `POI` | Maps `(vertebra_id, subregion_id) → (x, y, z)` |
| `calc_centroids(seg_nii)` | Compute centroids for every label in a segmentation |
| `calc_poi_from_subreg_vert(vert, subreg)` | Compute POIs from paired vertebra + subregion segmentations |
| `POI.save(path)` | Serialise to JSON |
| `POI.load(path)` | Deserialise from JSON |
| `POI.to_global(ref)` | Convert from voxel to world (mm) coordinates |
| `POI.to_local(ref)` | Convert from world to voxel coordinates |

## Other Key Classes and Functions

### `np_utils.py` — NumPy utilities

Numpy functionalities that a lot f NII functions above utilize under the hood. Most of them are optimized to run on uint numpy arrays.
Comment thread
Hendrik-code marked this conversation as resolved.
Outdated

| Symbol | Description |
|---|---|
| `np_extract_label(arr, label)` | Extract a single label as a binary mask |
Expand All @@ -63,6 +35,15 @@ The `core` subpackage is the foundation of TPTBox. It provides the three primary
| `np_map_labels(arr, label_map)` | Remap label integers via a dict |
| `np_unique(arr)` | Unique values (faster than `np.unique` for uint arrays) |

```python
from TPTBox.core.np_utils import np_unique, np_center_of_mass

a = np.array([0,1,2,3], [4,5,6,7], dtype=np.uint8)

label = np_unique(a)
center_of_mass_of_label_four = np_center_of_mass(a)[4]
```
Comment on lines +39 to +45

### `vert_constants.py` — Anatomical constants

| Symbol | Description |
Expand All @@ -76,22 +57,11 @@ The `core` subpackage is the foundation of TPTBox. It provides the three primary
| `AX_CODES` | Type alias: `tuple[str, str, str]` |
| `AFFINE` | Type alias: `np.ndarray` (4×4) |

## Quick Example

```python
from TPTBox import NII, BIDS_Global_info, calc_centroids

# Load and resample a CT
ct = NII.load("sub-001_ct.nii.gz", seg=False)
ct_ras = ct.reorient(("R", "A", "S")).rescale((1.0, 1.0, 1.0))

# Compute centroids from a segmentation
seg = NII.load("sub-001_seg.nii.gz", seg=True)
poi = calc_centroids(seg)
print(poi)
from TPTBox import NII, Location
# Segmentation
seg = NII.load("path/to/seg.nii.gz", seg=True)

# Scan a BIDS dataset
bids = BIDS_Global_info(["dataset/"], parents=["rawdata"])
for subj, container in bids.enumerate_subjects():
t2 = container.new_query().filter("format", "T2w").first()
# Get the segmentation of the Vertebra Corpus
seg_corpus = seg.extract_label(Location.Vertebra_Corpus)
```
89 changes: 89 additions & 0 deletions TPTBox/core/README_BIDS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# TPTBox `bids_files.py` — BIDS dataset navigation

| Symbol | Description |
|---|---|
| `BIDS_Global_info` | Scans a dataset root and indexes all BIDS files |
| `BIDS_Global_info.enumerate_subjects()` | Iterate over subjects as `(subject_id, Subject_Container)` |
| `Subject_Container` | Per-subject file index; entry point for queries |
| `Subject_Container.new_query()` | Returns a `Searchquery` for this subject |
| `BIDS_FILE` | One file parsed into BIDS entities (sub, ses, format, …) |
| `BIDS_FILE.open_nii()` | Load this file's NIfTI |
| `BIDS_FILE.get_changed_path(...)` | Derive a new path with changed BIDS entities |
| `Searchquery` | Fluent query builder: `.filter()`, `.loop_dict()`, `.first()` |
| `BIDS_Family` | `dict[str, list[BIDS_FILE]]` grouping files by format |


Loop over every T2w MRI in a dataset:
```python
from TPTBox import BIDS_Global_info, BIDS_FILE, NII

# Initialize the dataset and the folders therein to use
bids_dataset = BIDS_Global_info(["path/to/dataset"], parents=["rawdata"])

# looping over every subject in the dataset
for subject, container in bids.enumerate_subjects():
q = container.new_query()
q.filter("format", "T2w")
# more filter here
bids_families = q.loop_dict()
# A subject can have multiple MRI images
for bids_family in bids_families:
# ensure this family has a T2w
if "T2w" in bids_family:
# get the reference to the T2w image
t2w_ref: BIDS_FILE = bids_family["T2w"][0]
# load the nifty
t2w: NII = t2w.open_nii()

# further processing or analysis that would be
# run on every T2w MRI in this dataset
```

Investigate one BIDS_FILE and get a BIDS-compliant file path relative to it, guaranteeing a valid BIDS filename.
```python
from pathlib import Path

from TPTBox import BIDS_FILE

# Dataset root directory (must start with "dataset-")
root = Path("path/to/dataset-dsname")

# Example BIDS-compliant input file
example_file = (
root
/ "rawdata/sub-Max-Mustermann/ses-01012026/anat/"
"sub-Max-Mustermann_ses-01012026_acq-sag_ce-GBCA_T1w.nii.gz"
)

# Create a BIDS_FILE object
bf_file = BIDS_FILE(example_file, root)

# Access individual BIDS keys
print(f"Subject name : {bf_file.get('sub')}")
print(f"Session : {bf_file.get('ses')}")
print(f"Acquisition direction : {bf_file.get('acq')}")
print(f"Contrast agent : {bf_file.get('ce')}")
print(f"Modality : {bf_file.bids_format}")

# Generate a new BIDS-compliant file path relative to the source file
# (keys that are not explicitly overridden remain unchanged)
seg_path = bf_file.get_changed_path(
# File extension
file_type="nii.gz",
# Final suffix without a key, e.g., *_msk.nii.gz
bids_format="msk",
# Parent folder relative to the dataset root
parent="derivatives",
info={
# Name of the segmentation
"seg": "spine",
# Modality from which this file was generated
"mod": bf_file.mod,
},
# If True, disables sorting of keys according to the BIDS specification
no_sorting_mode=False,
# If True, disables strict validation against predefined key--value pairs
non_strict_mode=False,
)

```
37 changes: 37 additions & 0 deletions TPTBox/core/README_NII.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# TPTBox: `nii_wrapper.py` — NIfTI image wrapper

The `core` subpackage is the foundation of TPTBox. It provides the three primary abstractions —
`NII`, `POI`, and `BIDS_FILE` — along with helper utilities for array operations and anatomical constants.

| Symbol | Description |
|---|---|
| `NII` | Wraps `nibabel.Nifti1Image`; the central image type throughout TPTBox |
| `NII.load(path, seg)` | Load a NIfTI file from disk (classmethod) |
| `NII.from_numpy(arr, affine, seg)` | Construct from a numpy array and affine matrix |
| `NII.reorient(axcodes_to)` | Reorient to a canonical axis code (e.g. `("R","A","S")`) |
| `NII.rescale(voxel_spacing)` | Resample to new voxel spacing in mm |
| `NII.resample_from_to(other)` | Resample to match the grid of another `NII` |
| `NII.apply_mask(mask)` | Zero-out voxels outside a binary/label mask |
| `NII.map_labels(label_map)` | Remap integer labels |
| `NII.save(path)` | Save to disk as `.nii` or `.nii.gz` |
| `NII.get_array()` | Return a copy of the underlying numpy array |
| `NII.get_seg_array()` | Same as `get_array()` but asserts `seg=True` |
| `Image_Reference` | Type alias: `BIDS_FILE | Nifti1Image | Path | str | NII` |

```python
from TPTBox import NII
# Image
nii = NII.load("path/to/img.nii.gz", seg=False)
# Segmentation
seg = NII.load("path/to/seg.nii.gz", seg=True)

# Standardize the image to a fixed orientation (Right-Anterior-Superior in the nibabel coordinate system)
# and resample it to an isotropic resolution of 1 mm x 1 mm x 1 mm
nii_rescaled = nii.reorient("RAS").rescale((1, 1, 1))

# One-line function to resample another image to match a reference image
seg_resampled = seg.resample_from_to(nii_rescaled)

# The appropriate resampling method is automatically selected depending on
# whether the image represents a segmentation or a continuous-valued image.
```
142 changes: 142 additions & 0 deletions TPTBox/core/README_POI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# TPTBox `poi.py` — Points of Interest

![Example of two lumbar vertebrae. The left example is derived from 1 mm isotropic CT, the right from sagittal MRI with a resolution of 3.3 mm in the left–right direction. Top row: Subregion of the vertebra used for analysis. Middle row: Extreme points. Bottom row: Corpus edge and ligamentum flavum points.](TPTBox/images/poi_preview.png)
Comment thread
Copilot marked this conversation as resolved.
Outdated


| Symbol | Description |
|---|---|
| `POI` | Maps `(vertebra_id, subregion_id) → (x, y, z)` |
| `calc_centroids(seg_nii)` | Compute centroids for every label in a segmentation |
| `calc_poi_from_subreg_vert(vert, subreg)` | Compute POIs from paired vertebra + subregion segmentations |
| `POI.save(path)` | Serialise to JSON |
| `POI.load(path)` | Deserialise from JSON |
| `POI.to_global(ref)` | Convert from voxel to world (mm) coordinates |
| `POI.to_local(ref)` | Convert from world to voxel coordinates |
| `POI.save_mrk(ref)` | Saves the POI as Markup (to be used for 3D Slicer for example) |

Compute a simple poi object from a segmentation file
```python
from TPTBox import NII, calc_centroids

vert = NII.load("path/to/seg.nii.gz", True)
label_id = 20
second_stage = 1
# compute CMS
poi = calc_centroids(vert, second_stage=second_stage)
# The coordinate can be extracted by [Label-id, second_stage_id]
coords = poi[label_id, second_stage]
```

Compute a full set of anatomical landmarks. The registry of supported non-centroid POI strategies is exposed as
```python
from TPTBox.core.poi_fun.vertebra_pois_non_centroids import all_poi_functions

poi_full = calc_poi_from_subreg_vert(
instance_nii, semantic_nii,
subreg_id=list(all_poi_functions.keys()),
)
# export as a 3D Slicer markup file
poi_full.to_global().save_mrk(
"poi_as_markup.mrk.json",
split_by_region=True,
pointLabelsVisibility=True
)
```


```python
from TPTBox import NII, POI, Location, POI_Global, calc_poi_from_subreg_vert
from TPTBox.core.vert_constants import v_name2idx
from TPTBox.segmentation.spineps import run_spineps_single

# This requires that spineps is installed
output_paths = run_spineps_single(
"file-path-of_T2w.nii.gz",
model_semantic="t2w",
ignore_compatibility_issues=True,
)
out_spine = output_paths["out_spine"]
out_vert = output_paths["out_vert"]
semantic_nii = NII.load(out_spine, seg=True)
instance_nii = NII.load(out_vert, seg=True)

poi = calc_poi_from_subreg_vert(
instance_nii,
semantic_nii,
subreg_id=[
Location.Vertebra_Full,
Location.Arcus_Vertebrae,
Location.Spinosus_Process,
Location.Costal_Process_Left,
Location.Costal_Process_Right,
Location.Superior_Articular_Left,
Location.Superior_Articular_Right,
Location.Inferior_Articular_Left,
Location.Inferior_Articular_Right,
# Location.Vertebra_Corpus_border, CT only
Location.Vertebra_Corpus,
Location.Vertebra_Disc,
Location.Muscle_Inserts_Spinosus_Process,
Location.Muscle_Inserts_Transverse_Process_Left,
Location.Muscle_Inserts_Transverse_Process_Right,
Location.Muscle_Inserts_Vertebral_Body_Left,
Location.Muscle_Inserts_Vertebral_Body_Right,
Location.Muscle_Inserts_Articulate_Process_Inferior_Left,
Location.Muscle_Inserts_Articulate_Process_Inferior_Right,
Location.Muscle_Inserts_Articulate_Process_Superior_Left,
Location.Muscle_Inserts_Articulate_Process_Superior_Right,
Location.Ligament_Attachment_Point_Anterior_Longitudinal_Superior_Median,
Location.Ligament_Attachment_Point_Posterior_Longitudinal_Superior_Median,
Location.Ligament_Attachment_Point_Anterior_Longitudinal_Inferior_Median,
Location.Ligament_Attachment_Point_Posterior_Longitudinal_Inferior_Median,
Location.Additional_Vertebral_Body_Middle_Superior_Median,
Location.Additional_Vertebral_Body_Posterior_Central_Median,
Location.Additional_Vertebral_Body_Middle_Inferior_Median,
Location.Additional_Vertebral_Body_Anterior_Central_Median,
Location.Ligament_Attachment_Point_Anterior_Longitudinal_Superior_Left,
Location.Ligament_Attachment_Point_Posterior_Longitudinal_Superior_Left,
Location.Ligament_Attachment_Point_Anterior_Longitudinal_Inferior_Left,
Location.Ligament_Attachment_Point_Posterior_Longitudinal_Inferior_Left,
Location.Additional_Vertebral_Body_Middle_Superior_Left,
Location.Additional_Vertebral_Body_Posterior_Central_Left,
Location.Additional_Vertebral_Body_Middle_Inferior_Left,
Location.Additional_Vertebral_Body_Anterior_Central_Left,
Location.Ligament_Attachment_Point_Anterior_Longitudinal_Superior_Right,
Location.Ligament_Attachment_Point_Posterior_Longitudinal_Superior_Right,
Location.Ligament_Attachment_Point_Anterior_Longitudinal_Inferior_Right,
Location.Ligament_Attachment_Point_Posterior_Longitudinal_Inferior_Right,
Location.Additional_Vertebral_Body_Middle_Superior_Right,
Location.Additional_Vertebral_Body_Posterior_Central_Right,
Location.Additional_Vertebral_Body_Middle_Inferior_Right,
Location.Additional_Vertebral_Body_Anterior_Central_Right,
Location.Ligament_Attachment_Point_Flava_Superior_Median,
Location.Ligament_Attachment_Point_Flava_Inferior_Median,
Location.Vertebra_Direction_Posterior,
Location.Vertebra_Direction_Inferior,
Location.Vertebra_Direction_Right,
],
)
poi = poi.round(2)
print("Vertebra T4 Vertebra Corpus Center of mass:", poi[v_name2idx["T4"], Location.Vertebra_Corpus])
print("The id number of T4 Vertebra_Corpus is ", v_name2idx["T4"], Location.Vertebra_Corpus.value)

# rescale/reorante local poi like nii
poi_new = poi.reorient(("P", "I", "R")).rescale((1, 1, 1))
# Local and global POIs can be rescaled to a target spacing with:
poi_new = poi.resample_from_to(other_nii_or_poi)

# local to global poi
global_poi = poi.to_global(itk_coords=True)
# You can save global pois in mrk.json format for import and editing in slicer.
global_poi.save_mrk("FILE.mrk.json", glyphScale=3.0)
# Import as a Markup in slicer; To make points editable you must click on the "lock" symbol under Markups - Control Points - Interaction

# Save in our format:
poi.save(poi_path)
# Loading local/global Poi
poi = POI.load(poi_path)
poi = POI_Global.load(poi_path)



```
Loading
Loading