Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 85 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Contributing to TPTBox

Thanks for helping improve this codebase. The following overview will help you as guide to contribute.

## Code Style

- **Line length**: 140 characters
- **Formatter**: Ruff (Black-compatible, double quotes)
- **Target Python**: 3.10+ syntax, but the package supports 3.9–3.14
- **Naming**: Ruff N-rules are largely ignored — mixed-case class/method names are acceptable in this codebase (medical domain conventions)
- **Complexity**: McCabe max=20; research code legitimately has deep branching
- `from __future__ import annotations` is used widely for forward references

## Get started!

Ready to contribute?

### 1) Create an <a href=https://github.com/Hendrik-code/TPTBox/issues>issue</a> on the GitHub repository

### 2) Fork the repository

### 3) Clone your fork

```bash
git clone https://github.com/<your-username>/TPTBox.git
cd TPTBox
```

### 4) Local install (alternative)

Make a venv (in whatever fashion you like), then:
```bash
pip install -e .
```

### 5) Ensure you install the pre-commit hook:
```bash
pre-commit install
```
This pre-commit hook will automatically fix some linting issues and block your commits so that every commit is clean in terms of formatting

### 6) Running checks locally

Before pushing, make sure all unit tests and ruff pass. Tests live in `unit_tests/` (not `TPTBox/tests/`). `TPTBox/tests/` contains test utilities and sample data (CT/MRI NIfTIs) used by the unit tests. Some test files are very large.

```bash
# All tests
pytest unit_tests/

# Single test file
pytest unit_tests/test_nii.py

# Single test function
pytest unit_tests/test_nii.py::test_function_name

# With coverage
coverage run --source=TPTBox -m pytest unit_tests/
```

Linting & Formatting:
```bash
# Lint (auto-fix where possible)
ruff check . --fix

# Format
ruff format .

# Both (mirrors pre-commit behavior)
pre-commit run --all-files
```




### 7) Submitting changes

1. Create a feature branch from `main`:

```bash
git checkout -b my-feature
```

2. Make your changes and commit with a clear, descriptive message.
3. Push your branch and open a <a href=https://github.com/Hendrik-code/TPTBox/pulls>pull request</a> against `main`.
4. CI runs on all pull requests — ensure all checks are green before requesting review.
317 changes: 109 additions & 208 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=README_NII.md>NII: nii_wrapper.py -- NIfTI image wrapper </a>
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=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=README_BIDS.md>BIDS: bids_files.py -- Dataset Handling </a>
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 of NII functions above utilize under the hood. Most of them are optimized to run on uint numpy arrays.

| 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.
```
Loading
Loading