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.
- bone_cutting_plane_visualization-0.1.0/LICENSE +21 -0
- bone_cutting_plane_visualization-0.1.0/PKG-INFO +174 -0
- bone_cutting_plane_visualization-0.1.0/README.md +131 -0
- bone_cutting_plane_visualization-0.1.0/pyproject.toml +39 -0
- bone_cutting_plane_visualization-0.1.0/setup.cfg +4 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/__init__.py +61 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/classification.py +38 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/cli.py +107 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/compatibility.py +171 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/data.py +334 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/geometry.py +69 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/ubd.py +158 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization/visualization.py +762 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/PKG-INFO +174 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/SOURCES.txt +22 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/dependency_links.txt +1 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/entry_points.txt +2 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/requires.txt +15 -0
- bone_cutting_plane_visualization-0.1.0/src/bone_cutting_plane_visualization.egg-info/top_level.txt +1 -0
- bone_cutting_plane_visualization-0.1.0/tests/test_cli.py +31 -0
- bone_cutting_plane_visualization-0.1.0/tests/test_compatibility.py +52 -0
- bone_cutting_plane_visualization-0.1.0/tests/test_data.py +149 -0
- bone_cutting_plane_visualization-0.1.0/tests/test_ubd.py +69 -0
- 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,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"]
|