cardio 2025.9.0__py3-none-any.whl → 2025.10.1__py3-none-any.whl
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.
- cardio/__init__.py +2 -1
- cardio/app.py +6 -13
- cardio/logic.py +151 -18
- cardio/mesh.py +1 -2
- cardio/object.py +7 -7
- cardio/orientation.py +215 -0
- cardio/scene.py +74 -43
- cardio/segmentation.py +5 -3
- cardio/ui.py +231 -32
- cardio/utils.py +4 -47
- cardio/volume.py +124 -115
- {cardio-2025.9.0.dist-info → cardio-2025.10.1.dist-info}/METADATA +2 -2
- {cardio-2025.9.0.dist-info → cardio-2025.10.1.dist-info}/RECORD +15 -14
- {cardio-2025.9.0.dist-info → cardio-2025.10.1.dist-info}/WHEEL +0 -0
- {cardio-2025.9.0.dist-info → cardio-2025.10.1.dist-info}/entry_points.txt +0 -0
cardio/scene.py
CHANGED
|
@@ -1,54 +1,52 @@
|
|
|
1
1
|
# System
|
|
2
2
|
import logging
|
|
3
3
|
import pathlib as pl
|
|
4
|
-
|
|
5
|
-
# Type adapters for list types to improve CLI integration
|
|
6
|
-
from typing import Annotated
|
|
4
|
+
import typing as ty
|
|
7
5
|
|
|
8
6
|
# Third Party
|
|
9
7
|
import numpy as np
|
|
10
8
|
import pydantic as pc
|
|
11
9
|
import pydantic_settings as ps
|
|
12
10
|
import vtk
|
|
13
|
-
from pydantic import Field, PrivateAttr, TypeAdapter, field_validator, model_validator
|
|
14
11
|
|
|
15
12
|
# Internal
|
|
16
13
|
from .mesh import Mesh
|
|
17
14
|
from .segmentation import Segmentation
|
|
18
15
|
from .types import RGBColor
|
|
16
|
+
from .utils import AngleUnit
|
|
19
17
|
from .volume import Volume
|
|
20
18
|
|
|
21
|
-
MeshListAdapter = TypeAdapter(list[Mesh])
|
|
22
|
-
VolumeListAdapter = TypeAdapter(list[Volume])
|
|
23
|
-
SegmentationListAdapter = TypeAdapter(list[Segmentation])
|
|
19
|
+
MeshListAdapter = pc.TypeAdapter(list[Mesh])
|
|
20
|
+
VolumeListAdapter = pc.TypeAdapter(list[Volume])
|
|
21
|
+
SegmentationListAdapter = pc.TypeAdapter(list[Segmentation])
|
|
24
22
|
|
|
25
23
|
# Create annotated types for better CLI integration
|
|
26
|
-
MeshList = Annotated[
|
|
24
|
+
MeshList = ty.Annotated[
|
|
27
25
|
list[Mesh],
|
|
28
|
-
Field(
|
|
26
|
+
pc.Field(
|
|
29
27
|
description='List of mesh objects. CLI usage: --meshes \'[{"label":"mesh1","directory":"./data/mesh1"}]\''
|
|
30
28
|
),
|
|
31
29
|
]
|
|
32
|
-
VolumeList = Annotated[
|
|
30
|
+
VolumeList = ty.Annotated[
|
|
33
31
|
list[Volume],
|
|
34
|
-
Field(
|
|
32
|
+
pc.Field(
|
|
35
33
|
description='Volume objects. CLI: --volumes \'[{"label":"vol1","directory":"./data/vol1"}]\''
|
|
36
34
|
),
|
|
37
35
|
]
|
|
38
|
-
SegmentationList = Annotated[
|
|
36
|
+
SegmentationList = ty.Annotated[
|
|
39
37
|
list[Segmentation],
|
|
40
|
-
Field(
|
|
38
|
+
pc.Field(
|
|
41
39
|
description='Segmentation objects. CLI: --segmentations \'[{"label":"seg1","directory":"./data/seg1"}]\''
|
|
42
40
|
),
|
|
43
41
|
]
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
class Background(pc.BaseModel):
|
|
47
|
-
light: RGBColor = Field(
|
|
45
|
+
light: RGBColor = pc.Field(
|
|
48
46
|
default=(1.0, 1.0, 1.0),
|
|
49
47
|
description="Background color in light mode. CLI usage: --background.light '[0.8,0.9,1.0]'",
|
|
50
48
|
)
|
|
51
|
-
dark: RGBColor = Field(
|
|
49
|
+
dark: RGBColor = pc.Field(
|
|
52
50
|
default=(0.0, 0.0, 0.0),
|
|
53
51
|
description="Background color in dark mode. CLI usage: --background.dark '[0.1,0.1,0.2]'",
|
|
54
52
|
)
|
|
@@ -92,63 +90,83 @@ class Scene(ps.BaseSettings):
|
|
|
92
90
|
|
|
93
91
|
project_name: str = "Cardio"
|
|
94
92
|
current_frame: int = 0
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
serialization_directory: pl.Path = pc.Field(
|
|
94
|
+
default=pl.Path("./data"),
|
|
95
|
+
description="Base directory for all serialized data (screenshots, exports, etc.)",
|
|
96
|
+
)
|
|
97
|
+
timestamp_format: str = pc.Field(
|
|
98
|
+
default="%Y-%m-%d-%H-%M-%S",
|
|
99
|
+
description="Timestamp format for serialized data subdirectories",
|
|
100
|
+
)
|
|
97
101
|
rotation_factor: float = 3.0
|
|
98
|
-
background: Background = Field(
|
|
102
|
+
background: Background = pc.Field(
|
|
99
103
|
default_factory=Background,
|
|
100
104
|
description='Background colors. CLI usage: \'{"light": [0.8, 0.9, 1.0], "dark": [0.1, 0.1, 0.2]}\'',
|
|
101
105
|
)
|
|
102
|
-
meshes: MeshList = Field(default_factory=list)
|
|
103
|
-
volumes: VolumeList = Field(default_factory=list)
|
|
104
|
-
segmentations: SegmentationList = Field(default_factory=list)
|
|
105
|
-
mpr_enabled: bool = Field(
|
|
106
|
+
meshes: MeshList = pc.Field(default_factory=list)
|
|
107
|
+
volumes: VolumeList = pc.Field(default_factory=list)
|
|
108
|
+
segmentations: SegmentationList = pc.Field(default_factory=list)
|
|
109
|
+
mpr_enabled: bool = pc.Field(
|
|
106
110
|
default=False,
|
|
107
111
|
description="Enable multi-planar reconstruction (MPR) mode with quad-view layout",
|
|
108
112
|
)
|
|
109
|
-
active_volume_label: str = Field(
|
|
113
|
+
active_volume_label: str = pc.Field(
|
|
110
114
|
default="",
|
|
111
115
|
description="Label of the volume to use for multi-planar reconstruction",
|
|
112
116
|
)
|
|
113
|
-
axial_slice: float = Field(
|
|
114
|
-
default=0.
|
|
117
|
+
axial_slice: float = pc.Field(
|
|
118
|
+
default=0.0,
|
|
119
|
+
description="Axial slice position in physical coordinates (LAS Z axis)",
|
|
115
120
|
)
|
|
116
|
-
sagittal_slice: float = Field(
|
|
117
|
-
default=0.
|
|
121
|
+
sagittal_slice: float = pc.Field(
|
|
122
|
+
default=0.0,
|
|
123
|
+
description="Sagittal slice position in physical coordinates (LAS X axis)",
|
|
118
124
|
)
|
|
119
|
-
coronal_slice: float = Field(
|
|
120
|
-
default=0.
|
|
125
|
+
coronal_slice: float = pc.Field(
|
|
126
|
+
default=0.0,
|
|
127
|
+
description="Coronal slice position in physical coordinates (LAS Y axis)",
|
|
121
128
|
)
|
|
122
|
-
mpr_window: float = Field(
|
|
129
|
+
mpr_window: float = pc.Field(
|
|
123
130
|
default=800.0, description="Window width for MPR image display"
|
|
124
131
|
)
|
|
125
|
-
mpr_level: float = Field(
|
|
132
|
+
mpr_level: float = pc.Field(
|
|
126
133
|
default=200.0, description="Window level for MPR image display"
|
|
127
134
|
)
|
|
128
|
-
mpr_window_level_preset: int = Field(
|
|
135
|
+
mpr_window_level_preset: int = pc.Field(
|
|
129
136
|
default=7, description="Window/level preset key for MPR views"
|
|
130
137
|
)
|
|
131
|
-
mpr_rotation_sequence: list = Field(
|
|
138
|
+
mpr_rotation_sequence: list = pc.Field(
|
|
132
139
|
default_factory=list,
|
|
133
140
|
description="Dynamic rotation sequence for MPR views - list of rotation steps",
|
|
134
141
|
)
|
|
142
|
+
max_mpr_rotations: int = pc.Field(
|
|
143
|
+
default=20,
|
|
144
|
+
description="Maximum number of MPR rotations supported",
|
|
145
|
+
)
|
|
146
|
+
angle_units: AngleUnit = pc.Field(
|
|
147
|
+
default=AngleUnit.DEGREES,
|
|
148
|
+
description="Units for angle measurements in rotation serialization",
|
|
149
|
+
)
|
|
150
|
+
coordinate_system: str = pc.Field(
|
|
151
|
+
default="LAS", description="Coordinate system orientation (e.g., LAS, RAS, LPS)"
|
|
152
|
+
)
|
|
135
153
|
|
|
136
154
|
# Field validators for JSON string inputs
|
|
137
|
-
@field_validator("meshes", mode="before")
|
|
155
|
+
@pc.field_validator("meshes", mode="before")
|
|
138
156
|
@classmethod
|
|
139
157
|
def validate_meshes(cls, v):
|
|
140
158
|
if isinstance(v, str):
|
|
141
159
|
return MeshListAdapter.validate_json(v)
|
|
142
160
|
return v
|
|
143
161
|
|
|
144
|
-
@field_validator("volumes", mode="before")
|
|
162
|
+
@pc.field_validator("volumes", mode="before")
|
|
145
163
|
@classmethod
|
|
146
164
|
def validate_volumes(cls, v):
|
|
147
165
|
if isinstance(v, str):
|
|
148
166
|
return VolumeListAdapter.validate_json(v)
|
|
149
167
|
return v
|
|
150
168
|
|
|
151
|
-
@field_validator("segmentations", mode="before")
|
|
169
|
+
@pc.field_validator("segmentations", mode="before")
|
|
152
170
|
@classmethod
|
|
153
171
|
def validate_segmentations(cls, v):
|
|
154
172
|
if isinstance(v, str):
|
|
@@ -156,18 +174,18 @@ class Scene(ps.BaseSettings):
|
|
|
156
174
|
return v
|
|
157
175
|
|
|
158
176
|
# VTK objects as private attributes
|
|
159
|
-
_renderer: vtk.vtkRenderer = PrivateAttr(default_factory=vtk.vtkRenderer)
|
|
160
|
-
_renderWindow: vtk.vtkRenderWindow = PrivateAttr(
|
|
177
|
+
_renderer: vtk.vtkRenderer = pc.PrivateAttr(default_factory=vtk.vtkRenderer)
|
|
178
|
+
_renderWindow: vtk.vtkRenderWindow = pc.PrivateAttr(
|
|
161
179
|
default_factory=vtk.vtkRenderWindow
|
|
162
180
|
)
|
|
163
|
-
_renderWindowInteractor: vtk.vtkRenderWindowInteractor = PrivateAttr(
|
|
181
|
+
_renderWindowInteractor: vtk.vtkRenderWindowInteractor = pc.PrivateAttr(
|
|
164
182
|
default_factory=vtk.vtkRenderWindowInteractor
|
|
165
183
|
)
|
|
166
184
|
|
|
167
185
|
# MPR render windows
|
|
168
|
-
_axial_renderWindow: vtk.vtkRenderWindow = PrivateAttr(default=None)
|
|
169
|
-
_coronal_renderWindow: vtk.vtkRenderWindow = PrivateAttr(default=None)
|
|
170
|
-
_sagittal_renderWindow: vtk.vtkRenderWindow = PrivateAttr(default=None)
|
|
186
|
+
_axial_renderWindow: vtk.vtkRenderWindow = pc.PrivateAttr(default=None)
|
|
187
|
+
_coronal_renderWindow: vtk.vtkRenderWindow = pc.PrivateAttr(default=None)
|
|
188
|
+
_sagittal_renderWindow: vtk.vtkRenderWindow = pc.PrivateAttr(default=None)
|
|
171
189
|
|
|
172
190
|
@property
|
|
173
191
|
def renderer(self) -> vtk.vtkRenderer:
|
|
@@ -193,7 +211,7 @@ class Scene(ps.BaseSettings):
|
|
|
193
211
|
def sagittal_renderWindow(self) -> vtk.vtkRenderWindow:
|
|
194
212
|
return self._sagittal_renderWindow
|
|
195
213
|
|
|
196
|
-
@model_validator(mode="after")
|
|
214
|
+
@pc.model_validator(mode="after")
|
|
197
215
|
def setup_scene(self):
|
|
198
216
|
# Validate unique labels
|
|
199
217
|
self._validate_unique_labels()
|
|
@@ -264,6 +282,16 @@ class Scene(ps.BaseSettings):
|
|
|
264
282
|
"Active volume label specified but no volumes are available"
|
|
265
283
|
)
|
|
266
284
|
|
|
285
|
+
@property
|
|
286
|
+
def screenshot_directory(self) -> pl.Path:
|
|
287
|
+
"""Computed property that returns the screenshots subdirectory."""
|
|
288
|
+
return self.serialization_directory / "screenshots"
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def rotations_directory(self) -> pl.Path:
|
|
292
|
+
"""Computed property that returns the rotations subdirectory."""
|
|
293
|
+
return self.serialization_directory / "rotations"
|
|
294
|
+
|
|
267
295
|
@property
|
|
268
296
|
def nframes(self) -> int:
|
|
269
297
|
ns = []
|
|
@@ -316,6 +344,7 @@ class Scene(ps.BaseSettings):
|
|
|
316
344
|
|
|
317
345
|
# Create and set interactor for axial
|
|
318
346
|
axial_interactor = vtk.vtkRenderWindowInteractor()
|
|
347
|
+
axial_interactor.SetInteractorStyle(vtk.vtkInteractorStyle())
|
|
319
348
|
self._axial_renderWindow.SetInteractor(axial_interactor)
|
|
320
349
|
|
|
321
350
|
# Create coronal render window
|
|
@@ -327,6 +356,7 @@ class Scene(ps.BaseSettings):
|
|
|
327
356
|
|
|
328
357
|
# Create and set interactor for coronal
|
|
329
358
|
coronal_interactor = vtk.vtkRenderWindowInteractor()
|
|
359
|
+
coronal_interactor.SetInteractorStyle(vtk.vtkInteractorStyle())
|
|
330
360
|
self._coronal_renderWindow.SetInteractor(coronal_interactor)
|
|
331
361
|
|
|
332
362
|
# Create sagittal render window
|
|
@@ -338,6 +368,7 @@ class Scene(ps.BaseSettings):
|
|
|
338
368
|
|
|
339
369
|
# Create and set interactor for sagittal
|
|
340
370
|
sagittal_interactor = vtk.vtkRenderWindowInteractor()
|
|
371
|
+
sagittal_interactor.SetInteractorStyle(vtk.vtkInteractorStyle())
|
|
341
372
|
self._sagittal_renderWindow.SetInteractor(sagittal_interactor)
|
|
342
373
|
|
|
343
374
|
def hide_all_frames(self):
|
cardio/segmentation.py
CHANGED
|
@@ -8,16 +8,18 @@ import pydantic as pc
|
|
|
8
8
|
import vtk
|
|
9
9
|
|
|
10
10
|
# Internal
|
|
11
|
+
from .orientation import (
|
|
12
|
+
reset_direction,
|
|
13
|
+
)
|
|
11
14
|
from .object import Object
|
|
12
15
|
from .property_config import vtkPropertyConfig
|
|
13
|
-
from .utils import InterpolatorType, reset_direction
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class Segmentation(Object):
|
|
17
19
|
"""Segmentation object with multi-label mesh extraction using SurfaceNets."""
|
|
18
20
|
|
|
19
21
|
pattern: str = pc.Field(
|
|
20
|
-
default="
|
|
22
|
+
default="{frame}.nii.gz",
|
|
21
23
|
description="Filename pattern with $frame placeholder",
|
|
22
24
|
)
|
|
23
25
|
_actors: list[vtk.vtkActor] = pc.PrivateAttr(default_factory=list)
|
|
@@ -35,7 +37,7 @@ class Segmentation(Object):
|
|
|
35
37
|
|
|
36
38
|
# Read and process segmentation image
|
|
37
39
|
image = itk.imread(path)
|
|
38
|
-
image = reset_direction(image
|
|
40
|
+
image = reset_direction(image)
|
|
39
41
|
vtk_image = itk.vtk_image_from_image(image)
|
|
40
42
|
|
|
41
43
|
# Create SurfaceNets3D filter
|
cardio/ui.py
CHANGED
|
@@ -1,15 +1,140 @@
|
|
|
1
|
+
import functools as ft
|
|
2
|
+
|
|
1
3
|
from trame.ui.vuetify3 import SinglePageWithDrawerLayout
|
|
2
4
|
from trame.widgets import vtk as vtk_widgets
|
|
3
5
|
from trame.widgets import vuetify3 as vuetify
|
|
4
6
|
|
|
5
7
|
from .scene import Scene
|
|
6
8
|
from .volume_property_presets import list_volume_property_presets
|
|
9
|
+
from .window_level import presets
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class UI:
|
|
13
|
+
@property
|
|
14
|
+
def handled_events(self):
|
|
15
|
+
return [
|
|
16
|
+
"MouseMove",
|
|
17
|
+
"LeftButtonPress",
|
|
18
|
+
"LeftButtonRelease",
|
|
19
|
+
"RightButtonPress",
|
|
20
|
+
"RightButtonRelease",
|
|
21
|
+
"KeyPress",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def event_listeners_for_view(self, view_name):
|
|
25
|
+
"""Create event listeners for a specific view."""
|
|
26
|
+
result = {}
|
|
27
|
+
for event in self.handled_events:
|
|
28
|
+
callback = ft.partial(self.on_event, view_name=view_name)
|
|
29
|
+
result[event] = (callback, "[utils.vtk.event($event)]")
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
def on_event(self, *args, view_name=None, **kwargs):
|
|
33
|
+
if not args:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
event = args[0]
|
|
37
|
+
|
|
38
|
+
match event["type"]:
|
|
39
|
+
case "KeyPress":
|
|
40
|
+
if event["key"].isdigit():
|
|
41
|
+
key_num = int(event["key"])
|
|
42
|
+
if key_num in presets.keys():
|
|
43
|
+
self.server.state.mpr_window_level_preset = key_num
|
|
44
|
+
|
|
45
|
+
case "LeftButtonPress":
|
|
46
|
+
self.left_dragging = True
|
|
47
|
+
self._store_mouse_position(view_name, event)
|
|
48
|
+
|
|
49
|
+
case "LeftButtonRelease":
|
|
50
|
+
self.left_dragging = False
|
|
51
|
+
|
|
52
|
+
case "RightButtonPress":
|
|
53
|
+
self.right_dragging = True
|
|
54
|
+
self._store_mouse_position(view_name, event)
|
|
55
|
+
|
|
56
|
+
case "RightButtonRelease":
|
|
57
|
+
self.right_dragging = False
|
|
58
|
+
|
|
59
|
+
case "MouseMove" if self.left_dragging:
|
|
60
|
+
if (
|
|
61
|
+
view_name in {"axial", "sagittal", "coronal"}
|
|
62
|
+
and view_name in self.last_mouse_pos
|
|
63
|
+
and "position" in event
|
|
64
|
+
):
|
|
65
|
+
current_pos = [event["position"]["x"], event["position"]["y"]]
|
|
66
|
+
dx = current_pos[0] - self.last_mouse_pos[view_name][0]
|
|
67
|
+
dy = current_pos[1] - self.last_mouse_pos[view_name][1]
|
|
68
|
+
self.last_mouse_pos[view_name] = current_pos
|
|
69
|
+
|
|
70
|
+
current_window = getattr(self.server.state, "mpr_window", 400.0)
|
|
71
|
+
current_level = getattr(self.server.state, "mpr_level", 40.0)
|
|
72
|
+
|
|
73
|
+
window_delta = -dx * self.window_sensitivity
|
|
74
|
+
level_delta = -dy * self.level_sensitivity
|
|
75
|
+
new_window = max(1.0, current_window + window_delta)
|
|
76
|
+
new_level = current_level + level_delta
|
|
77
|
+
|
|
78
|
+
self.server.state.mpr_window = new_window
|
|
79
|
+
self.server.state.mpr_level = new_level
|
|
80
|
+
|
|
81
|
+
case "MouseMove" if self.right_dragging:
|
|
82
|
+
if (
|
|
83
|
+
view_name in {"axial", "sagittal", "coronal"}
|
|
84
|
+
and view_name in self.last_mouse_pos
|
|
85
|
+
and "position" in event
|
|
86
|
+
):
|
|
87
|
+
current_pos = [event["position"]["x"], event["position"]["y"]]
|
|
88
|
+
dy = current_pos[1] - self.last_mouse_pos[view_name][1]
|
|
89
|
+
self.last_mouse_pos[view_name] = current_pos
|
|
90
|
+
|
|
91
|
+
base_slice_delta = dy * self.slice_sensitivity
|
|
92
|
+
self._handle_slice_scroll(view_name, base_slice_delta)
|
|
93
|
+
|
|
94
|
+
def _store_mouse_position(self, view_name, event):
|
|
95
|
+
"""Store mouse position for drag operations."""
|
|
96
|
+
if view_name and "position" in event:
|
|
97
|
+
self.last_mouse_pos[view_name] = [
|
|
98
|
+
event["position"]["x"],
|
|
99
|
+
event["position"]["y"],
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
def _handle_slice_scroll(self, view_name, base_slice_delta):
|
|
103
|
+
"""Handle slice scrolling for a specific view."""
|
|
104
|
+
match view_name:
|
|
105
|
+
case "axial":
|
|
106
|
+
current_slice = getattr(self.server.state, "axial_slice", 0.0)
|
|
107
|
+
bounds = getattr(self.server.state, "axial_slice_bounds", [0.0, 100.0])
|
|
108
|
+
slice_attr = "axial_slice"
|
|
109
|
+
case "sagittal":
|
|
110
|
+
current_slice = getattr(self.server.state, "sagittal_slice", 0.0)
|
|
111
|
+
bounds = getattr(
|
|
112
|
+
self.server.state, "sagittal_slice_bounds", [0.0, 100.0]
|
|
113
|
+
)
|
|
114
|
+
slice_attr = "sagittal_slice"
|
|
115
|
+
case "coronal":
|
|
116
|
+
current_slice = getattr(self.server.state, "coronal_slice", 0.0)
|
|
117
|
+
bounds = getattr(
|
|
118
|
+
self.server.state, "coronal_slice_bounds", [0.0, 100.0]
|
|
119
|
+
)
|
|
120
|
+
slice_attr = "coronal_slice"
|
|
121
|
+
case _:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
min_bound, max_bound = min(bounds), max(bounds)
|
|
125
|
+
slice_delta = base_slice_delta if bounds[1] > bounds[0] else -base_slice_delta
|
|
126
|
+
new_slice = max(min_bound, min(max_bound, current_slice + slice_delta))
|
|
127
|
+
setattr(self.server.state, slice_attr, new_slice)
|
|
128
|
+
|
|
10
129
|
def __init__(self, server, scene: Scene):
|
|
11
130
|
self.server = server
|
|
12
131
|
self.scene: Scene = scene
|
|
132
|
+
self.left_dragging = False
|
|
133
|
+
self.right_dragging = False
|
|
134
|
+
self.last_mouse_pos = {}
|
|
135
|
+
self.window_sensitivity = 5.0
|
|
136
|
+
self.level_sensitivity = 2.0
|
|
137
|
+
self.slice_sensitivity = 1.0
|
|
13
138
|
|
|
14
139
|
self.setup()
|
|
15
140
|
|
|
@@ -63,7 +188,10 @@ class UI:
|
|
|
63
188
|
classes="pa-0 fill-height",
|
|
64
189
|
):
|
|
65
190
|
view = vtk_widgets.VtkRemoteView(
|
|
66
|
-
self.scene.renderWindow,
|
|
191
|
+
self.scene.renderWindow,
|
|
192
|
+
interactor_events=("event_types", self.handled_events),
|
|
193
|
+
**self.event_listeners_for_view("volume"),
|
|
194
|
+
interactive_ratio=1,
|
|
67
195
|
)
|
|
68
196
|
self.server.controller.view_update = view.update
|
|
69
197
|
self.server.controller.view_reset_camera = view.reset_camera
|
|
@@ -88,15 +216,19 @@ class UI:
|
|
|
88
216
|
axial_view = vtk_widgets.VtkRemoteView(
|
|
89
217
|
self.scene.axial_renderWindow,
|
|
90
218
|
style="height: 100%; width: 100%;",
|
|
219
|
+
interactor_events=("event_types", self.handled_events),
|
|
220
|
+
**self.event_listeners_for_view("axial"),
|
|
91
221
|
interactive_ratio=1,
|
|
92
222
|
)
|
|
93
223
|
with vuetify.VCol(
|
|
94
224
|
cols="6", classes="pa-1", style="height: 100%;"
|
|
95
225
|
):
|
|
96
|
-
# Volume view
|
|
226
|
+
# Volume view
|
|
97
227
|
volume_view = vtk_widgets.VtkRemoteView(
|
|
98
228
|
self.scene.renderWindow,
|
|
99
229
|
style="height: 100%; width: 100%;",
|
|
230
|
+
interactor_events=("event_types", self.handled_events),
|
|
231
|
+
**self.event_listeners_for_view("volume_mpr"),
|
|
100
232
|
interactive_ratio=1,
|
|
101
233
|
)
|
|
102
234
|
|
|
@@ -109,6 +241,8 @@ class UI:
|
|
|
109
241
|
coronal_view = vtk_widgets.VtkRemoteView(
|
|
110
242
|
self.scene.coronal_renderWindow,
|
|
111
243
|
style="height: 100%; width: 100%;",
|
|
244
|
+
interactor_events=("event_types", self.handled_events),
|
|
245
|
+
**self.event_listeners_for_view("coronal"),
|
|
112
246
|
interactive_ratio=1,
|
|
113
247
|
)
|
|
114
248
|
with vuetify.VCol(
|
|
@@ -118,6 +252,8 @@ class UI:
|
|
|
118
252
|
sagittal_view = vtk_widgets.VtkRemoteView(
|
|
119
253
|
self.scene.sagittal_renderWindow,
|
|
120
254
|
style="height: 100%; width: 100%;",
|
|
255
|
+
interactor_events=("event_types", self.handled_events),
|
|
256
|
+
**self.event_listeners_for_view("sagittal"),
|
|
121
257
|
interactive_ratio=1,
|
|
122
258
|
)
|
|
123
259
|
|
|
@@ -207,10 +343,9 @@ class UI:
|
|
|
207
343
|
|
|
208
344
|
vuetify.VSlider(
|
|
209
345
|
v_if="mpr_enabled && active_volume_label",
|
|
210
|
-
v_model=("axial_slice", 0.
|
|
211
|
-
min=0.0,
|
|
212
|
-
max=1.0,
|
|
213
|
-
step=0.01,
|
|
346
|
+
v_model=("axial_slice", 0.0),
|
|
347
|
+
min=("axial_slice_bounds[0]", 0.0),
|
|
348
|
+
max=("axial_slice_bounds[1]", 100.0),
|
|
214
349
|
hint="A (I ↔ S)",
|
|
215
350
|
persistent_hint=True,
|
|
216
351
|
dense=True,
|
|
@@ -220,10 +355,9 @@ class UI:
|
|
|
220
355
|
|
|
221
356
|
vuetify.VSlider(
|
|
222
357
|
v_if="mpr_enabled && active_volume_label",
|
|
223
|
-
v_model=("sagittal_slice", 0.
|
|
224
|
-
min=0.0,
|
|
225
|
-
max=1.0,
|
|
226
|
-
step=0.01,
|
|
358
|
+
v_model=("sagittal_slice", 0.0),
|
|
359
|
+
min=("sagittal_slice_bounds[0]", 0.0),
|
|
360
|
+
max=("sagittal_slice_bounds[1]", 100.0),
|
|
227
361
|
hint="S (R ↔ L)",
|
|
228
362
|
persistent_hint=True,
|
|
229
363
|
dense=True,
|
|
@@ -233,10 +367,9 @@ class UI:
|
|
|
233
367
|
|
|
234
368
|
vuetify.VSlider(
|
|
235
369
|
v_if="mpr_enabled && active_volume_label",
|
|
236
|
-
v_model=("coronal_slice", 0.
|
|
237
|
-
min=0.0,
|
|
238
|
-
max=1.0,
|
|
239
|
-
step=0.01,
|
|
370
|
+
v_model=("coronal_slice", 0.0),
|
|
371
|
+
min=("coronal_slice_bounds[0]", 0.0),
|
|
372
|
+
max=("coronal_slice_bounds[1]", 100.0),
|
|
240
373
|
hint="C (P ↔ A)",
|
|
241
374
|
persistent_hint=True,
|
|
242
375
|
dense=True,
|
|
@@ -262,6 +395,7 @@ class UI:
|
|
|
262
395
|
small=True,
|
|
263
396
|
dense=True,
|
|
264
397
|
outlined=True,
|
|
398
|
+
color="primary",
|
|
265
399
|
)
|
|
266
400
|
with vuetify.VCol(cols="4"):
|
|
267
401
|
vuetify.VBtn(
|
|
@@ -270,6 +404,7 @@ class UI:
|
|
|
270
404
|
small=True,
|
|
271
405
|
dense=True,
|
|
272
406
|
outlined=True,
|
|
407
|
+
color="primary",
|
|
273
408
|
)
|
|
274
409
|
with vuetify.VCol(cols="4"):
|
|
275
410
|
vuetify.VBtn(
|
|
@@ -278,6 +413,7 @@ class UI:
|
|
|
278
413
|
small=True,
|
|
279
414
|
dense=True,
|
|
280
415
|
outlined=True,
|
|
416
|
+
color="primary",
|
|
281
417
|
)
|
|
282
418
|
|
|
283
419
|
# Reset rotations button
|
|
@@ -291,22 +427,84 @@ class UI:
|
|
|
291
427
|
color="warning",
|
|
292
428
|
block=True,
|
|
293
429
|
classes="mb-2",
|
|
430
|
+
prepend_icon="mdi-refresh",
|
|
294
431
|
)
|
|
295
432
|
|
|
296
|
-
# Individual rotation sliders
|
|
297
|
-
for i in range(
|
|
298
|
-
vuetify.
|
|
433
|
+
# Individual rotation sliders
|
|
434
|
+
for i in range(self.scene.max_mpr_rotations):
|
|
435
|
+
with vuetify.VRow(
|
|
299
436
|
v_if=f"mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > {i}",
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
437
|
+
no_gutters=True,
|
|
438
|
+
classes="align-center mb-1",
|
|
439
|
+
):
|
|
440
|
+
with vuetify.VCol(cols="8"):
|
|
441
|
+
vuetify.VSlider(
|
|
442
|
+
v_model=(f"mpr_rotation_angle_{i}", 0),
|
|
443
|
+
min=-180,
|
|
444
|
+
max=180,
|
|
445
|
+
step=1,
|
|
446
|
+
hint=(
|
|
447
|
+
f"mpr_rotation_axis_{i}",
|
|
448
|
+
f"Rotation {i + 1}",
|
|
449
|
+
),
|
|
450
|
+
persistent_hint=True,
|
|
451
|
+
dense=True,
|
|
452
|
+
hide_details=False,
|
|
453
|
+
thumb_label=True,
|
|
454
|
+
)
|
|
455
|
+
with vuetify.VCol(cols="2"):
|
|
456
|
+
vuetify.VCheckbox(
|
|
457
|
+
v_model=(f"mpr_rotation_visible_{i}", True),
|
|
458
|
+
true_icon="mdi-eye",
|
|
459
|
+
false_icon="mdi-eye-off",
|
|
460
|
+
hide_details=True,
|
|
461
|
+
dense=True,
|
|
462
|
+
title="Toggle this rotation and all subsequent ones",
|
|
463
|
+
)
|
|
464
|
+
with vuetify.VCol(cols="2"):
|
|
465
|
+
vuetify.VBtn(
|
|
466
|
+
icon="mdi-delete",
|
|
467
|
+
click=ft.partial(
|
|
468
|
+
self.server.controller.remove_rotation_event, i
|
|
469
|
+
),
|
|
470
|
+
small=True,
|
|
471
|
+
dense=True,
|
|
472
|
+
color="error",
|
|
473
|
+
title="Remove this rotation and all subsequent ones",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Angle units selector
|
|
477
|
+
with vuetify.VRow(
|
|
478
|
+
v_if="mpr_enabled && active_volume_label",
|
|
479
|
+
no_gutters=True,
|
|
480
|
+
classes="align-center mb-2 mt-2",
|
|
481
|
+
):
|
|
482
|
+
with vuetify.VCol(cols="4"):
|
|
483
|
+
vuetify.VLabel("Units:")
|
|
484
|
+
with vuetify.VCol(cols="8"):
|
|
485
|
+
vuetify.VSelect(
|
|
486
|
+
v_model=("angle_units", "degrees"),
|
|
487
|
+
items=("angle_units_items", []),
|
|
488
|
+
item_title="text",
|
|
489
|
+
item_value="value",
|
|
490
|
+
dense=True,
|
|
491
|
+
hide_details=True,
|
|
492
|
+
outlined=True,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# Save rotations button
|
|
496
|
+
vuetify.VBtn(
|
|
497
|
+
"Save Rotations",
|
|
498
|
+
v_if="mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > 0",
|
|
499
|
+
click=self.server.controller.save_rotation_angles,
|
|
500
|
+
small=True,
|
|
501
|
+
dense=True,
|
|
502
|
+
outlined=True,
|
|
503
|
+
color="success",
|
|
504
|
+
block=True,
|
|
505
|
+
classes="mb-2",
|
|
506
|
+
prepend_icon="mdi-content-save",
|
|
507
|
+
)
|
|
310
508
|
|
|
311
509
|
vuetify.VDivider(classes="my-2")
|
|
312
510
|
|
|
@@ -412,10 +610,15 @@ class UI:
|
|
|
412
610
|
|
|
413
611
|
with vuetify.VRow(justify="center", classes="my-3"):
|
|
414
612
|
vuetify.VBtn(
|
|
415
|
-
|
|
613
|
+
"Capture Cine",
|
|
416
614
|
small=True,
|
|
615
|
+
dense=True,
|
|
616
|
+
outlined=True,
|
|
617
|
+
color="info",
|
|
618
|
+
block=True,
|
|
417
619
|
click=self.server.controller.screenshot,
|
|
418
620
|
title=f"Capture cine to {self.scene.screenshot_directory}",
|
|
621
|
+
prepend_icon="mdi-video",
|
|
419
622
|
)
|
|
420
623
|
|
|
421
624
|
vuetify.VListSubheader("Appearance and Visibility")
|
|
@@ -518,10 +721,6 @@ class UI:
|
|
|
518
721
|
|
|
519
722
|
# Preset selection in collapsible panel
|
|
520
723
|
available_presets = list_volume_property_presets()
|
|
521
|
-
current_preset = self.server.state[f"volume_preset_{v.label}"]
|
|
522
|
-
current_desc = available_presets.get(
|
|
523
|
-
current_preset, current_preset
|
|
524
|
-
)
|
|
525
724
|
|
|
526
725
|
with vuetify.VExpansionPanels(
|
|
527
726
|
v_model=f"preset_panel_{v.label}",
|