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
25 changes: 25 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Docs

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install package and docs dependencies
run: |
pip install -e .
pip install mkdocs mkdocs-material "mkdocstrings[python]" mkdocs-gen-files mkdocs-literate-nav

- name: Build docs
run: mkdocs build
17 changes: 17 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2

build:
os: ubuntu-24.04
tools:
python: "3.12"
jobs:
post_install:
- pip install "mkdocs>=1.6" "mkdocs-material>=9.5" "mkdocstrings[python]>=0.25"

mkdocs:
configuration: mkdocs.yml

python:
install:
- method: pip
path: .
95 changes: 95 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

TPTBox is a Python package for processing BIDS-compliant medical imaging datasets (CT, MRI), with a focus on torso/spine analysis. It provides NIfTI I/O, reorientation/resampling, Points of Interest (POI) computation for vertebrae, 2D/3D snapshots, registration, and segmentation integrations (SPINEPS, nnU-Net).

## Commands

### Installation
```bash
pip install poetry
poetry install --with dev
```

### Running Tests
```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
```

### CI Linting (as run in GitHub Actions)
```bash
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
```

## Architecture

### Core Abstractions

**`NII`** (`TPTBox/core/nii_wrapper.py`) — Central class wrapping nibabel NIfTI images. Handles reorientation, resampling, affine transforms, boolean masking, and mathematical operations. Nearly all image operations in the package go through `NII`. Math operations live in `nii_wrapper_math.py` as mixins.

**`POI`** (`TPTBox/core/poi.py`) — Points of Interest container mapping `(vertebra_id, subregion_id) → 3D coordinate`. Coordinates can be in voxel or world space. POI computation strategies live in `TPTBox/core/poi_fun/`.

**`BIDS_FILE` / `BIDS_Global_info`** (`TPTBox/core/bids_files.py`) — BIDS dataset navigator. `BIDS_Global_info` scans a dataset root, while `BIDS_FILE` represents a single file parsed according to BIDS naming entities. Constants for BIDS key/value vocabulary are in `bids_constants.py`.

**`Location` / `Vertebra_Instance`** (`TPTBox/core/vert_constants.py`) — Enum-like constants for vertebral anatomy (vertebra IDs, subregion labels). Used as keys into `POI` objects throughout the codebase.

### Package Layout

```
TPTBox/
├── core/ # NII, POI, BIDS parsing, numpy utilities, vertebra constants
│ └── poi_fun/ # POI calculation strategies and serialization
├── spine/ # Spine-specific: 2D snapshots, statistics (distances, angles)
├── segmentation/ # SPINEPS integration, nnU-Net inference, defacing
├── registration/ # ANTs rigid/deformable, DeepALI deep learning registration
├── mesh3D/ # 3D mesh generation and rendering from segmentations
├── stitching/ # Multi-station image stitching
└── logger/ # Logging infrastructure (Logger, Print_Logger, No_Logger)
```

Public API is re-exported from `TPTBox/__init__.py`. All major classes and utility functions are importable directly from `TPTBox`.

### Key Relationships

- `NII` + `POI` are used together constantly: compute POIs from segmentation `NII`s, then use POIs to guide further processing.
- `BIDS_Global_info` iterates subjects/sessions; each subject has a `Subject_Container` with `BIDS_FILE` entries; `BIDS_FILE.get_nii()` returns a `NII`.
- `vert_constants.py` defines the shared label space (`Location` enum) that ties segmentation labels to POI keys to snapshot rendering.
- External tool integrations (SPINEPS, nnU-Net, ANTs, DeepALI) are optional; their imports are guarded so the core works without them.

### Test Layout

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 generated test files are very large (>20K LOC) — they are autogenerated and should not be edited by hand.

## 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
97 changes: 97 additions & 0 deletions TPTBox/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# TPTBox Core

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

| 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

| 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

| 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 |

### `np_utils.py` — NumPy utilities

| Symbol | Description |
|---|---|
| `np_extract_label(arr, label)` | Extract a single label as a binary mask |
| `np_center_of_mass(arr)` | Per-label centre-of-mass |
| `np_volume(arr)` | Per-label voxel count |
| `np_bbox_binary(mask)` | Bounding-box slice tuple for a binary array |
| `np_dilate_msk(arr, mm, zoom)` | Morphological dilation by `mm` millimetres |
| `np_erode_msk(arr, mm, zoom)` | Morphological erosion |
| `np_fill_holes(arr)` | Fill holes per label |
| `np_connected_components(arr)` | Label connected components |
| `np_map_labels(arr, label_map)` | Remap label integers via a dict |
| `np_unique(arr)` | Unique values (faster than `np.unique` for uint arrays) |

### `vert_constants.py` — Anatomical constants

| Symbol | Description |
|---|---|
| `Location` | `IntEnum` of anatomical subregion IDs (used as POI keys) |
| `Vertebra_Instance` | Maps integer IDs → anatomical names (C1–S1) |
| `v_name2idx` | Dict: `"L1" → 20`, etc. |
| `v_idx2name` | Dict: `20 → "L1"`, etc. |
| `v_idx_order` | Canonical sort order for vertebra IDs |
| `ZOOMS` | Type alias: `tuple[float, float, float]` |
| `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)

# 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()
```
2 changes: 1 addition & 1 deletion TPTBox/core/bids_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@
"Hemisphere": "hemi", # [L,R]
"Sample": "sample", # such as tissue, primary cell or cell-free sample.
# Sub recordings - Use when necessary.
"Split": "split", # Never use, legacy. Is applied inconsistently by other datasets.
## Contrast
"Contrast enhancement phase": "ce",
"Tracer": "trc", # use ce before this one.
Expand Down Expand Up @@ -340,7 +341,6 @@
# Single class segmentation
"Label": "label",
# Others (never used)
"Split": "split", # Never use, legacy. Is applied inconsistently by other datasets.
"Density": "den",
"version": "version",
"Description": "desc",
Expand Down
Loading
Loading