cardio 2025.8.0__py3-none-any.whl → 2025.9.0__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 +8 -7
- cardio/app.py +1 -1
- cardio/blend_transfer_functions.py +84 -0
- cardio/color_transfer_function.py +29 -0
- cardio/logic.py +415 -5
- cardio/mesh.py +2 -2
- cardio/object.py +1 -6
- cardio/piecewise_function.py +29 -0
- cardio/scene.py +132 -1
- cardio/transfer_function_pair.py +25 -0
- cardio/ui.py +296 -60
- cardio/volume.py +216 -3
- cardio/volume_property.py +46 -0
- cardio/volume_property_presets.py +53 -0
- cardio/window_level.py +35 -0
- {cardio-2025.8.0.dist-info → cardio-2025.9.0.dist-info}/METADATA +16 -6
- cardio-2025.9.0.dist-info/RECORD +28 -0
- cardio/transfer_functions.py +0 -272
- cardio-2025.8.0.dist-info/RECORD +0 -22
- {cardio-2025.8.0.dist-info → cardio-2025.9.0.dist-info}/WHEEL +0 -0
- {cardio-2025.8.0.dist-info → cardio-2025.9.0.dist-info}/entry_points.txt +0 -0
cardio/ui.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
from trame.ui.
|
1
|
+
from trame.ui.vuetify3 import SinglePageWithDrawerLayout
|
2
2
|
from trame.widgets import vtk as vtk_widgets
|
3
|
-
from trame.widgets import vuetify
|
3
|
+
from trame.widgets import vuetify3 as vuetify
|
4
4
|
|
5
5
|
from .scene import Scene
|
6
|
-
from .
|
6
|
+
from .volume_property_presets import list_volume_property_presets
|
7
7
|
|
8
8
|
|
9
9
|
class UI:
|
@@ -16,7 +16,9 @@ class UI:
|
|
16
16
|
def setup(self):
|
17
17
|
self.server.state.trame__title = self.scene.project_name
|
18
18
|
|
19
|
-
with SinglePageWithDrawerLayout(
|
19
|
+
with SinglePageWithDrawerLayout(
|
20
|
+
self.server, theme=("theme_mode", "dark")
|
21
|
+
) as layout:
|
20
22
|
layout.icon.click = self.server.controller.view_reset_camera
|
21
23
|
layout.title.set_text(self.scene.project_name)
|
22
24
|
|
@@ -25,36 +27,24 @@ class UI:
|
|
25
27
|
|
26
28
|
vuetify.VSpacer()
|
27
29
|
|
28
|
-
# vuetify.VCheckbox(
|
29
|
-
# v_model=("viewMode", "local"),
|
30
|
-
# on_icon="mdi-lan-disconnect",
|
31
|
-
# off_icon="mdi-lan-connect",
|
32
|
-
# true_value="local",
|
33
|
-
# false_value="remote",
|
34
|
-
# classes="mx-1",
|
35
|
-
# hide_details=True,
|
36
|
-
# dense=True,
|
37
|
-
# )
|
38
|
-
|
39
30
|
vuetify.VCheckbox(
|
40
|
-
v_model=("
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
31
|
+
v_model=("theme_mode", "dark"),
|
32
|
+
true_value="dark",
|
33
|
+
false_value="light",
|
34
|
+
label="Dark Mode",
|
35
|
+
true_icon="mdi-lightbulb-off-outline",
|
36
|
+
false_icon="mdi-lightbulb-outline",
|
37
|
+
density="compact",
|
38
|
+
style="max-width: 150px;",
|
48
39
|
)
|
49
40
|
|
50
|
-
#
|
41
|
+
# Close button
|
51
42
|
vuetify.VCheckbox(
|
52
43
|
value=False,
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
click=self.server.controller.reset_all,
|
44
|
+
true_icon="mdi-close-circle",
|
45
|
+
false_icon="mdi-close-circle",
|
46
|
+
label="Close Application",
|
47
|
+
click=self.server.controller.close_application,
|
58
48
|
readonly=True,
|
59
49
|
)
|
60
50
|
|
@@ -66,26 +56,261 @@ class UI:
|
|
66
56
|
)
|
67
57
|
|
68
58
|
with layout.content:
|
59
|
+
# Single VR view (default mode)
|
69
60
|
with vuetify.VContainer(
|
61
|
+
v_if="!mpr_enabled",
|
70
62
|
fluid=True,
|
71
63
|
classes="pa-0 fill-height",
|
72
64
|
):
|
73
65
|
view = vtk_widgets.VtkRemoteView(
|
74
66
|
self.scene.renderWindow, interactive_ratio=1
|
75
67
|
)
|
76
|
-
# view = vtk_widgets.VtkLocalView(renderWindow)
|
77
|
-
# view = vtk_widgets.VtkRemoteLocalView(
|
78
|
-
# scene.renderWindow,
|
79
|
-
# namespace="view",
|
80
|
-
# mode="local",
|
81
|
-
# interactive_ratio=1,
|
82
|
-
# )
|
83
68
|
self.server.controller.view_update = view.update
|
84
69
|
self.server.controller.view_reset_camera = view.reset_camera
|
85
70
|
self.server.controller.on_server_ready.add(view.update)
|
86
71
|
|
72
|
+
# Quad-view layout (MPR mode) - directly in content like app.py
|
73
|
+
with vuetify.VContainer(
|
74
|
+
v_if="mpr_enabled",
|
75
|
+
fluid=True,
|
76
|
+
classes="pa-0",
|
77
|
+
style="height: calc(100vh - 85px);",
|
78
|
+
):
|
79
|
+
# Setup MPR render windows in Scene
|
80
|
+
self.scene.setup_mpr_render_windows()
|
81
|
+
|
82
|
+
# First row: Axial and Volume (50% height)
|
83
|
+
with vuetify.VRow(classes="ma-0", style="height: 50%;"):
|
84
|
+
with vuetify.VCol(
|
85
|
+
cols="6", classes="pa-1", style="height: 100%;"
|
86
|
+
):
|
87
|
+
# Axial view
|
88
|
+
axial_view = vtk_widgets.VtkRemoteView(
|
89
|
+
self.scene.axial_renderWindow,
|
90
|
+
style="height: 100%; width: 100%;",
|
91
|
+
interactive_ratio=1,
|
92
|
+
)
|
93
|
+
with vuetify.VCol(
|
94
|
+
cols="6", classes="pa-1", style="height: 100%;"
|
95
|
+
):
|
96
|
+
# Volume view (reuse existing render window)
|
97
|
+
volume_view = vtk_widgets.VtkRemoteView(
|
98
|
+
self.scene.renderWindow,
|
99
|
+
style="height: 100%; width: 100%;",
|
100
|
+
interactive_ratio=1,
|
101
|
+
)
|
102
|
+
|
103
|
+
# Second row: Coronal and Sagittal (50% height)
|
104
|
+
with vuetify.VRow(classes="ma-0", style="height: 50%;"):
|
105
|
+
with vuetify.VCol(
|
106
|
+
cols="6", classes="pa-1", style="height: 100%;"
|
107
|
+
):
|
108
|
+
# Coronal view
|
109
|
+
coronal_view = vtk_widgets.VtkRemoteView(
|
110
|
+
self.scene.coronal_renderWindow,
|
111
|
+
style="height: 100%; width: 100%;",
|
112
|
+
interactive_ratio=1,
|
113
|
+
)
|
114
|
+
with vuetify.VCol(
|
115
|
+
cols="6", classes="pa-1", style="height: 100%;"
|
116
|
+
):
|
117
|
+
# Sagittal view
|
118
|
+
sagittal_view = vtk_widgets.VtkRemoteView(
|
119
|
+
self.scene.sagittal_renderWindow,
|
120
|
+
style="height: 100%; width: 100%;",
|
121
|
+
interactive_ratio=1,
|
122
|
+
)
|
123
|
+
|
124
|
+
# Set up controller functions for MPR mode
|
125
|
+
self.server.controller.view_update = self._update_all_mpr_views
|
126
|
+
self.server.controller.view_reset_camera = volume_view.reset_camera
|
127
|
+
self.server.controller.on_server_ready.add(
|
128
|
+
self._update_all_mpr_views
|
129
|
+
)
|
130
|
+
|
131
|
+
# Store individual view update functions
|
132
|
+
self.server.controller.axial_update = axial_view.update
|
133
|
+
self.server.controller.coronal_update = coronal_view.update
|
134
|
+
self.server.controller.sagittal_update = sagittal_view.update
|
135
|
+
self.server.controller.volume_update = volume_view.update
|
136
|
+
|
87
137
|
with layout.drawer:
|
88
|
-
|
138
|
+
# MPR Mode Toggle (only show when volumes are present)
|
139
|
+
if self.scene.volumes:
|
140
|
+
vuetify.VListSubheader("View Mode")
|
141
|
+
vuetify.VCheckbox(
|
142
|
+
v_model=("mpr_enabled", False),
|
143
|
+
label="Multi-Planar Reconstruction (MPR)",
|
144
|
+
title="Toggle between single 3D view and quad-view with slice planes",
|
145
|
+
dense=True,
|
146
|
+
hide_details=True,
|
147
|
+
)
|
148
|
+
|
149
|
+
# Volume selection dropdown (only show when MPR is enabled)
|
150
|
+
vuetify.VSelect(
|
151
|
+
v_if="mpr_enabled",
|
152
|
+
v_model=("active_volume_label", ""),
|
153
|
+
items=("volume_items", []),
|
154
|
+
item_title="text",
|
155
|
+
item_value="value",
|
156
|
+
title="Select which volume to use for MPR",
|
157
|
+
dense=True,
|
158
|
+
hide_details=True,
|
159
|
+
)
|
160
|
+
|
161
|
+
# Window/Level controls for MPR
|
162
|
+
vuetify.VListSubheader(
|
163
|
+
"Window/Level", v_if="mpr_enabled && active_volume_label"
|
164
|
+
)
|
165
|
+
|
166
|
+
vuetify.VSelect(
|
167
|
+
v_if="mpr_enabled && active_volume_label",
|
168
|
+
v_model=("mpr_window_level_preset", 7),
|
169
|
+
items=("mpr_presets", []),
|
170
|
+
item_title="text",
|
171
|
+
item_value="value",
|
172
|
+
label="Preset",
|
173
|
+
dense=True,
|
174
|
+
hide_details=True,
|
175
|
+
)
|
176
|
+
|
177
|
+
vuetify.VSlider(
|
178
|
+
v_if="mpr_enabled && active_volume_label",
|
179
|
+
v_model="mpr_window",
|
180
|
+
min=1.0,
|
181
|
+
max=2000.0,
|
182
|
+
step=1.0,
|
183
|
+
hint="Window",
|
184
|
+
persistent_hint=True,
|
185
|
+
dense=True,
|
186
|
+
hide_details=False,
|
187
|
+
thumb_label=True,
|
188
|
+
)
|
189
|
+
|
190
|
+
vuetify.VSlider(
|
191
|
+
v_if="mpr_enabled && active_volume_label",
|
192
|
+
v_model="mpr_level",
|
193
|
+
min=-1000.0,
|
194
|
+
max=1000.0,
|
195
|
+
step=1.0,
|
196
|
+
hint="Level",
|
197
|
+
persistent_hint=True,
|
198
|
+
dense=True,
|
199
|
+
hide_details=False,
|
200
|
+
thumb_label=True,
|
201
|
+
)
|
202
|
+
|
203
|
+
# Slice position controls (only show when MPR is enabled and volume is selected)
|
204
|
+
vuetify.VListSubheader(
|
205
|
+
"Slice Positions", v_if="mpr_enabled && active_volume_label"
|
206
|
+
)
|
207
|
+
|
208
|
+
vuetify.VSlider(
|
209
|
+
v_if="mpr_enabled && active_volume_label",
|
210
|
+
v_model=("axial_slice", 0.5),
|
211
|
+
min=0.0,
|
212
|
+
max=1.0,
|
213
|
+
step=0.01,
|
214
|
+
hint="A (I ↔ S)",
|
215
|
+
persistent_hint=True,
|
216
|
+
dense=True,
|
217
|
+
hide_details=False,
|
218
|
+
thumb_label=True,
|
219
|
+
)
|
220
|
+
|
221
|
+
vuetify.VSlider(
|
222
|
+
v_if="mpr_enabled && active_volume_label",
|
223
|
+
v_model=("sagittal_slice", 0.5),
|
224
|
+
min=0.0,
|
225
|
+
max=1.0,
|
226
|
+
step=0.01,
|
227
|
+
hint="S (R ↔ L)",
|
228
|
+
persistent_hint=True,
|
229
|
+
dense=True,
|
230
|
+
hide_details=False,
|
231
|
+
thumb_label=True,
|
232
|
+
)
|
233
|
+
|
234
|
+
vuetify.VSlider(
|
235
|
+
v_if="mpr_enabled && active_volume_label",
|
236
|
+
v_model=("coronal_slice", 0.5),
|
237
|
+
min=0.0,
|
238
|
+
max=1.0,
|
239
|
+
step=0.01,
|
240
|
+
hint="C (P ↔ A)",
|
241
|
+
persistent_hint=True,
|
242
|
+
dense=True,
|
243
|
+
hide_details=False,
|
244
|
+
thumb_label=True,
|
245
|
+
)
|
246
|
+
|
247
|
+
# MPR Rotation controls
|
248
|
+
vuetify.VListSubheader(
|
249
|
+
"Rotations", v_if="mpr_enabled && active_volume_label"
|
250
|
+
)
|
251
|
+
|
252
|
+
# Rotation buttons
|
253
|
+
with vuetify.VRow(
|
254
|
+
v_if="mpr_enabled && active_volume_label",
|
255
|
+
no_gutters=True,
|
256
|
+
classes="mb-2",
|
257
|
+
):
|
258
|
+
with vuetify.VCol(cols="4"):
|
259
|
+
vuetify.VBtn(
|
260
|
+
"X",
|
261
|
+
click=self.server.controller.add_x_rotation,
|
262
|
+
small=True,
|
263
|
+
dense=True,
|
264
|
+
outlined=True,
|
265
|
+
)
|
266
|
+
with vuetify.VCol(cols="4"):
|
267
|
+
vuetify.VBtn(
|
268
|
+
"Y",
|
269
|
+
click=self.server.controller.add_y_rotation,
|
270
|
+
small=True,
|
271
|
+
dense=True,
|
272
|
+
outlined=True,
|
273
|
+
)
|
274
|
+
with vuetify.VCol(cols="4"):
|
275
|
+
vuetify.VBtn(
|
276
|
+
"Z",
|
277
|
+
click=self.server.controller.add_z_rotation,
|
278
|
+
small=True,
|
279
|
+
dense=True,
|
280
|
+
outlined=True,
|
281
|
+
)
|
282
|
+
|
283
|
+
# Reset rotations button
|
284
|
+
vuetify.VBtn(
|
285
|
+
"Reset",
|
286
|
+
v_if="mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > 0",
|
287
|
+
click=self.server.controller.reset_rotations,
|
288
|
+
small=True,
|
289
|
+
dense=True,
|
290
|
+
outlined=True,
|
291
|
+
color="warning",
|
292
|
+
block=True,
|
293
|
+
classes="mb-2",
|
294
|
+
)
|
295
|
+
|
296
|
+
# Individual rotation sliders (show up to 10)
|
297
|
+
for i in range(10):
|
298
|
+
vuetify.VSlider(
|
299
|
+
v_if=f"mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > {i}",
|
300
|
+
v_model=(f"mpr_rotation_angle_{i}", 0),
|
301
|
+
min=-180,
|
302
|
+
max=180,
|
303
|
+
step=1,
|
304
|
+
hint=(f"mpr_rotation_axis_{i}", f"Rotation {i + 1}"),
|
305
|
+
persistent_hint=True,
|
306
|
+
dense=True,
|
307
|
+
hide_details=False,
|
308
|
+
thumb_label=True,
|
309
|
+
)
|
310
|
+
|
311
|
+
vuetify.VDivider(classes="my-2")
|
312
|
+
|
313
|
+
vuetify.VListSubheader("Playback Controls")
|
89
314
|
|
90
315
|
with vuetify.VToolbar(dense=True, flat=True):
|
91
316
|
# NOTE: Previous/Next controls should be VBtn components, but we use
|
@@ -93,8 +318,8 @@ class UI:
|
|
93
318
|
# This may be easier to fix in Vuetify 3.
|
94
319
|
vuetify.VCheckbox(
|
95
320
|
value=False,
|
96
|
-
|
97
|
-
|
321
|
+
true_icon="mdi-skip-previous-circle",
|
322
|
+
false_icon="mdi-skip-previous-circle",
|
98
323
|
hide_details=True,
|
99
324
|
title="Previous",
|
100
325
|
click=self.server.controller.decrement_frame,
|
@@ -105,8 +330,8 @@ class UI:
|
|
105
330
|
|
106
331
|
vuetify.VCheckbox(
|
107
332
|
value=False,
|
108
|
-
|
109
|
-
|
333
|
+
true_icon="mdi-skip-next-circle",
|
334
|
+
false_icon="mdi-skip-next-circle",
|
110
335
|
hide_details=True,
|
111
336
|
title="Next",
|
112
337
|
click=self.server.controller.increment_frame,
|
@@ -117,8 +342,8 @@ class UI:
|
|
117
342
|
|
118
343
|
vuetify.VCheckbox(
|
119
344
|
v_model=("playing", False),
|
120
|
-
|
121
|
-
|
345
|
+
true_icon="mdi-pause-circle",
|
346
|
+
false_icon="mdi-play-circle",
|
122
347
|
title="Play/Pause",
|
123
348
|
hide_details=True,
|
124
349
|
)
|
@@ -127,8 +352,8 @@ class UI:
|
|
127
352
|
|
128
353
|
vuetify.VCheckbox(
|
129
354
|
v_model=("incrementing", True),
|
130
|
-
|
131
|
-
|
355
|
+
true_icon="mdi-movie-open-outline",
|
356
|
+
false_icon="mdi-movie-open-off-outline",
|
132
357
|
hide_details=True,
|
133
358
|
title="Incrementing",
|
134
359
|
)
|
@@ -137,8 +362,8 @@ class UI:
|
|
137
362
|
|
138
363
|
vuetify.VCheckbox(
|
139
364
|
v_model=("rotating", False),
|
140
|
-
|
141
|
-
|
365
|
+
true_icon="mdi-autorenew",
|
366
|
+
false_icon="mdi-autorenew-off",
|
142
367
|
hide_details=True,
|
143
368
|
title="Rotating",
|
144
369
|
)
|
@@ -193,10 +418,10 @@ class UI:
|
|
193
418
|
title=f"Capture cine to {self.scene.screenshot_directory}",
|
194
419
|
)
|
195
420
|
|
196
|
-
vuetify.
|
421
|
+
vuetify.VListSubheader("Appearance and Visibility")
|
197
422
|
|
198
423
|
if self.scene.meshes:
|
199
|
-
vuetify.
|
424
|
+
vuetify.VListSubheader("Meshes", classes="text-caption pl-4")
|
200
425
|
for i, m in enumerate(self.scene.meshes):
|
201
426
|
vuetify.VCheckbox(
|
202
427
|
v_model=f"mesh_visibility_{m.label}",
|
@@ -233,8 +458,8 @@ class UI:
|
|
233
458
|
style="max-width: 270px;",
|
234
459
|
):
|
235
460
|
with vuetify.VExpansionPanel():
|
236
|
-
vuetify.
|
237
|
-
with vuetify.
|
461
|
+
vuetify.VExpansionPanelTitle("Clip Bounds")
|
462
|
+
with vuetify.VExpansionPanelText():
|
238
463
|
# X bounds
|
239
464
|
vuetify.VRangeSlider(
|
240
465
|
v_model=(
|
@@ -279,7 +504,7 @@ class UI:
|
|
279
504
|
)
|
280
505
|
|
281
506
|
if self.scene.volumes:
|
282
|
-
vuetify.
|
507
|
+
vuetify.VListSubheader("Volumes", classes="text-caption pl-4")
|
283
508
|
for i, v in enumerate(self.scene.volumes):
|
284
509
|
vuetify.VCheckbox(
|
285
510
|
v_model=f"volume_visibility_{v.label}",
|
@@ -292,7 +517,7 @@ class UI:
|
|
292
517
|
)
|
293
518
|
|
294
519
|
# Preset selection in collapsible panel
|
295
|
-
available_presets =
|
520
|
+
available_presets = list_volume_property_presets()
|
296
521
|
current_preset = self.server.state[f"volume_preset_{v.label}"]
|
297
522
|
current_desc = available_presets.get(
|
298
523
|
current_preset, current_preset
|
@@ -306,8 +531,8 @@ class UI:
|
|
306
531
|
style="max-width: 270px;",
|
307
532
|
):
|
308
533
|
with vuetify.VExpansionPanel():
|
309
|
-
vuetify.
|
310
|
-
with vuetify.
|
534
|
+
vuetify.VExpansionPanelTitle("Transfer Function")
|
535
|
+
with vuetify.VExpansionPanelText():
|
311
536
|
with vuetify.VRadioGroup(
|
312
537
|
v_model=f"volume_preset_{v.label}",
|
313
538
|
dense=True,
|
@@ -347,8 +572,8 @@ class UI:
|
|
347
572
|
style="max-width: 270px;",
|
348
573
|
):
|
349
574
|
with vuetify.VExpansionPanel():
|
350
|
-
vuetify.
|
351
|
-
with vuetify.
|
575
|
+
vuetify.VExpansionPanelTitle("Clip Bounds")
|
576
|
+
with vuetify.VExpansionPanelText():
|
352
577
|
# X bounds
|
353
578
|
vuetify.VRangeSlider(
|
354
579
|
v_model=(
|
@@ -393,7 +618,7 @@ class UI:
|
|
393
618
|
)
|
394
619
|
|
395
620
|
if self.scene.segmentations:
|
396
|
-
vuetify.
|
621
|
+
vuetify.VListSubheader("Segmentations", classes="text-caption pl-4")
|
397
622
|
for i, s in enumerate(self.scene.segmentations):
|
398
623
|
vuetify.VCheckbox(
|
399
624
|
v_model=f"segmentation_visibility_{s.label}",
|
@@ -431,8 +656,8 @@ class UI:
|
|
431
656
|
style="max-width: 270px;",
|
432
657
|
):
|
433
658
|
with vuetify.VExpansionPanel():
|
434
|
-
vuetify.
|
435
|
-
with vuetify.
|
659
|
+
vuetify.VExpansionPanelTitle("Clip Bounds")
|
660
|
+
with vuetify.VExpansionPanelText():
|
436
661
|
# X bounds
|
437
662
|
vuetify.VRangeSlider(
|
438
663
|
v_model=(
|
@@ -475,3 +700,14 @@ class UI:
|
|
475
700
|
dense=True,
|
476
701
|
thumb_label=False,
|
477
702
|
)
|
703
|
+
|
704
|
+
def _update_all_mpr_views(self, **kwargs):
|
705
|
+
"""Update all MPR views."""
|
706
|
+
if hasattr(self.server.controller, "axial_update"):
|
707
|
+
self.server.controller.axial_update()
|
708
|
+
if hasattr(self.server.controller, "coronal_update"):
|
709
|
+
self.server.controller.coronal_update()
|
710
|
+
if hasattr(self.server.controller, "sagittal_update"):
|
711
|
+
self.server.controller.sagittal_update()
|
712
|
+
if hasattr(self.server.controller, "volume_update"):
|
713
|
+
self.server.controller.volume_update()
|