bone-cutting-plane-visualization 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. bone_cutting_plane_visualization-0.1.0/LICENSE +21 -0
  2. bone_cutting_plane_visualization-0.1.0/PKG-INFO +174 -0
  3. bone_cutting_plane_visualization-0.1.0/README.md +131 -0
  4. bone_cutting_plane_visualization-0.1.0/pyproject.toml +39 -0
  5. bone_cutting_plane_visualization-0.1.0/setup.cfg +4 -0
  6. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/__init__.py +61 -0
  7. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/classification.py +38 -0
  8. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/cli.py +107 -0
  9. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/compatibility.py +171 -0
  10. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/data.py +334 -0
  11. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/geometry.py +69 -0
  12. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/ubd.py +158 -0
  13. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/visualization.py +762 -0
  14. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/PKG-INFO +174 -0
  15. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/SOURCES.txt +22 -0
  16. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/dependency_links.txt +1 -0
  17. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/entry_points.txt +2 -0
  18. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/requires.txt +15 -0
  19. bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/top_level.txt +1 -0
  20. bone_cutting_plane_visualization-0.1.0/tests/test_cli.py +31 -0
  21. bone_cutting_plane_visualization-0.1.0/tests/test_compatibility.py +52 -0
  22. bone_cutting_plane_visualization-0.1.0/tests/test_data.py +149 -0
  23. bone_cutting_plane_visualization-0.1.0/tests/test_ubd.py +69 -0
  24. bone_cutting_plane_visualization-0.1.0/tests/test_visualization.py +176 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GGN_2015
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: bone-cutting-plane-visualization
3
+ Version: 0.1.0
4
+ Summary: Standalone VTK visualization for bone tumor cutting-plane plans
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 GGN_2015
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: ct-mri-dicom-nii-reader<0.2,>=0.1.2
31
+ Requires-Dist: numpy>=1.26
32
+ Requires-Dist: scipy>=1.11
33
+ Provides-Extra: viz
34
+ Requires-Dist: vtk>=9.2; extra == "viz"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=8; extra == "dev"
37
+ Requires-Dist: ruff>=0.6; extra == "dev"
38
+ Provides-Extra: all
39
+ Requires-Dist: vtk>=9.2; extra == "all"
40
+ Requires-Dist: pytest>=8; extra == "all"
41
+ Requires-Dist: ruff>=0.6; extra == "all"
42
+ Dynamic: license-file
43
+
44
+ # bone-cutting-plane-visualization
45
+
46
+ Standalone data contracts and VTK visualization for bone-tumor cutting-plane
47
+ plans. The package is independent of any planning or optimization repository.
48
+
49
+ All persisted inputs use the `.ubd.npz` format from
50
+ [`ct-mri-dicom-nii-reader`](https://pypi.org/project/ct-mri-dicom-nii-reader/).
51
+ Volumes are interpreted in `(Left, Posterior, Superior)` axis order. Standard
52
+ UBD files can be used for the bone/tumor and convex-hull views; a compatible,
53
+ namespaced extension stores a complete resection plan in one UBD archive.
54
+
55
+ ## Installation
56
+
57
+ ```powershell
58
+ python -m pip install -e ".[all]"
59
+ ```
60
+
61
+ The mandatory runtime dependency on `ct-mri-dicom-nii-reader` provides the UBD
62
+ loader and `BodyData` contract. VTK remains optional so data inspection and
63
+ scene preparation work on headless systems:
64
+
65
+ ```powershell
66
+ python -m pip install -e .
67
+ python -m pip install -e ".[viz]"
68
+ ```
69
+
70
+ ## Data contracts
71
+
72
+ The public plan consists of five explicit classes:
73
+
74
+ - `LabeledVolume`: `-1` normal bone, `0` other, `1` tumor.
75
+ - `SelectedCuttingPlanes`: ordered index-space rows `[a, b, c, d]`.
76
+ - `RegionClassificationVolume`: negative retained bone and `2` resected bone.
77
+ - `VolumeGeometry`: spacing, LPS origin, and index-to-LPS direction.
78
+ - `TumorSafetyMarginData`: optional planning volume, protected mask, danger-bone
79
+ mask, and physical margin.
80
+
81
+ `ResectionPlanData` combines them and validates shapes, labels, plane references,
82
+ and safety-margin clearance.
83
+
84
+ ```python
85
+ import numpy as np
86
+
87
+ from bone_cutting_plane_visualization import (
88
+ ResectionPlanData,
89
+ SelectedCuttingPlanes,
90
+ classify_regions,
91
+ load_labeled_volume,
92
+ save_resection_plan,
93
+ )
94
+
95
+ volume, geometry = load_labeled_volume("case-labels.ubd.npz")
96
+ planes = SelectedCuttingPlanes(
97
+ np.asarray([[1.0, 0.0, 0.0, -42.5]], dtype=np.float64)
98
+ )
99
+ classification = classify_regions(volume, planes)
100
+ plan = ResectionPlanData(volume, planes, classification, geometry)
101
+ save_resection_plan(plan, "case-plan.ubd.npz")
102
+ ```
103
+
104
+ Arrays are copied, normalized to stable dtypes, and exposed read-only.
105
+
106
+ Callers migrating from NumPy-based APIs can use `compatibility.plan_from_arrays`,
107
+ `compatibility.visualize_resection_arrays`, `compatibility.visualize_volume`,
108
+ and `compatibility.visualize_solution`. These adapters depend only on this package;
109
+ they never import a planning repository.
110
+
111
+ ## UBD plan extension
112
+
113
+ Every plan archive retains the standard UBD v1 fields:
114
+
115
+ ```text
116
+ format_version, body_data, image_type, mmpd
117
+ ```
118
+
119
+ It adds the following `bcv_` fields:
120
+
121
+ ```text
122
+ bcv_schema_version
123
+ bcv_cutting_planes
124
+ bcv_region_classification
125
+ bcv_spacing_mm
126
+ bcv_origin_lps_mm
127
+ bcv_direction_index_to_lps
128
+ bcv_has_safety_margin
129
+ bcv_safety_planning_volume # present when enabled
130
+ bcv_safety_protected_mask # present when enabled
131
+ bcv_safety_danger_bone_mask # present when enabled
132
+ bcv_safety_margin_mm # present when enabled
133
+ ```
134
+
135
+ Unknown fields are ignored by `UnifiedBodyDataLoader`, so a plan archive remains
136
+ readable as an ordinary mask UBD file. The package verifies that compatibility
137
+ after every save. Because UBD stores one scalar `mmpd`, persisted plans require
138
+ isotropic spacing. Direct in-memory visualization still supports arbitrary valid
139
+ `VolumeGeometry` values.
140
+
141
+ ## Visualization
142
+
143
+ ```python
144
+ from bone_cutting_plane_visualization import load_resection_plan, visualize_resection
145
+
146
+ plan = load_resection_plan("case-plan.ubd.npz")
147
+ visualize_resection(plan)
148
+ ```
149
+
150
+ The final view includes retained normal bone, resected normal bone, tumor,
151
+ optional safety-margin bone, bounded cutting-plane polygons, legend, retention
152
+ rate, and the same camera and visual styling as the source implementation. Every
153
+ VTK scene also includes a compact black orientation cube with white edges and
154
+ `L/R`, `P/A`, and `S/i` face labels. It is scaled from the physical volume bounds
155
+ and placed beyond the negative X/Y side of the LPS bounds, outside the clinical
156
+ geometry and business legend.
157
+
158
+ The CLI accepts only `.ubd.npz` inputs:
159
+
160
+ ```powershell
161
+ bone-cutting-plane-viz view case-labels.ubd.npz
162
+ bone-cutting-plane-viz hull case-labels.ubd.npz
163
+ bone-cutting-plane-viz plan case-plan.ubd.npz
164
+ bone-cutting-plane-viz plan case-plan.ubd.npz --view resected
165
+ bone-cutting-plane-viz plan case-plan.ubd.npz --offscreen --screenshot plan.png
166
+ bone-cutting-plane-viz inspect case-plan.ubd.npz
167
+ ```
168
+
169
+ ## Development
170
+
171
+ ```powershell
172
+ python -m pytest
173
+ ruff check src tests
174
+ ```
@@ -0,0 +1,131 @@
1
+ # bone-cutting-plane-visualization
2
+
3
+ Standalone data contracts and VTK visualization for bone-tumor cutting-plane
4
+ plans. The package is independent of any planning or optimization repository.
5
+
6
+ All persisted inputs use the `.ubd.npz` format from
7
+ [`ct-mri-dicom-nii-reader`](https://pypi.org/project/ct-mri-dicom-nii-reader/).
8
+ Volumes are interpreted in `(Left, Posterior, Superior)` axis order. Standard
9
+ UBD files can be used for the bone/tumor and convex-hull views; a compatible,
10
+ namespaced extension stores a complete resection plan in one UBD archive.
11
+
12
+ ## Installation
13
+
14
+ ```powershell
15
+ python -m pip install -e ".[all]"
16
+ ```
17
+
18
+ The mandatory runtime dependency on `ct-mri-dicom-nii-reader` provides the UBD
19
+ loader and `BodyData` contract. VTK remains optional so data inspection and
20
+ scene preparation work on headless systems:
21
+
22
+ ```powershell
23
+ python -m pip install -e .
24
+ python -m pip install -e ".[viz]"
25
+ ```
26
+
27
+ ## Data contracts
28
+
29
+ The public plan consists of five explicit classes:
30
+
31
+ - `LabeledVolume`: `-1` normal bone, `0` other, `1` tumor.
32
+ - `SelectedCuttingPlanes`: ordered index-space rows `[a, b, c, d]`.
33
+ - `RegionClassificationVolume`: negative retained bone and `2` resected bone.
34
+ - `VolumeGeometry`: spacing, LPS origin, and index-to-LPS direction.
35
+ - `TumorSafetyMarginData`: optional planning volume, protected mask, danger-bone
36
+ mask, and physical margin.
37
+
38
+ `ResectionPlanData` combines them and validates shapes, labels, plane references,
39
+ and safety-margin clearance.
40
+
41
+ ```python
42
+ import numpy as np
43
+
44
+ from bone_cutting_plane_visualization import (
45
+ ResectionPlanData,
46
+ SelectedCuttingPlanes,
47
+ classify_regions,
48
+ load_labeled_volume,
49
+ save_resection_plan,
50
+ )
51
+
52
+ volume, geometry = load_labeled_volume("case-labels.ubd.npz")
53
+ planes = SelectedCuttingPlanes(
54
+ np.asarray([[1.0, 0.0, 0.0, -42.5]], dtype=np.float64)
55
+ )
56
+ classification = classify_regions(volume, planes)
57
+ plan = ResectionPlanData(volume, planes, classification, geometry)
58
+ save_resection_plan(plan, "case-plan.ubd.npz")
59
+ ```
60
+
61
+ Arrays are copied, normalized to stable dtypes, and exposed read-only.
62
+
63
+ Callers migrating from NumPy-based APIs can use `compatibility.plan_from_arrays`,
64
+ `compatibility.visualize_resection_arrays`, `compatibility.visualize_volume`,
65
+ and `compatibility.visualize_solution`. These adapters depend only on this package;
66
+ they never import a planning repository.
67
+
68
+ ## UBD plan extension
69
+
70
+ Every plan archive retains the standard UBD v1 fields:
71
+
72
+ ```text
73
+ format_version, body_data, image_type, mmpd
74
+ ```
75
+
76
+ It adds the following `bcv_` fields:
77
+
78
+ ```text
79
+ bcv_schema_version
80
+ bcv_cutting_planes
81
+ bcv_region_classification
82
+ bcv_spacing_mm
83
+ bcv_origin_lps_mm
84
+ bcv_direction_index_to_lps
85
+ bcv_has_safety_margin
86
+ bcv_safety_planning_volume # present when enabled
87
+ bcv_safety_protected_mask # present when enabled
88
+ bcv_safety_danger_bone_mask # present when enabled
89
+ bcv_safety_margin_mm # present when enabled
90
+ ```
91
+
92
+ Unknown fields are ignored by `UnifiedBodyDataLoader`, so a plan archive remains
93
+ readable as an ordinary mask UBD file. The package verifies that compatibility
94
+ after every save. Because UBD stores one scalar `mmpd`, persisted plans require
95
+ isotropic spacing. Direct in-memory visualization still supports arbitrary valid
96
+ `VolumeGeometry` values.
97
+
98
+ ## Visualization
99
+
100
+ ```python
101
+ from bone_cutting_plane_visualization import load_resection_plan, visualize_resection
102
+
103
+ plan = load_resection_plan("case-plan.ubd.npz")
104
+ visualize_resection(plan)
105
+ ```
106
+
107
+ The final view includes retained normal bone, resected normal bone, tumor,
108
+ optional safety-margin bone, bounded cutting-plane polygons, legend, retention
109
+ rate, and the same camera and visual styling as the source implementation. Every
110
+ VTK scene also includes a compact black orientation cube with white edges and
111
+ `L/R`, `P/A`, and `S/i` face labels. It is scaled from the physical volume bounds
112
+ and placed beyond the negative X/Y side of the LPS bounds, outside the clinical
113
+ geometry and business legend.
114
+
115
+ The CLI accepts only `.ubd.npz` inputs:
116
+
117
+ ```powershell
118
+ bone-cutting-plane-viz view case-labels.ubd.npz
119
+ bone-cutting-plane-viz hull case-labels.ubd.npz
120
+ bone-cutting-plane-viz plan case-plan.ubd.npz
121
+ bone-cutting-plane-viz plan case-plan.ubd.npz --view resected
122
+ bone-cutting-plane-viz plan case-plan.ubd.npz --offscreen --screenshot plan.png
123
+ bone-cutting-plane-viz inspect case-plan.ubd.npz
124
+ ```
125
+
126
+ ## Development
127
+
128
+ ```powershell
129
+ python -m pytest
130
+ ruff check src tests
131
+ ```
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bone-cutting-plane-visualization"
7
+ version = "0.1.0"
8
+ description = "Standalone VTK visualization for bone tumor cutting-plane plans"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ dependencies = [
13
+ "ct-mri-dicom-nii-reader>=0.1.2,<0.2",
14
+ "numpy>=1.26",
15
+ "scipy>=1.11",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ viz = ["vtk>=9.2"]
20
+ dev = ["pytest>=8", "ruff>=0.6"]
21
+ all = ["vtk>=9.2", "pytest>=8", "ruff>=0.6"]
22
+
23
+ [project.scripts]
24
+ bone-cutting-plane-viz = "bone_cutting_plane_visualization.cli:main"
25
+
26
+ [tool.setuptools]
27
+ package-dir = { "" = "src" }
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
31
+ include = ["bone_cutting_plane_visualization*"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ addopts = "-ra"
36
+
37
+ [tool.ruff]
38
+ target-version = "py310"
39
+ line-length = 100
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,61 @@
1
+ """Standalone data contracts and VTK rendering for bone cutting-plane plans."""
2
+
3
+ from .classification import classify_regions
4
+ from .data import (
5
+ LabeledVolume,
6
+ RegionClassificationVolume,
7
+ ResectionPlanData,
8
+ SelectedCuttingPlanes,
9
+ TumorSafetyMarginData,
10
+ VolumeGeometry,
11
+ )
12
+ from .geometry import plane_box_intersection
13
+ from .ubd import (
14
+ IncompleteResectionPlanError,
15
+ load_labeled_volume,
16
+ load_resection_plan,
17
+ save_resection_plan,
18
+ )
19
+ from .visualization import (
20
+ BoneTumorVisualizer,
21
+ MeshLayer,
22
+ PatientOrientationCube,
23
+ PolygonLayer,
24
+ SurfaceLayer,
25
+ VisualizationModel,
26
+ VisualizationScene,
27
+ patient_orientation_cube,
28
+ resection_boundary,
29
+ visualize_bone_and_tumor,
30
+ visualize_labeled_volume,
31
+ visualize_resection,
32
+ visualize_tumor_convex_hull,
33
+ )
34
+
35
+ __all__ = [
36
+ "BoneTumorVisualizer",
37
+ "IncompleteResectionPlanError",
38
+ "LabeledVolume",
39
+ "MeshLayer",
40
+ "PatientOrientationCube",
41
+ "PolygonLayer",
42
+ "RegionClassificationVolume",
43
+ "ResectionPlanData",
44
+ "SelectedCuttingPlanes",
45
+ "SurfaceLayer",
46
+ "TumorSafetyMarginData",
47
+ "VisualizationModel",
48
+ "VisualizationScene",
49
+ "VolumeGeometry",
50
+ "classify_regions",
51
+ "load_labeled_volume",
52
+ "load_resection_plan",
53
+ "patient_orientation_cube",
54
+ "plane_box_intersection",
55
+ "resection_boundary",
56
+ "save_resection_plan",
57
+ "visualize_bone_and_tumor",
58
+ "visualize_labeled_volume",
59
+ "visualize_resection",
60
+ "visualize_tumor_convex_hull",
61
+ ]
@@ -0,0 +1,38 @@
1
+ """Portable region classification matching the historical native implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+ from .data import LabeledVolume, RegionClassificationVolume, SelectedCuttingPlanes
8
+
9
+
10
+ def classify_regions(
11
+ volume: LabeledVolume,
12
+ cutting_planes: SelectedCuttingPlanes,
13
+ *,
14
+ chunk_size: int = 1_000_000,
15
+ ) -> RegionClassificationVolume:
16
+ """Classify normal bone by the first plane whose positive half-space contains it."""
17
+
18
+ if not isinstance(volume, LabeledVolume):
19
+ raise TypeError("volume must be a LabeledVolume")
20
+ if not isinstance(cutting_planes, SelectedCuttingPlanes):
21
+ raise TypeError("cutting_planes must be SelectedCuttingPlanes")
22
+ if not isinstance(chunk_size, int) or isinstance(chunk_size, bool) or chunk_size <= 0:
23
+ raise ValueError("chunk_size must be a positive integer")
24
+
25
+ result = volume.data.astype(np.int32, copy=True)
26
+ result[result == -1] = 2
27
+ flat = result.ravel()
28
+ dimensions = volume.shape
29
+
30
+ for plane_index, equation in enumerate(cutting_planes.equations, start=1):
31
+ remaining = np.flatnonzero(flat == 2)
32
+ for start in range(0, len(remaining), chunk_size):
33
+ positions = remaining[start : start + chunk_size]
34
+ i, j, k = np.unravel_index(positions, dimensions)
35
+ values = i * equation[0] + j * equation[1] + k * equation[2] + equation[3]
36
+ flat[positions[values > 0.0]] = -plane_index
37
+
38
+ return RegionClassificationVolume(result)
@@ -0,0 +1,107 @@
1
+ """Command-line entry points for UBD-backed visualization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from .ubd import IncompleteResectionPlanError, load_labeled_volume, load_resection_plan
9
+ from .visualization import (
10
+ BoneTumorVisualizer,
11
+ visualize_bone_and_tumor,
12
+ visualize_resection,
13
+ visualize_tumor_convex_hull,
14
+ )
15
+
16
+
17
+ def _render_options(args: argparse.Namespace) -> dict:
18
+ return {
19
+ "interactive": not args.offscreen,
20
+ "offscreen": args.offscreen,
21
+ "screenshot": args.screenshot,
22
+ "window_size": tuple(args.window_size),
23
+ }
24
+
25
+
26
+ def _view(args: argparse.Namespace) -> int:
27
+ volume, geometry = load_labeled_volume(args.ubd)
28
+ visualize_bone_and_tumor(volume, geometry=geometry, **_render_options(args))
29
+ return 0
30
+
31
+
32
+ def _hull(args: argparse.Namespace) -> int:
33
+ volume, geometry = load_labeled_volume(args.ubd)
34
+ visualize_tumor_convex_hull(volume, geometry=geometry, **_render_options(args))
35
+ return 0
36
+
37
+
38
+ def _plan(args: argparse.Namespace) -> int:
39
+ plan = load_resection_plan(args.ubd)
40
+ visualize_resection(
41
+ plan,
42
+ show_retained=args.view != "resected",
43
+ boundary_only=args.view == "boundary",
44
+ **_render_options(args),
45
+ )
46
+ return 0
47
+
48
+
49
+ def _inspect(args: argparse.Namespace) -> int:
50
+ try:
51
+ plan = load_resection_plan(args.ubd)
52
+ except IncompleteResectionPlanError:
53
+ volume, geometry = load_labeled_volume(args.ubd)
54
+ print(f"kind=labeled-volume shape={volume.shape} mmpd={geometry.spacing[0]:g}")
55
+ return 0
56
+ print(
57
+ f"kind=resection-plan shape={plan.labeled_volume.shape} "
58
+ f"planes={plan.cutting_planes.count} keep_rate={plan.keep_rate:.6f} "
59
+ f"mmpd={plan.geometry.spacing[0]:g}"
60
+ )
61
+ return 0
62
+
63
+
64
+ def _add_render_arguments(parser: argparse.ArgumentParser) -> None:
65
+ parser.add_argument("--screenshot", type=Path, help="also save the rendered view as PNG")
66
+ parser.add_argument(
67
+ "--offscreen", action="store_true", help="render without opening an interactive window"
68
+ )
69
+ parser.add_argument(
70
+ "--window-size",
71
+ type=int,
72
+ nargs=2,
73
+ default=(1100, 800),
74
+ metavar=("WIDTH", "HEIGHT"),
75
+ )
76
+
77
+
78
+ def build_parser() -> argparse.ArgumentParser:
79
+ parser = argparse.ArgumentParser(prog="bone-cutting-plane-viz")
80
+ subparsers = parser.add_subparsers(dest="command", required=True)
81
+ view = subparsers.add_parser("view", help="visualize bone and tumor from .ubd.npz")
82
+ view.add_argument("ubd", type=Path)
83
+ _add_render_arguments(view)
84
+ view.set_defaults(handler=_view)
85
+ hull = subparsers.add_parser("hull", help="visualize bone, tumor, and tumor hull")
86
+ hull.add_argument("ubd", type=Path)
87
+ _add_render_arguments(hull)
88
+ hull.set_defaults(handler=_hull)
89
+ plan = subparsers.add_parser("plan", help="visualize a complete resection-plan .ubd.npz")
90
+ plan.add_argument("ubd", type=Path)
91
+ plan.add_argument("--view", choices=("kept", "resected", "boundary"), default="kept")
92
+ _add_render_arguments(plan)
93
+ plan.set_defaults(handler=_plan)
94
+ inspect = subparsers.add_parser("inspect", help="inspect a visualization .ubd.npz")
95
+ inspect.add_argument("ubd", type=Path)
96
+ inspect.set_defaults(handler=_inspect)
97
+ return parser
98
+
99
+
100
+ def main(argv: list[str] | None = None) -> int:
101
+ args = build_parser().parse_args(argv)
102
+ if hasattr(args, "window_size") and any(value <= 0 for value in args.window_size):
103
+ raise ValueError("window dimensions must be positive")
104
+ return int(args.handler(args))
105
+
106
+
107
+ __all__ = ["BoneTumorVisualizer", "build_parser", "main"]