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
7 changes: 6 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ jobs:
python -m pip install --upgrade pip
python -m pip install poetry
python -m pip install flake8 pytest
- name: Install system deps for CPU-only mesh3D tests (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y xvfb libgl1
- name: Install dependancies
run: |
python -m poetry install
python -m poetry install --with dev
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/tests_mr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ jobs:
python -m pip install --upgrade pip
python -m pip install poetry
python -m pip install flake8 pytest
- name: Install system deps for CPU-only mesh3D tests
run: |
sudo apt-get update
sudo apt-get install -y xvfb libgl1
- name: Install dependencies
run: |
python -m poetry install
python -m poetry install --with dev
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand Down
59 changes: 45 additions & 14 deletions TPTBox/mesh3D/README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,61 @@
# Mesh 3D (`TPTBox.mesh3D`)

3D surface mesh generation from segmentation NIfTI volumes and rendering of 3D snapshots.
Requires `pyvista` and `vtk` (included in the `dev` extras).
Requires `pyvista`, `vtk`, `scikit-image` and (for `snapshot3D`) `fury`, `Pillow`
and `xvfbwrapper` (all included in the `dev` extras).

![Snapshot3D example](TPTBox/images/snp3D_example.jpg)

## Key symbols

| Symbol | Module | Description |
|---|---|---|
| `Mesh` | `mesh.py` | Generates a surface mesh from a segmentation label using marching cubes |
| `create_snapshot3D` | `snapshot3D.py` | Renders a 3D snapshot from a list of meshes to a PNG file |
| `LABEL_COLORS` | `mesh_colors.py` | Default colour mapping for anatomical label IDs |
| `label_to_color(label_id)` | `mesh_colors.py` | Look up the RGB colour for a given label |
| `create_html_preview(meshes)` | `html_preview.py` | Generate an interactive HTML file with an embedded 3D viewer |
| `Mesh3D` | `mesh.py` | Thin wrapper around a `pyvista.PolyData` mesh with save / load / display helpers |
| `SegmentationMesh` | `mesh.py` | Generates a surface mesh from a segmentation array or `NII` via marching cubes |
| `POIMesh` | `mesh.py` | Glyph mesh (spheres) built from a `POI` container |
| `make_snapshot3D` | `snapshot3D.py` | Render a segmentation as one or more 3D views to a PNG file |
| `make_snapshot3D_parallel` | `snapshot3D.py` | Run `make_snapshot3D` in a process pool over many images |
| `Mesh_Color_List` | `mesh_colors.py` | Catalog of named `RGB_Color` constants (ITK palette) |
| `get_color_by_label(label)` | `mesh_colors.py` | Look up the `RGB_Color` for an integer label |
| `write_ctbl(path)` | `mesh_colors.py` | Export the palette as a 3D Slicer `.ctbl` color-table file |
| `make_html_preview(images, html_out)` | `html_preview.py` | Render `NII`/`POI` objects to an interactive HTML file |
| `Preview_Settings` | `html_preview.py` | Per-object visualization settings (color, opacity, offset) for `make_html_preview` |


## Installation

```bash
pip install pyvista vtk
pip install pyvista vtk scikit-image fury Pillow xvfbwrapper
# or via the dev extras:
poetry install --with dev
```


## Example
## Examples

### Building meshes and saving them

```python
from TPTBox import NII
from TPTBox.mesh3D.mesh import Mesh
from TPTBox.mesh3D.snapshot3D import create_snapshot3D
from TPTBox.mesh3D.mesh import SegmentationMesh

seg = NII.load("seg.nii.gz", seg=True)

# Build meshes for all labels and render
meshes = [Mesh(seg, label=lbl) for lbl in seg.unique_labels()]
create_snapshot3D(meshes, to="snapshot3D.png")
# Build a single surface mesh over all labels
mesh = SegmentationMesh.from_segmentation_nii(seg)
mesh.save("seg.ply")
```

More extensive:
### Rendering a 3D snapshot

```python
from TPTBox.mesh3D.snapshot3D import make_snapshot3D

make_snapshot3D("seg.nii.gz", "snapshot3D.png", view=["A", "L"])
```

### Parallel snapshots across body systems

```python
from TPTBox.core.vert_constants import Full_Body_Instance
from TPTBox.mesh3D.snapshot3D import make_snapshot3D_parallel
Expand Down Expand Up @@ -74,3 +90,18 @@ make_snapshot3D_parallel(
],
)
```

### Interactive HTML preview

```python
from TPTBox import NII, POI
from TPTBox.mesh3D.html_preview import Preview_Settings, make_html_preview

seg = NII.load("seg.nii.gz", seg=True)
poi = POI.load("poi.json", reference=seg)
make_html_preview(
[seg, Preview_Settings(poi, opacity=1.0)],
"preview.html",
poi_size=2.0,
)
```
35 changes: 35 additions & 0 deletions TPTBox/mesh3D/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ class SegmentationMesh(Mesh3D):
"""Mesh generated from a segmentation volume using the marching-cubes algorithm."""

def __init__(self, int_arr: np.ndarray | Image_Reference) -> None:
"""Build a surface mesh from a 3D integer segmentation array or image reference.

The array is cropped to its non-zero bounding box (with a 2-voxel margin),
then converted to a surface mesh via marching cubes. Vertex coordinates are
shifted back so they remain aligned with the original (uncropped) voxel grid.

Args:
int_arr: A 3D numpy array with integer labels (background must be 0),
or an ``Image_Reference`` that can be loaded as a segmentation
``NII``. ``NII`` inputs are reoriented and rescaled to isotropic
spacing before extraction.

Raises:
AssertionError: If the array's minimum value is not zero or the array
does not have exactly three dimensions.
"""
if not isinstance(int_arr, np.ndarray):
seg_nii = to_nii_seg(int_arr)
seg_nii.reorient_().rescale_()
Expand Down Expand Up @@ -196,6 +212,25 @@ def __init__(
subregions: list[int] | None = None,
size_factor: float = 5,
) -> None:
"""Build a glyph mesh (spheres) from a POI container.

Filters POIs by region and subregion, then renders each remaining coordinate
as a small sphere so it can be visualized alongside a segmentation mesh.

Args:
poi: Input ``POI`` container. Reoriented in place and optionally
rescaled to isotropic spacing.
rescale_to_iso: If True, rescales ``poi`` to isotropic spacing before
extracting coordinates.
regions: Vertebra / region IDs to keep. If None, all regions in ``poi``
are used.
subregions: Subregion IDs to keep. If None, all subregions in ``poi``
are used.
size_factor: Radius (in the POI's coordinate units) of each sphere glyph.

Raises:
AssertionError: If no POIs match the requested region/subregion filter.
"""
poi.reorient_()
if rescale_to_iso:
poi.rescale_()
Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ coverage = ">=7.0.1"
pytest-mock = "^3.6.0"
exceptiongroup = { version = "^1.2", python = "<3.11" }
tomli = {version = "*", python = "<3.11" }
# Extra optional deps used by TPTBox.mesh3D (3D snapshots and HTML previews).
# Kept in the dev group so CPU-only mesh tests can run in CI without pulling
# them into every install.
# fury 2.x is a full pygfx-based rewrite that removed `window.record` and
# changed `ShowManager` / `Scene` signatures used by TPTBox.mesh3D.snapshot3D.
# Pin to the last VTK-backed line until snapshot3D is ported.
fury = ">=0.9,<2.0"
Pillow = "*"
xvfbwrapper = { version = "*", markers = "sys_platform == 'linux'" }

[tool.poetry.group.docs.dependencies]
mkdocs = ">=1.6"
Expand Down
Loading
Loading