cardio 2025.8.1__py3-none-any.whl → 2025.10.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 +9 -7
- cardio/app.py +6 -13
- cardio/blend_transfer_functions.py +84 -0
- cardio/color_transfer_function.py +29 -0
- cardio/logic.py +549 -6
- cardio/mesh.py +2 -3
- cardio/object.py +9 -13
- cardio/orientation.py +215 -0
- cardio/piecewise_function.py +29 -0
- cardio/scene.py +190 -28
- cardio/segmentation.py +4 -2
- cardio/transfer_function_pair.py +25 -0
- cardio/ui.py +501 -66
- cardio/utils.py +4 -47
- cardio/volume.py +227 -5
- cardio/volume_property.py +46 -0
- cardio/volume_property_presets.py +53 -0
- cardio/window_level.py +35 -0
- {cardio-2025.8.1.dist-info → cardio-2025.10.0.dist-info}/METADATA +5 -3
- cardio-2025.10.0.dist-info/RECORD +29 -0
- cardio/transfer_functions.py +0 -272
- cardio-2025.8.1.dist-info/RECORD +0 -22
- {cardio-2025.8.1.dist-info → cardio-2025.10.0.dist-info}/WHEEL +0 -0
- {cardio-2025.8.1.dist-info → cardio-2025.10.0.dist-info}/entry_points.txt +0 -0
cardio/logic.py
CHANGED
|
@@ -12,9 +12,49 @@ class Logic:
|
|
|
12
12
|
self.server = server
|
|
13
13
|
self.scene = scene
|
|
14
14
|
|
|
15
|
+
# Initialize mpr_presets early to avoid undefined errors
|
|
16
|
+
self.server.state.mpr_presets = []
|
|
17
|
+
|
|
18
|
+
# Initialize volume items for MPR dropdown
|
|
19
|
+
self.server.state.volume_items = [
|
|
20
|
+
{"text": volume.label, "value": volume.label}
|
|
21
|
+
for volume in self.scene.volumes
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Initialize angle units items for dropdown
|
|
25
|
+
self.server.state.angle_units_items = [
|
|
26
|
+
{"text": "Degrees", "value": "degrees"},
|
|
27
|
+
{"text": "Radians", "value": "radians"},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Initialize slice bounds (will be updated when active volume changes)
|
|
31
|
+
self.server.state.axial_slice_bounds = [0.0, 100.0]
|
|
32
|
+
self.server.state.sagittal_slice_bounds = [0.0, 100.0]
|
|
33
|
+
self.server.state.coronal_slice_bounds = [0.0, 100.0]
|
|
34
|
+
|
|
15
35
|
self.server.state.change("frame")(self.update_frame)
|
|
16
36
|
self.server.state.change("playing")(self.play)
|
|
17
|
-
self.server.state.change("
|
|
37
|
+
self.server.state.change("theme_mode")(self.sync_background_color)
|
|
38
|
+
self.server.state.change("mpr_enabled")(self.sync_mpr_mode)
|
|
39
|
+
self.server.state.change("active_volume_label")(self.sync_active_volume)
|
|
40
|
+
self.server.state.change("axial_slice", "sagittal_slice", "coronal_slice")(
|
|
41
|
+
self.update_slice_positions
|
|
42
|
+
)
|
|
43
|
+
self.server.state.change("mpr_window", "mpr_level")(
|
|
44
|
+
self.update_mpr_window_level
|
|
45
|
+
)
|
|
46
|
+
self.server.state.change("mpr_window_level_preset")(self.update_mpr_preset)
|
|
47
|
+
self.server.state.change("mpr_rotation_sequence")(self.update_mpr_rotation)
|
|
48
|
+
self.server.state.change("angle_units")(self.sync_angle_units)
|
|
49
|
+
|
|
50
|
+
# Add handlers for individual rotation angles and visibility
|
|
51
|
+
for i in range(self.scene.max_mpr_rotations):
|
|
52
|
+
self.server.state.change(f"mpr_rotation_angle_{i}")(
|
|
53
|
+
self.update_mpr_rotation
|
|
54
|
+
)
|
|
55
|
+
self.server.state.change(f"mpr_rotation_visible_{i}")(
|
|
56
|
+
self.update_mpr_rotation
|
|
57
|
+
)
|
|
18
58
|
|
|
19
59
|
# Initialize visibility state variables
|
|
20
60
|
for m in self.scene.meshes:
|
|
@@ -97,7 +137,54 @@ class Logic:
|
|
|
97
137
|
self.server.controller.increment_frame = self.increment_frame
|
|
98
138
|
self.server.controller.decrement_frame = self.decrement_frame
|
|
99
139
|
self.server.controller.screenshot = self.screenshot
|
|
140
|
+
self.server.controller.save_rotation_angles = self.save_rotation_angles
|
|
100
141
|
self.server.controller.reset_all = self.reset_all
|
|
142
|
+
self.server.controller.close_application = self.close_application
|
|
143
|
+
|
|
144
|
+
# MPR rotation controllers
|
|
145
|
+
self.server.controller.add_x_rotation = lambda: self.add_mpr_rotation("X")
|
|
146
|
+
self.server.controller.add_y_rotation = lambda: self.add_mpr_rotation("Y")
|
|
147
|
+
self.server.controller.add_z_rotation = lambda: self.add_mpr_rotation("Z")
|
|
148
|
+
self.server.controller.remove_rotation_event = self.remove_mpr_rotation
|
|
149
|
+
self.server.controller.reset_rotations = self.reset_mpr_rotations
|
|
150
|
+
|
|
151
|
+
# Initialize MPR state
|
|
152
|
+
self.server.state.mpr_enabled = self.scene.mpr_enabled
|
|
153
|
+
self.server.state.active_volume_label = self.scene.active_volume_label
|
|
154
|
+
self.server.state.axial_slice = self.scene.axial_slice
|
|
155
|
+
self.server.state.sagittal_slice = self.scene.sagittal_slice
|
|
156
|
+
self.server.state.coronal_slice = self.scene.coronal_slice
|
|
157
|
+
self.server.state.mpr_window = self.scene.mpr_window
|
|
158
|
+
self.server.state.mpr_level = self.scene.mpr_level
|
|
159
|
+
self.server.state.mpr_window_level_preset = self.scene.mpr_window_level_preset
|
|
160
|
+
self.server.state.mpr_rotation_sequence = self.scene.mpr_rotation_sequence
|
|
161
|
+
self.server.state.angle_units = self.scene.angle_units.value
|
|
162
|
+
|
|
163
|
+
# Initialize MPR presets data
|
|
164
|
+
try:
|
|
165
|
+
from .window_level import presets
|
|
166
|
+
|
|
167
|
+
self.server.state.mpr_presets = [
|
|
168
|
+
{"text": "Select W/L...", "value": None}
|
|
169
|
+
] + [{"text": preset.name, "value": key} for key, preset in presets.items()]
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"Error initializing MPR presets: {e}")
|
|
172
|
+
self.server.state.mpr_presets = []
|
|
173
|
+
|
|
174
|
+
# Initialize rotation angle states
|
|
175
|
+
for i in range(self.scene.max_mpr_rotations):
|
|
176
|
+
setattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
|
|
177
|
+
setattr(self.server.state, f"mpr_rotation_axis_{i}", f"Rotation {i + 1}")
|
|
178
|
+
setattr(self.server.state, f"mpr_rotation_visible_{i}", True)
|
|
179
|
+
|
|
180
|
+
# Apply initial preset to ensure window/level values are set correctly
|
|
181
|
+
# Only update state values, don't call update methods yet since MPR may not be enabled
|
|
182
|
+
from .window_level import presets
|
|
183
|
+
|
|
184
|
+
if self.scene.mpr_window_level_preset in presets:
|
|
185
|
+
preset = presets[self.scene.mpr_window_level_preset]
|
|
186
|
+
self.server.state.mpr_window = preset.window
|
|
187
|
+
self.server.state.mpr_level = preset.level
|
|
101
188
|
|
|
102
189
|
# Initialize clipping state variables
|
|
103
190
|
self._initialize_clipping_state()
|
|
@@ -122,8 +209,95 @@ class Logic:
|
|
|
122
209
|
actor = segmentation.actors[frame % len(segmentation.actors)]
|
|
123
210
|
actor.SetVisibility(True)
|
|
124
211
|
|
|
212
|
+
# Update MPR views if MPR is enabled
|
|
213
|
+
self.update_mpr_frame(frame)
|
|
214
|
+
|
|
125
215
|
self.server.controller.view_update()
|
|
126
216
|
|
|
217
|
+
def update_mpr_frame(self, frame):
|
|
218
|
+
"""Update MPR views to show the specified frame."""
|
|
219
|
+
if not getattr(self.server.state, "mpr_enabled", False):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
active_volume_label = getattr(self.server.state, "active_volume_label", "")
|
|
223
|
+
if not active_volume_label:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Find the active volume
|
|
227
|
+
active_volume = None
|
|
228
|
+
for volume in self.scene.volumes:
|
|
229
|
+
if volume.label == active_volume_label:
|
|
230
|
+
active_volume = volume
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
if not active_volume:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# Get or create MPR actors for the new frame
|
|
237
|
+
mpr_actors = active_volume.get_mpr_actors_for_frame(frame)
|
|
238
|
+
|
|
239
|
+
# Update each MPR renderer with the new frame's actors
|
|
240
|
+
if self.scene.axial_renderWindow:
|
|
241
|
+
axial_renderer = (
|
|
242
|
+
self.scene.axial_renderWindow.GetRenderers().GetFirstRenderer()
|
|
243
|
+
)
|
|
244
|
+
if axial_renderer:
|
|
245
|
+
axial_renderer.RemoveAllViewProps()
|
|
246
|
+
axial_renderer.AddActor(mpr_actors["axial"]["actor"])
|
|
247
|
+
mpr_actors["axial"]["actor"].SetVisibility(True)
|
|
248
|
+
# Apply current slice position and window/level
|
|
249
|
+
self._apply_current_mpr_settings(active_volume, frame)
|
|
250
|
+
axial_renderer.ResetCamera()
|
|
251
|
+
|
|
252
|
+
if self.scene.coronal_renderWindow:
|
|
253
|
+
coronal_renderer = (
|
|
254
|
+
self.scene.coronal_renderWindow.GetRenderers().GetFirstRenderer()
|
|
255
|
+
)
|
|
256
|
+
if coronal_renderer:
|
|
257
|
+
coronal_renderer.RemoveAllViewProps()
|
|
258
|
+
coronal_renderer.AddActor(mpr_actors["coronal"]["actor"])
|
|
259
|
+
mpr_actors["coronal"]["actor"].SetVisibility(True)
|
|
260
|
+
coronal_renderer.ResetCamera()
|
|
261
|
+
|
|
262
|
+
if self.scene.sagittal_renderWindow:
|
|
263
|
+
sagittal_renderer = (
|
|
264
|
+
self.scene.sagittal_renderWindow.GetRenderers().GetFirstRenderer()
|
|
265
|
+
)
|
|
266
|
+
if sagittal_renderer:
|
|
267
|
+
sagittal_renderer.RemoveAllViewProps()
|
|
268
|
+
sagittal_renderer.AddActor(mpr_actors["sagittal"]["actor"])
|
|
269
|
+
mpr_actors["sagittal"]["actor"].SetVisibility(True)
|
|
270
|
+
sagittal_renderer.ResetCamera()
|
|
271
|
+
|
|
272
|
+
def _apply_current_mpr_settings(self, active_volume, frame):
|
|
273
|
+
"""Apply current slice positions and window/level to MPR actors."""
|
|
274
|
+
# Apply slice positions
|
|
275
|
+
axial_slice = getattr(self.server.state, "axial_slice", 0.5)
|
|
276
|
+
sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
|
|
277
|
+
coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
|
|
278
|
+
|
|
279
|
+
# Get rotation data
|
|
280
|
+
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
281
|
+
rotation_angles = {}
|
|
282
|
+
for i in range(len(rotation_sequence)):
|
|
283
|
+
rotation_angles[i] = getattr(
|
|
284
|
+
self.server.state, f"mpr_rotation_angle_{i}", 0
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
active_volume.update_slice_positions(
|
|
288
|
+
frame,
|
|
289
|
+
axial_slice,
|
|
290
|
+
sagittal_slice,
|
|
291
|
+
coronal_slice,
|
|
292
|
+
rotation_sequence,
|
|
293
|
+
rotation_angles,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Apply window/level
|
|
297
|
+
window = getattr(self.server.state, "mpr_window", 400.0)
|
|
298
|
+
level = getattr(self.server.state, "mpr_level", 40.0)
|
|
299
|
+
active_volume.update_mpr_window_level(frame, window, level)
|
|
300
|
+
|
|
127
301
|
@asynchronous.task
|
|
128
302
|
async def play(self, playing, **kwargs):
|
|
129
303
|
if not (self.server.state.incrementing or self.server.state.rotating):
|
|
@@ -159,11 +333,11 @@ class Logic:
|
|
|
159
333
|
|
|
160
334
|
def sync_volume_presets(self, **kwargs):
|
|
161
335
|
"""Update volume transfer function presets based on UI selection."""
|
|
162
|
-
from .
|
|
336
|
+
from .volume_property_presets import load_volume_property_preset
|
|
163
337
|
|
|
164
338
|
for v in self.scene.volumes:
|
|
165
339
|
preset_name = self.server.state[f"volume_preset_{v.label}"]
|
|
166
|
-
preset =
|
|
340
|
+
preset = load_volume_property_preset(preset_name)
|
|
167
341
|
|
|
168
342
|
# Apply preset to all actors
|
|
169
343
|
for actor in v.actors:
|
|
@@ -261,7 +435,7 @@ class Logic:
|
|
|
261
435
|
|
|
262
436
|
@asynchronous.task
|
|
263
437
|
async def screenshot(self):
|
|
264
|
-
dr = dt.datetime.now().strftime(self.scene.
|
|
438
|
+
dr = dt.datetime.now().strftime(self.scene.timestamp_format)
|
|
265
439
|
dr = self.scene.screenshot_directory / dr
|
|
266
440
|
dr.mkdir(parents=True, exist_ok=True)
|
|
267
441
|
|
|
@@ -286,6 +460,65 @@ class Logic:
|
|
|
286
460
|
1 / self.server.state.bpm * 60 / self.scene.nframes
|
|
287
461
|
)
|
|
288
462
|
|
|
463
|
+
@asynchronous.task
|
|
464
|
+
async def save_rotation_angles(self):
|
|
465
|
+
"""Save current rotation angles to a TOML file."""
|
|
466
|
+
import tomlkit as tk
|
|
467
|
+
|
|
468
|
+
# Get current timestamp
|
|
469
|
+
timestamp = dt.datetime.now()
|
|
470
|
+
timestamp_str = timestamp.strftime(self.scene.timestamp_format)
|
|
471
|
+
iso_timestamp = timestamp.isoformat()
|
|
472
|
+
|
|
473
|
+
# Get active volume label
|
|
474
|
+
active_volume_label = getattr(self.server.state, "active_volume_label", "")
|
|
475
|
+
if not active_volume_label:
|
|
476
|
+
print("Warning: No active volume selected for rotation saving")
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# Create directory structure
|
|
480
|
+
save_dir = self.scene.rotations_directory / active_volume_label
|
|
481
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
482
|
+
|
|
483
|
+
# Create TOML structure
|
|
484
|
+
doc = tk.document()
|
|
485
|
+
|
|
486
|
+
# Metadata section
|
|
487
|
+
metadata = tk.table()
|
|
488
|
+
metadata["coordinate_system"] = self.scene.coordinate_system
|
|
489
|
+
metadata["units"] = self.scene.angle_units.value
|
|
490
|
+
metadata["timestamp"] = iso_timestamp
|
|
491
|
+
metadata["volume_label"] = active_volume_label
|
|
492
|
+
doc["metadata"] = metadata
|
|
493
|
+
|
|
494
|
+
# Slice positions section
|
|
495
|
+
slice_positions = tk.table()
|
|
496
|
+
slice_positions["axial"] = getattr(self.server.state, "axial_slice", 0.5)
|
|
497
|
+
slice_positions["sagittal"] = getattr(self.server.state, "sagittal_slice", 0.5)
|
|
498
|
+
slice_positions["coronal"] = getattr(self.server.state, "coronal_slice", 0.5)
|
|
499
|
+
doc["slice_positions"] = slice_positions
|
|
500
|
+
|
|
501
|
+
# Rotations section (array of tables)
|
|
502
|
+
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
503
|
+
rotations_array = tk.aot()
|
|
504
|
+
|
|
505
|
+
for i, rotation_def in enumerate(rotation_sequence):
|
|
506
|
+
rotation = tk.table()
|
|
507
|
+
rotation["axis"] = rotation_def["axis"]
|
|
508
|
+
angle = getattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
|
|
509
|
+
rotation["angle"] = float(angle)
|
|
510
|
+
rotation["visible"] = getattr(
|
|
511
|
+
self.server.state, f"mpr_rotation_visible_{i}", True
|
|
512
|
+
)
|
|
513
|
+
rotations_array.append(rotation)
|
|
514
|
+
|
|
515
|
+
doc["rotations"] = rotations_array
|
|
516
|
+
|
|
517
|
+
# Save to file
|
|
518
|
+
output_path = save_dir / f"{timestamp_str}.toml"
|
|
519
|
+
with open(output_path, "w") as f:
|
|
520
|
+
f.write(tk.dumps(doc))
|
|
521
|
+
|
|
289
522
|
def reset_all(self):
|
|
290
523
|
self.server.state.frame = 0
|
|
291
524
|
self.server.state.playing = False
|
|
@@ -295,9 +528,9 @@ class Logic:
|
|
|
295
528
|
self.server.state.bpr = 5
|
|
296
529
|
self.server.controller.view_update()
|
|
297
530
|
|
|
298
|
-
def sync_background_color(self,
|
|
531
|
+
def sync_background_color(self, theme_mode, **kwargs):
|
|
299
532
|
"""Sync VTK renderer background with dark mode."""
|
|
300
|
-
if
|
|
533
|
+
if theme_mode == "dark":
|
|
301
534
|
# Dark mode: use dark background from config
|
|
302
535
|
self.scene.renderer.SetBackground(
|
|
303
536
|
*self.scene.background.dark,
|
|
@@ -309,6 +542,16 @@ class Logic:
|
|
|
309
542
|
)
|
|
310
543
|
self.server.controller.view_update()
|
|
311
544
|
|
|
545
|
+
def sync_angle_units(self, angle_units, **kwargs):
|
|
546
|
+
"""Sync angle units selection - updates the scene configuration."""
|
|
547
|
+
from .utils import AngleUnit
|
|
548
|
+
|
|
549
|
+
# Update the scene's angle_units field based on UI selection
|
|
550
|
+
if angle_units == "degrees":
|
|
551
|
+
self.scene.angle_units = AngleUnit.DEGREES
|
|
552
|
+
elif angle_units == "radians":
|
|
553
|
+
self.scene.angle_units = AngleUnit.RADIANS
|
|
554
|
+
|
|
312
555
|
def _initialize_clipping_state(self):
|
|
313
556
|
"""Initialize clipping state variables for all objects."""
|
|
314
557
|
# Initialize mesh clipping state
|
|
@@ -363,3 +606,303 @@ class Logic:
|
|
|
363
606
|
setattr(self.server.state, f"clip_x_{s.label}", [bounds[0], bounds[1]])
|
|
364
607
|
setattr(self.server.state, f"clip_y_{s.label}", [bounds[2], bounds[3]])
|
|
365
608
|
setattr(self.server.state, f"clip_z_{s.label}", [bounds[4], bounds[5]])
|
|
609
|
+
|
|
610
|
+
def sync_mpr_mode(self, mpr_enabled, **kwargs):
|
|
611
|
+
"""Handle MPR mode toggle."""
|
|
612
|
+
if (
|
|
613
|
+
mpr_enabled
|
|
614
|
+
and self.scene.volumes
|
|
615
|
+
and not self.server.state.active_volume_label
|
|
616
|
+
):
|
|
617
|
+
# Auto-select first volume when MPR is enabled and no volume is selected
|
|
618
|
+
self.server.state.active_volume_label = self.scene.volumes[0].label
|
|
619
|
+
|
|
620
|
+
def sync_active_volume(self, active_volume_label, **kwargs):
|
|
621
|
+
"""Handle active volume selection for MPR."""
|
|
622
|
+
|
|
623
|
+
if not active_volume_label or not self.server.state.mpr_enabled:
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
# Find the selected volume
|
|
627
|
+
active_volume = None
|
|
628
|
+
for volume in self.scene.volumes:
|
|
629
|
+
if volume.label == active_volume_label:
|
|
630
|
+
active_volume = volume
|
|
631
|
+
break
|
|
632
|
+
|
|
633
|
+
if not active_volume:
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
# Update slice bounds based on active volume
|
|
637
|
+
try:
|
|
638
|
+
bounds = active_volume.get_physical_bounds()
|
|
639
|
+
self.server.state.axial_slice_bounds = [bounds[4], bounds[5]] # Z bounds
|
|
640
|
+
self.server.state.sagittal_slice_bounds = [bounds[0], bounds[1]] # X bounds
|
|
641
|
+
self.server.state.coronal_slice_bounds = [bounds[2], bounds[3]] # Y bounds
|
|
642
|
+
|
|
643
|
+
# Initialize slice positions to volume center if they are currently 0.0 (scene defaults)
|
|
644
|
+
if self.server.state.axial_slice == 0.0:
|
|
645
|
+
self.server.state.axial_slice = (bounds[4] + bounds[5]) / 2 # Z center
|
|
646
|
+
if self.server.state.sagittal_slice == 0.0:
|
|
647
|
+
self.server.state.sagittal_slice = (
|
|
648
|
+
bounds[0] + bounds[1]
|
|
649
|
+
) / 2 # X center
|
|
650
|
+
if self.server.state.coronal_slice == 0.0:
|
|
651
|
+
self.server.state.coronal_slice = (
|
|
652
|
+
bounds[2] + bounds[3]
|
|
653
|
+
) / 2 # Y center
|
|
654
|
+
except (RuntimeError, IndexError) as e:
|
|
655
|
+
print(f"Error: Cannot get bounds for volume '{active_volume_label}': {e}")
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
# Create MPR actors for current frame
|
|
659
|
+
current_frame = getattr(self.server.state, "frame", 0)
|
|
660
|
+
mpr_actors = active_volume.get_mpr_actors_for_frame(current_frame)
|
|
661
|
+
|
|
662
|
+
# Add MPR actors to their respective renderers
|
|
663
|
+
if self.scene.axial_renderWindow:
|
|
664
|
+
axial_renderer = (
|
|
665
|
+
self.scene.axial_renderWindow.GetRenderers().GetFirstRenderer()
|
|
666
|
+
)
|
|
667
|
+
if axial_renderer:
|
|
668
|
+
axial_renderer.RemoveAllViewProps() # Clear existing actors
|
|
669
|
+
axial_renderer.AddActor(mpr_actors["axial"]["actor"])
|
|
670
|
+
mpr_actors["axial"]["actor"].SetVisibility(True)
|
|
671
|
+
axial_renderer.ResetCamera()
|
|
672
|
+
|
|
673
|
+
if self.scene.coronal_renderWindow:
|
|
674
|
+
coronal_renderer = (
|
|
675
|
+
self.scene.coronal_renderWindow.GetRenderers().GetFirstRenderer()
|
|
676
|
+
)
|
|
677
|
+
if coronal_renderer:
|
|
678
|
+
coronal_renderer.RemoveAllViewProps()
|
|
679
|
+
coronal_renderer.AddActor(mpr_actors["coronal"]["actor"])
|
|
680
|
+
mpr_actors["coronal"]["actor"].SetVisibility(True)
|
|
681
|
+
coronal_renderer.ResetCamera()
|
|
682
|
+
|
|
683
|
+
if self.scene.sagittal_renderWindow:
|
|
684
|
+
sagittal_renderer = (
|
|
685
|
+
self.scene.sagittal_renderWindow.GetRenderers().GetFirstRenderer()
|
|
686
|
+
)
|
|
687
|
+
if sagittal_renderer:
|
|
688
|
+
sagittal_renderer.RemoveAllViewProps()
|
|
689
|
+
sagittal_renderer.AddActor(mpr_actors["sagittal"]["actor"])
|
|
690
|
+
mpr_actors["sagittal"]["actor"].SetVisibility(True)
|
|
691
|
+
sagittal_renderer.ResetCamera()
|
|
692
|
+
|
|
693
|
+
# Apply current window/level settings to the MPR actors
|
|
694
|
+
window = getattr(self.server.state, "mpr_window", 800.0)
|
|
695
|
+
level = getattr(self.server.state, "mpr_level", 200.0)
|
|
696
|
+
active_volume.update_mpr_window_level(current_frame, window, level)
|
|
697
|
+
|
|
698
|
+
# Update all views
|
|
699
|
+
self.server.controller.view_update()
|
|
700
|
+
|
|
701
|
+
def update_slice_positions(self, **kwargs):
|
|
702
|
+
"""Update MPR slice positions when sliders change."""
|
|
703
|
+
|
|
704
|
+
if not getattr(self.server.state, "mpr_enabled", False):
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
active_volume_label = getattr(self.server.state, "active_volume_label", "")
|
|
708
|
+
if not active_volume_label:
|
|
709
|
+
return
|
|
710
|
+
|
|
711
|
+
# Find the active volume
|
|
712
|
+
active_volume = None
|
|
713
|
+
for volume in self.scene.volumes:
|
|
714
|
+
if volume.label == active_volume_label:
|
|
715
|
+
active_volume = volume
|
|
716
|
+
break
|
|
717
|
+
|
|
718
|
+
if not active_volume:
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
# Get current slice positions
|
|
722
|
+
axial_slice = getattr(self.server.state, "axial_slice", 0.5)
|
|
723
|
+
sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
|
|
724
|
+
coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
|
|
725
|
+
current_frame = getattr(self.server.state, "frame", 0)
|
|
726
|
+
|
|
727
|
+
# Get rotation data
|
|
728
|
+
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
729
|
+
rotation_angles = {}
|
|
730
|
+
for i in range(len(rotation_sequence)):
|
|
731
|
+
rotation_angles[i] = getattr(
|
|
732
|
+
self.server.state, f"mpr_rotation_angle_{i}", 0
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
# Update slice positions with rotation
|
|
736
|
+
active_volume.update_slice_positions(
|
|
737
|
+
current_frame,
|
|
738
|
+
axial_slice,
|
|
739
|
+
sagittal_slice,
|
|
740
|
+
coronal_slice,
|
|
741
|
+
rotation_sequence,
|
|
742
|
+
rotation_angles,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Update all views
|
|
746
|
+
self.server.controller.view_update()
|
|
747
|
+
|
|
748
|
+
def update_mpr_window_level(self, **kwargs):
|
|
749
|
+
"""Update MPR window/level when sliders change."""
|
|
750
|
+
|
|
751
|
+
if not getattr(self.server.state, "mpr_enabled", False):
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
active_volume_label = getattr(self.server.state, "active_volume_label", "")
|
|
755
|
+
if not active_volume_label:
|
|
756
|
+
return
|
|
757
|
+
|
|
758
|
+
# Find the active volume
|
|
759
|
+
active_volume = None
|
|
760
|
+
for volume in self.scene.volumes:
|
|
761
|
+
if volume.label == active_volume_label:
|
|
762
|
+
active_volume = volume
|
|
763
|
+
break
|
|
764
|
+
|
|
765
|
+
if not active_volume:
|
|
766
|
+
return
|
|
767
|
+
|
|
768
|
+
# Get current window/level values
|
|
769
|
+
window = getattr(self.server.state, "mpr_window", 400.0)
|
|
770
|
+
level = getattr(self.server.state, "mpr_level", 40.0)
|
|
771
|
+
current_frame = getattr(self.server.state, "frame", 0)
|
|
772
|
+
|
|
773
|
+
# Check if this change is from manual adjustment (not from preset)
|
|
774
|
+
# by checking if we're not in the middle of a preset update
|
|
775
|
+
if not getattr(self, "_updating_from_preset", False):
|
|
776
|
+
# Reset preset selection when manually adjusting window/level
|
|
777
|
+
current_preset = getattr(self.server.state, "mpr_window_level_preset", None)
|
|
778
|
+
if current_preset is not None:
|
|
779
|
+
self.server.state.mpr_window_level_preset = None
|
|
780
|
+
|
|
781
|
+
# Update window/level for MPR actors
|
|
782
|
+
active_volume.update_mpr_window_level(current_frame, window, level)
|
|
783
|
+
|
|
784
|
+
# Update all views
|
|
785
|
+
self.server.controller.view_update()
|
|
786
|
+
|
|
787
|
+
def update_mpr_preset(self, mpr_window_level_preset, **kwargs):
|
|
788
|
+
"""Update MPR window/level when preset changes."""
|
|
789
|
+
from .window_level import presets
|
|
790
|
+
|
|
791
|
+
# Handle None value (Select W/L... option) - do nothing
|
|
792
|
+
if mpr_window_level_preset is None:
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
if mpr_window_level_preset in presets:
|
|
796
|
+
preset = presets[mpr_window_level_preset]
|
|
797
|
+
|
|
798
|
+
# Set flag to indicate we're updating from preset
|
|
799
|
+
self._updating_from_preset = True
|
|
800
|
+
try:
|
|
801
|
+
self.server.state.mpr_window = preset.window
|
|
802
|
+
self.server.state.mpr_level = preset.level
|
|
803
|
+
|
|
804
|
+
# Update the actual MPR views with new window/level
|
|
805
|
+
self.update_mpr_window_level()
|
|
806
|
+
finally:
|
|
807
|
+
# Always clear the flag
|
|
808
|
+
self._updating_from_preset = False
|
|
809
|
+
|
|
810
|
+
def update_mpr_rotation(self, **kwargs):
|
|
811
|
+
"""Update MPR views when rotation changes."""
|
|
812
|
+
if not getattr(self.server.state, "mpr_enabled", False):
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
active_volume_label = getattr(self.server.state, "active_volume_label", "")
|
|
816
|
+
if not active_volume_label:
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
# Find the active volume
|
|
820
|
+
active_volume = None
|
|
821
|
+
for volume in self.scene.volumes:
|
|
822
|
+
if volume.label == active_volume_label:
|
|
823
|
+
active_volume = volume
|
|
824
|
+
break
|
|
825
|
+
|
|
826
|
+
if not active_volume:
|
|
827
|
+
return
|
|
828
|
+
|
|
829
|
+
# Get current slice positions
|
|
830
|
+
axial_slice = getattr(self.server.state, "axial_slice", 0.5)
|
|
831
|
+
sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
|
|
832
|
+
coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
|
|
833
|
+
current_frame = getattr(self.server.state, "frame", 0)
|
|
834
|
+
|
|
835
|
+
# Get rotation data - include all visible rotations
|
|
836
|
+
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
837
|
+
rotation_angles = {}
|
|
838
|
+
|
|
839
|
+
# Include all visible rotations regardless of position
|
|
840
|
+
for i in range(len(rotation_sequence)):
|
|
841
|
+
is_visible = getattr(self.server.state, f"mpr_rotation_visible_{i}", True)
|
|
842
|
+
if is_visible:
|
|
843
|
+
rotation_angles[i] = getattr(
|
|
844
|
+
self.server.state, f"mpr_rotation_angle_{i}", 0
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
# Update slice positions with rotation
|
|
848
|
+
active_volume.update_slice_positions(
|
|
849
|
+
current_frame,
|
|
850
|
+
axial_slice,
|
|
851
|
+
sagittal_slice,
|
|
852
|
+
coronal_slice,
|
|
853
|
+
rotation_sequence,
|
|
854
|
+
rotation_angles,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
# Update all views
|
|
858
|
+
self.server.controller.view_update()
|
|
859
|
+
|
|
860
|
+
def add_mpr_rotation(self, axis):
|
|
861
|
+
"""Add a new rotation to the MPR rotation sequence."""
|
|
862
|
+
import copy
|
|
863
|
+
|
|
864
|
+
current_sequence = copy.deepcopy(
|
|
865
|
+
getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
866
|
+
)
|
|
867
|
+
current_sequence.append({"axis": axis, "angle": 0})
|
|
868
|
+
self.server.state.mpr_rotation_sequence = current_sequence
|
|
869
|
+
self.update_mpr_rotation_labels()
|
|
870
|
+
|
|
871
|
+
def remove_mpr_rotation(self, index):
|
|
872
|
+
"""Remove a rotation at given index and all subsequent rotations."""
|
|
873
|
+
sequence = list(getattr(self.server.state, "mpr_rotation_sequence", []))
|
|
874
|
+
if 0 <= index < len(sequence):
|
|
875
|
+
sequence = sequence[:index]
|
|
876
|
+
self.server.state.mpr_rotation_sequence = sequence
|
|
877
|
+
|
|
878
|
+
# Reset angle states for all removed rotations
|
|
879
|
+
for i in range(index, self.scene.max_mpr_rotations):
|
|
880
|
+
setattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
|
|
881
|
+
|
|
882
|
+
self.update_mpr_rotation_labels()
|
|
883
|
+
|
|
884
|
+
def reset_mpr_rotations(self):
|
|
885
|
+
"""Reset all MPR rotations."""
|
|
886
|
+
self.server.state.mpr_rotation_sequence = []
|
|
887
|
+
for i in range(self.scene.max_mpr_rotations):
|
|
888
|
+
setattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
|
|
889
|
+
setattr(self.server.state, f"mpr_rotation_visible_{i}", True)
|
|
890
|
+
self.update_mpr_rotation_labels()
|
|
891
|
+
|
|
892
|
+
def update_mpr_rotation_labels(self):
|
|
893
|
+
"""Update the rotation axis labels for display."""
|
|
894
|
+
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
895
|
+
for i, rotation in enumerate(rotation_sequence):
|
|
896
|
+
setattr(
|
|
897
|
+
self.server.state,
|
|
898
|
+
f"mpr_rotation_axis_{i}",
|
|
899
|
+
f"{rotation['axis']} ({i + 1})",
|
|
900
|
+
)
|
|
901
|
+
# Clear unused labels
|
|
902
|
+
for i in range(len(rotation_sequence), self.scene.max_mpr_rotations):
|
|
903
|
+
setattr(self.server.state, f"mpr_rotation_axis_{i}", f"Rotation {i + 1}")
|
|
904
|
+
|
|
905
|
+
@asynchronous.task
|
|
906
|
+
async def close_application(self):
|
|
907
|
+
"""Close the application by stopping the server."""
|
|
908
|
+
await self.server.stop()
|
cardio/mesh.py
CHANGED
|
@@ -3,7 +3,6 @@ import enum
|
|
|
3
3
|
import logging
|
|
4
4
|
|
|
5
5
|
# Third Party
|
|
6
|
-
import numpy as np
|
|
7
6
|
import pydantic as pc
|
|
8
7
|
import vtk
|
|
9
8
|
|
|
@@ -167,9 +166,9 @@ class Mesh(Object):
|
|
|
167
166
|
|
|
168
167
|
def color_transfer_function(self):
|
|
169
168
|
ctf = vtk.vtkColorTransferFunction()
|
|
170
|
-
ctf.AddRGBPoint(
|
|
169
|
+
ctf.AddRGBPoint(self.ctf_min, 0.0, 0.0, 1.0)
|
|
171
170
|
ctf.AddRGBPoint(1.0, 1.0, 0.0, 0.0)
|
|
172
|
-
ctf.AddRGBPoint(
|
|
171
|
+
ctf.AddRGBPoint(self.ctf_max, 1.0, 1.0, 0.0)
|
|
173
172
|
return ctf
|
|
174
173
|
|
|
175
174
|
def setup_scalar_coloring(self, mapper):
|