Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
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
2 changes: 1 addition & 1 deletion TPTBox/core/bids_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,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 @@ -337,7 +338,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
9 changes: 0 additions & 9 deletions TPTBox/core/bids_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -1481,15 +1481,6 @@ def __lt__(self, other):
def get_identifier(self):
first_e = self.data_dict[next(iter(self.data_dict.keys()))][0]
return first_e.get_identifier(self.sequence_splitting_keys)
# if "sub" not in first_e.info:
# print(f"family_id, no sub-key, got {first_e.info} and data_dict {list(self.data_dict.keys())}")
# identifier = "sub-404"
# else:
# identifier = "sub-" + first_e.info["sub"]
# for s in first_e.info.keys():
# if s in self.sequence_splitting_keys:
# identifier += "_" + s + "-" + first_e.info[s]
# return identifier

def items(self):
return self.data_dict.items()
Expand Down
5 changes: 5 additions & 0 deletions TPTBox/core/nii_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ def __init__(self, nii: Nifti1Image|_unpacked_nii, seg=False,c_val=None, desc:st
self.set_dtype_("smallest_uint")


@classmethod
def from_numpy(cls, arr: np.ndarray, affine: np.ndarray, seg=False, c_val=None, desc:str|None=None, info=None):
nii = nib.nifti1.Nifti1Image(arr,affine)
return NII(nii=nii, seg=seg, c_val=c_val, desc=desc, info=info)

@classmethod
def load(cls, path: Image_Reference, seg, c_val=None)-> Self:
nii= to_nii(path,seg)
Expand Down
37 changes: 37 additions & 0 deletions TPTBox/tests/speedtests/speedtest_panoptica.py

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.

What is this file?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

oh shit the ai comitted this accidentially. will remove it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
if __name__ == "__main__":
# speed test dilation
import random

import numpy as np
from panoptica.utils.input_check_and_conversion.sanity_checker import sanity_check_and_convert_to_array
from scipy.ndimage import center_of_mass

from TPTBox.core.nii_wrapper import NII
from TPTBox.core.np_utils import (
_to_labels,
np_extract_label,
)
from TPTBox.tests.speedtests.speedtest import speed_test
from TPTBox.tests.test_utils import get_nii

def get_nii_array():
num_points = random.randint(5, 10)
nii, points, orientation, sizes = get_nii(x=(10, 10, 10), num_point=num_points)
# nii.map_labels_({1: -1}, verbose=False)
arr = nii.get_seg_array().astype(np.uint8)
# arr[arr == 1] = -1
# arr_r = arr.copy()
return arr

def check_sanity(arr: np.ndarray):
_, c = sanity_check_and_convert_to_array(arr, arr)
return c.name

speed_test(
repeats=5,
get_input_func=get_nii_array,
functions=[check_sanity],
assert_equal_function=lambda x, y: np.array_equal(x, y), # noqa: ARG005
# np.all([x[i] == y[i] for i in range(len(x))])
)
# print(time_measures)
38 changes: 31 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ pytest-mock = "^3.6.0"
exceptiongroup = { version = "^1.2", python = "<3.11" }
tomli = {version = "*", python = "<3.11" }

[tool.poetry.group.docs.dependencies]
mkdocs = ">=1.6"
mkdocs-material = ">=9.5"
mkdocstrings = {extras = ["python"], version = ">=0.25"}
mkdocs-gen-files = ">=0.5"
mkdocs-literate-nav = ">=0.6"

[build-system]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
build-backend = "poetry_dynamic_versioning.backend"
Expand Down Expand Up @@ -115,9 +122,9 @@ select = [
"TD005",
"FIX003",
"FIX004",
#"ERA", For clean up
#"D", Dockstring For clean up
#"ANN", Annoation For clean up
#"ERA",
"D",
"ANN201",
"PD",
"PGH",
"PL",
Expand Down Expand Up @@ -154,16 +161,25 @@ ignore = [
"UP038",
"N999",
"E741",
"SIM118", # dictionay keys
"SIM118", # dictionary keys
"N802", # function name lowercase
"F811",
"N803",
"N806",
"FURB171",
"FURB171",
"PLW0108",
"B905", # strict= in zip
"UP007", # Union and "|" python 3.9
"B905", # strict= in zip
"UP007", # Union and "|" python 3.9
"PLC0415", # import-outside-top-level
# Docstring rules relaxed for internal / magic / dunder members
"D100", # missing docstring in public module
"D104", # missing docstring in public package
"D105", # missing docstring in magic method
"D107", # missing docstring in __init__ (covered by class docstring)
# Return-type annotation not required for private helpers
"ANN202",
# En-dashes (–) in docstrings are legitimate range notation (e.g. "C1–C7")
"RUF002",
]

# Allow fix for all enabled rules (when `--fix`) is provided.
Expand All @@ -176,6 +192,14 @@ extend-safe-fixes = ["RUF015", "C419", "C408", "B006"]
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.per-file-ignores]
# Test files: don't require docstrings on test functions / helper utilities
"unit_tests/**" = ["D101", "D102", "D103", "D205", "D415", "ANN201"]
"TPTBox/tests/**" = ["D101", "D102", "D103", "D205", "D415", "ANN201"]

[tool.ruff.lint.mccabe]
# Flag errors (`C901`) whenever the complexity level exceeds 5.
max-complexity = 20
Expand Down