cardio 2023.1.2__py3-none-any.whl → 2025.8.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 +13 -5
- cardio/app.py +45 -14
- cardio/assets/bone.toml +42 -0
- cardio/assets/vascular_closed.toml +78 -0
- cardio/assets/vascular_open.toml +54 -0
- cardio/assets/xray.toml +42 -0
- cardio/logic.py +273 -10
- cardio/mesh.py +249 -29
- cardio/object.py +183 -10
- cardio/property_config.py +56 -0
- cardio/scene.py +204 -65
- cardio/screenshot.py +4 -5
- cardio/segmentation.py +178 -0
- cardio/transfer_functions.py +272 -0
- cardio/types.py +18 -0
- cardio/ui.py +359 -80
- cardio/utils.py +101 -0
- cardio/volume.py +41 -96
- cardio-2025.8.0.dist-info/METADATA +94 -0
- cardio-2025.8.0.dist-info/RECORD +22 -0
- cardio-2025.8.0.dist-info/WHEEL +4 -0
- {cardio-2023.1.2.dist-info → cardio-2025.8.0.dist-info}/entry_points.txt +1 -0
- __init__.py +0 -0
- cardio-2023.1.2.dist-info/LICENSE +0 -15
- cardio-2023.1.2.dist-info/METADATA +0 -69
- cardio-2023.1.2.dist-info/RECORD +0 -16
- cardio-2023.1.2.dist-info/WHEEL +0 -5
- cardio-2023.1.2.dist-info/top_level.txt +0 -2
cardio/ui.py
CHANGED
@@ -2,7 +2,8 @@ from trame.ui.vuetify import SinglePageWithDrawerLayout
|
|
2
2
|
from trame.widgets import vtk as vtk_widgets
|
3
3
|
from trame.widgets import vuetify
|
4
4
|
|
5
|
-
from . import Scene
|
5
|
+
from .scene import Scene
|
6
|
+
from .transfer_functions import list_available_presets
|
6
7
|
|
7
8
|
|
8
9
|
class UI:
|
@@ -13,7 +14,6 @@ class UI:
|
|
13
14
|
self.setup()
|
14
15
|
|
15
16
|
def setup(self):
|
16
|
-
|
17
17
|
self.server.state.trame__title = self.scene.project_name
|
18
18
|
|
19
19
|
with SinglePageWithDrawerLayout(self.server) as layout:
|
@@ -37,17 +37,26 @@ class UI:
|
|
37
37
|
# )
|
38
38
|
|
39
39
|
vuetify.VCheckbox(
|
40
|
-
v_model="
|
40
|
+
v_model=("dark_mode", False),
|
41
41
|
on_icon="mdi-lightbulb-outline",
|
42
42
|
off_icon="mdi-lightbulb-off-outline",
|
43
43
|
classes="mx-1",
|
44
44
|
hide_details=True,
|
45
45
|
dense=True,
|
46
46
|
outlined=True,
|
47
|
+
change="$vuetify.theme.dark = $event",
|
47
48
|
)
|
48
49
|
|
49
|
-
|
50
|
-
|
50
|
+
# NOTE: Reset button should be VBtn, but using VCheckbox for consistent sizing/spacing
|
51
|
+
vuetify.VCheckbox(
|
52
|
+
value=False,
|
53
|
+
on_icon="mdi-undo-variant",
|
54
|
+
off_icon="mdi-undo-variant",
|
55
|
+
hide_details=True,
|
56
|
+
title="Reset All",
|
57
|
+
click=self.server.controller.reset_all,
|
58
|
+
readonly=True,
|
59
|
+
)
|
51
60
|
|
52
61
|
vuetify.VProgressLinear(
|
53
62
|
indeterminate=True,
|
@@ -61,7 +70,6 @@ class UI:
|
|
61
70
|
fluid=True,
|
62
71
|
classes="pa-0 fill-height",
|
63
72
|
):
|
64
|
-
|
65
73
|
view = vtk_widgets.VtkRemoteView(
|
66
74
|
self.scene.renderWindow, interactive_ratio=1
|
67
75
|
)
|
@@ -77,64 +85,68 @@ class UI:
|
|
77
85
|
self.server.controller.on_server_ready.add(view.update)
|
78
86
|
|
79
87
|
with layout.drawer:
|
88
|
+
vuetify.VSubheader("Playback Controls")
|
80
89
|
|
81
|
-
with vuetify.
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
90
|
+
with vuetify.VToolbar(dense=True, flat=True):
|
91
|
+
# NOTE: Previous/Next controls should be VBtn components, but we use
|
92
|
+
# VCheckbox for consistent sizing/spacing with the other controls.
|
93
|
+
# This may be easier to fix in Vuetify 3.
|
94
|
+
vuetify.VCheckbox(
|
95
|
+
value=False,
|
96
|
+
on_icon="mdi-skip-previous-circle",
|
97
|
+
off_icon="mdi-skip-previous-circle",
|
98
|
+
hide_details=True,
|
99
|
+
title="Previous",
|
100
|
+
click=self.server.controller.decrement_frame,
|
101
|
+
readonly=True,
|
102
|
+
)
|
87
103
|
|
88
|
-
|
89
|
-
icon=True,
|
90
|
-
click=self.server.controller.increment_frame,
|
91
|
-
title="Next",
|
92
|
-
):
|
93
|
-
vuetify.VIcon("mdi-skip-next-circle")
|
104
|
+
vuetify.VSpacer()
|
94
105
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
106
|
+
vuetify.VCheckbox(
|
107
|
+
value=False,
|
108
|
+
on_icon="mdi-skip-next-circle",
|
109
|
+
off_icon="mdi-skip-next-circle",
|
110
|
+
hide_details=True,
|
111
|
+
title="Next",
|
112
|
+
click=self.server.controller.increment_frame,
|
113
|
+
readonly=True,
|
114
|
+
)
|
104
115
|
|
105
|
-
|
106
|
-
v_model=("incrementing", True),
|
107
|
-
on_icon="mdi-movie-open-outline",
|
108
|
-
off_icon="mdi-movie-open-off-outline",
|
109
|
-
classes="mx-1",
|
110
|
-
hide_details=True,
|
111
|
-
dense=True,
|
112
|
-
label="Incrementing",
|
113
|
-
title="Incrementing",
|
114
|
-
)
|
116
|
+
vuetify.VSpacer()
|
115
117
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
label="Rotating",
|
124
|
-
title="Rotating",
|
125
|
-
)
|
118
|
+
vuetify.VCheckbox(
|
119
|
+
v_model=("playing", False),
|
120
|
+
on_icon="mdi-pause-circle",
|
121
|
+
off_icon="mdi-play-circle",
|
122
|
+
title="Play/Pause",
|
123
|
+
hide_details=True,
|
124
|
+
)
|
126
125
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
126
|
+
vuetify.VSpacer()
|
127
|
+
|
128
|
+
vuetify.VCheckbox(
|
129
|
+
v_model=("incrementing", True),
|
130
|
+
on_icon="mdi-movie-open-outline",
|
131
|
+
off_icon="mdi-movie-open-off-outline",
|
132
|
+
hide_details=True,
|
133
|
+
title="Incrementing",
|
134
|
+
)
|
135
|
+
|
136
|
+
vuetify.VSpacer()
|
137
|
+
|
138
|
+
vuetify.VCheckbox(
|
139
|
+
v_model=("rotating", False),
|
140
|
+
on_icon="mdi-autorenew",
|
141
|
+
off_icon="mdi-autorenew-off",
|
142
|
+
hide_details=True,
|
143
|
+
title="Rotating",
|
144
|
+
)
|
134
145
|
|
135
146
|
vuetify.VSlider(
|
136
147
|
v_model=("frame", self.scene.current_frame),
|
137
|
-
|
148
|
+
hint="Phase",
|
149
|
+
persistent_hint=True,
|
138
150
|
min=0,
|
139
151
|
max=self.scene.nframes - 1,
|
140
152
|
step=1,
|
@@ -147,11 +159,10 @@ class UI:
|
|
147
159
|
|
148
160
|
vuetify.VSlider(
|
149
161
|
v_model=("bpm", 60),
|
150
|
-
|
151
|
-
hint="Beats Per Minute",
|
162
|
+
hint="Speed",
|
152
163
|
persistent_hint=True,
|
153
164
|
min=40,
|
154
|
-
max=
|
165
|
+
max=1024,
|
155
166
|
step=1,
|
156
167
|
hide_details=False,
|
157
168
|
dense=True,
|
@@ -162,11 +173,10 @@ class UI:
|
|
162
173
|
|
163
174
|
vuetify.VSlider(
|
164
175
|
v_model=("bpr", 3),
|
165
|
-
|
166
|
-
hint="Beats Per Rotation",
|
176
|
+
hint="Cycles/Rotation",
|
167
177
|
persistent_hint=True,
|
168
178
|
min=1,
|
169
|
-
max=
|
179
|
+
max=360,
|
170
180
|
step=1,
|
171
181
|
hide_details=False,
|
172
182
|
dense=True,
|
@@ -175,24 +185,293 @@ class UI:
|
|
175
185
|
thumb_label=True,
|
176
186
|
)
|
177
187
|
|
178
|
-
|
179
|
-
vuetify.
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
hide_details=True,
|
185
|
-
dense=True,
|
186
|
-
label=m.label,
|
188
|
+
with vuetify.VRow(justify="center", classes="my-3"):
|
189
|
+
vuetify.VBtn(
|
190
|
+
f"Capture Cine",
|
191
|
+
small=True,
|
192
|
+
click=self.server.controller.screenshot,
|
193
|
+
title=f"Capture cine to {self.scene.screenshot_directory}",
|
187
194
|
)
|
188
195
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
196
|
+
vuetify.VSubheader("Appearance and Visibility")
|
197
|
+
|
198
|
+
if self.scene.meshes:
|
199
|
+
vuetify.VSubheader("Meshes", classes="text-caption pl-4")
|
200
|
+
for i, m in enumerate(self.scene.meshes):
|
201
|
+
vuetify.VCheckbox(
|
202
|
+
v_model=f"mesh_visibility_{m.label}",
|
203
|
+
on_icon="mdi-eye",
|
204
|
+
off_icon="mdi-eye-off",
|
205
|
+
classes="mx-1",
|
206
|
+
hide_details=True,
|
207
|
+
dense=True,
|
208
|
+
label=m.label,
|
209
|
+
)
|
210
|
+
if m.clipping_enabled:
|
211
|
+
vuetify.VCheckbox(
|
212
|
+
v_model=(
|
213
|
+
f"mesh_clipping_{m.label}",
|
214
|
+
m.clipping_enabled,
|
215
|
+
),
|
216
|
+
on_icon="mdi-content-cut",
|
217
|
+
off_icon="mdi-content-cut",
|
218
|
+
classes="mx-1 ml-4",
|
219
|
+
hide_details=True,
|
220
|
+
dense=True,
|
221
|
+
label="Clip",
|
222
|
+
)
|
223
|
+
|
224
|
+
# Get initial mesh bounds for sliders
|
225
|
+
if m.actors:
|
226
|
+
bounds = m.combined_bounds
|
227
|
+
with vuetify.VExpansionPanels(
|
228
|
+
v_model=f"clip_panel_{m.label}",
|
229
|
+
multiple=True,
|
230
|
+
flat=True,
|
231
|
+
classes="ml-4",
|
232
|
+
dense=True,
|
233
|
+
style="max-width: 270px;",
|
234
|
+
):
|
235
|
+
with vuetify.VExpansionPanel():
|
236
|
+
vuetify.VExpansionPanelHeader("Clip Bounds")
|
237
|
+
with vuetify.VExpansionPanelContent():
|
238
|
+
# X bounds
|
239
|
+
vuetify.VRangeSlider(
|
240
|
+
v_model=(
|
241
|
+
f"clip_x_{m.label}",
|
242
|
+
[bounds[0], bounds[1]],
|
243
|
+
),
|
244
|
+
label="X Range",
|
245
|
+
min=bounds[0],
|
246
|
+
max=bounds[1],
|
247
|
+
step=(bounds[1] - bounds[0]) / 100,
|
248
|
+
hide_details=True,
|
249
|
+
dense=True,
|
250
|
+
thumb_label=False,
|
251
|
+
)
|
252
|
+
# Y bounds
|
253
|
+
vuetify.VRangeSlider(
|
254
|
+
v_model=(
|
255
|
+
f"clip_y_{m.label}",
|
256
|
+
[bounds[2], bounds[3]],
|
257
|
+
),
|
258
|
+
label="Y Range",
|
259
|
+
min=bounds[2],
|
260
|
+
max=bounds[3],
|
261
|
+
step=(bounds[3] - bounds[2]) / 100,
|
262
|
+
hide_details=True,
|
263
|
+
dense=True,
|
264
|
+
thumb_label=False,
|
265
|
+
)
|
266
|
+
# Z bounds
|
267
|
+
vuetify.VRangeSlider(
|
268
|
+
v_model=(
|
269
|
+
f"clip_z_{m.label}",
|
270
|
+
[bounds[4], bounds[5]],
|
271
|
+
),
|
272
|
+
label="Z Range",
|
273
|
+
min=bounds[4],
|
274
|
+
max=bounds[5],
|
275
|
+
step=(bounds[5] - bounds[4]) / 100,
|
276
|
+
hide_details=True,
|
277
|
+
dense=True,
|
278
|
+
thumb_label=False,
|
279
|
+
)
|
280
|
+
|
281
|
+
if self.scene.volumes:
|
282
|
+
vuetify.VSubheader("Volumes", classes="text-caption pl-4")
|
283
|
+
for i, v in enumerate(self.scene.volumes):
|
284
|
+
vuetify.VCheckbox(
|
285
|
+
v_model=f"volume_visibility_{v.label}",
|
286
|
+
on_icon="mdi-eye",
|
287
|
+
off_icon="mdi-eye-off",
|
288
|
+
classes="mx-1",
|
289
|
+
hide_details=True,
|
290
|
+
dense=True,
|
291
|
+
label=v.label,
|
292
|
+
)
|
293
|
+
|
294
|
+
# Preset selection in collapsible panel
|
295
|
+
available_presets = list_available_presets()
|
296
|
+
current_preset = self.server.state[f"volume_preset_{v.label}"]
|
297
|
+
current_desc = available_presets.get(
|
298
|
+
current_preset, current_preset
|
299
|
+
)
|
300
|
+
|
301
|
+
with vuetify.VExpansionPanels(
|
302
|
+
v_model=f"preset_panel_{v.label}",
|
303
|
+
flat=True,
|
304
|
+
classes="ml-4",
|
305
|
+
dense=True,
|
306
|
+
style="max-width: 270px;",
|
307
|
+
):
|
308
|
+
with vuetify.VExpansionPanel():
|
309
|
+
vuetify.VExpansionPanelHeader("Transfer Function")
|
310
|
+
with vuetify.VExpansionPanelContent():
|
311
|
+
with vuetify.VRadioGroup(
|
312
|
+
v_model=f"volume_preset_{v.label}",
|
313
|
+
dense=True,
|
314
|
+
):
|
315
|
+
for (
|
316
|
+
preset_key,
|
317
|
+
preset_desc,
|
318
|
+
) in available_presets.items():
|
319
|
+
vuetify.VRadio(
|
320
|
+
label=preset_desc, value=preset_key
|
321
|
+
)
|
322
|
+
|
323
|
+
# Add clipping controls for volumes
|
324
|
+
if v.clipping_enabled:
|
325
|
+
vuetify.VCheckbox(
|
326
|
+
v_model=(
|
327
|
+
f"volume_clipping_{v.label}",
|
328
|
+
v.clipping_enabled,
|
329
|
+
),
|
330
|
+
on_icon="mdi-cube-outline",
|
331
|
+
off_icon="mdi-cube-off-outline",
|
332
|
+
classes="mx-1 ml-4",
|
333
|
+
hide_details=True,
|
334
|
+
dense=True,
|
335
|
+
label=f"{v.label} Clipping",
|
336
|
+
)
|
337
|
+
|
338
|
+
# Get initial volume bounds for sliders
|
339
|
+
if v.actors:
|
340
|
+
bounds = v.combined_bounds
|
341
|
+
with vuetify.VExpansionPanels(
|
342
|
+
v_model=f"clip_panel_{v.label}",
|
343
|
+
multiple=True,
|
344
|
+
flat=True,
|
345
|
+
classes="ml-4",
|
346
|
+
dense=True,
|
347
|
+
style="max-width: 270px;",
|
348
|
+
):
|
349
|
+
with vuetify.VExpansionPanel():
|
350
|
+
vuetify.VExpansionPanelHeader("Clip Bounds")
|
351
|
+
with vuetify.VExpansionPanelContent():
|
352
|
+
# X bounds
|
353
|
+
vuetify.VRangeSlider(
|
354
|
+
v_model=(
|
355
|
+
f"clip_x_{v.label}",
|
356
|
+
[bounds[0], bounds[1]],
|
357
|
+
),
|
358
|
+
label="X Range",
|
359
|
+
min=bounds[0],
|
360
|
+
max=bounds[1],
|
361
|
+
step=(bounds[1] - bounds[0]) / 100,
|
362
|
+
hide_details=True,
|
363
|
+
dense=True,
|
364
|
+
thumb_label=False,
|
365
|
+
)
|
366
|
+
# Y bounds
|
367
|
+
vuetify.VRangeSlider(
|
368
|
+
v_model=(
|
369
|
+
f"clip_y_{v.label}",
|
370
|
+
[bounds[2], bounds[3]],
|
371
|
+
),
|
372
|
+
label="Y Range",
|
373
|
+
min=bounds[2],
|
374
|
+
max=bounds[3],
|
375
|
+
step=(bounds[3] - bounds[2]) / 100,
|
376
|
+
hide_details=True,
|
377
|
+
dense=True,
|
378
|
+
thumb_label=False,
|
379
|
+
)
|
380
|
+
# Z bounds
|
381
|
+
vuetify.VRangeSlider(
|
382
|
+
v_model=(
|
383
|
+
f"clip_z_{v.label}",
|
384
|
+
[bounds[4], bounds[5]],
|
385
|
+
),
|
386
|
+
label="Z Range",
|
387
|
+
min=bounds[4],
|
388
|
+
max=bounds[5],
|
389
|
+
step=(bounds[5] - bounds[4]) / 100,
|
390
|
+
hide_details=True,
|
391
|
+
dense=True,
|
392
|
+
thumb_label=False,
|
393
|
+
)
|
394
|
+
|
395
|
+
if self.scene.segmentations:
|
396
|
+
vuetify.VSubheader("Segmentations", classes="text-caption pl-4")
|
397
|
+
for i, s in enumerate(self.scene.segmentations):
|
398
|
+
vuetify.VCheckbox(
|
399
|
+
v_model=f"segmentation_visibility_{s.label}",
|
400
|
+
on_icon="mdi-eye",
|
401
|
+
off_icon="mdi-eye-off",
|
402
|
+
classes="mx-1",
|
403
|
+
hide_details=True,
|
404
|
+
dense=True,
|
405
|
+
label=s.label,
|
406
|
+
)
|
407
|
+
|
408
|
+
if s.clipping_enabled:
|
409
|
+
vuetify.VCheckbox(
|
410
|
+
v_model=(
|
411
|
+
f"segmentation_clipping_{s.label}",
|
412
|
+
s.clipping_enabled,
|
413
|
+
),
|
414
|
+
on_icon="mdi-content-cut",
|
415
|
+
off_icon="mdi-content-cut",
|
416
|
+
classes="mx-1 ml-4",
|
417
|
+
hide_details=True,
|
418
|
+
dense=True,
|
419
|
+
label="Clip",
|
420
|
+
)
|
421
|
+
|
422
|
+
# Get initial segmentation bounds for sliders
|
423
|
+
if s.actors:
|
424
|
+
bounds = s.combined_bounds
|
425
|
+
with vuetify.VExpansionPanels(
|
426
|
+
v_model=f"clip_panel_{s.label}",
|
427
|
+
multiple=True,
|
428
|
+
flat=True,
|
429
|
+
classes="ml-4",
|
430
|
+
dense=True,
|
431
|
+
style="max-width: 270px;",
|
432
|
+
):
|
433
|
+
with vuetify.VExpansionPanel():
|
434
|
+
vuetify.VExpansionPanelHeader("Clip Bounds")
|
435
|
+
with vuetify.VExpansionPanelContent():
|
436
|
+
# X bounds
|
437
|
+
vuetify.VRangeSlider(
|
438
|
+
v_model=(
|
439
|
+
f"clip_x_{s.label}",
|
440
|
+
[bounds[0], bounds[1]],
|
441
|
+
),
|
442
|
+
label="X Range",
|
443
|
+
min=bounds[0],
|
444
|
+
max=bounds[1],
|
445
|
+
step=(bounds[1] - bounds[0]) / 100,
|
446
|
+
hide_details=True,
|
447
|
+
dense=True,
|
448
|
+
thumb_label=False,
|
449
|
+
)
|
450
|
+
# Y bounds
|
451
|
+
vuetify.VRangeSlider(
|
452
|
+
v_model=(
|
453
|
+
f"clip_y_{s.label}",
|
454
|
+
[bounds[2], bounds[3]],
|
455
|
+
),
|
456
|
+
label="Y Range",
|
457
|
+
min=bounds[2],
|
458
|
+
max=bounds[3],
|
459
|
+
step=(bounds[3] - bounds[2]) / 100,
|
460
|
+
hide_details=True,
|
461
|
+
dense=True,
|
462
|
+
thumb_label=False,
|
463
|
+
)
|
464
|
+
# Z bounds
|
465
|
+
vuetify.VRangeSlider(
|
466
|
+
v_model=(
|
467
|
+
f"clip_z_{s.label}",
|
468
|
+
[bounds[4], bounds[5]],
|
469
|
+
),
|
470
|
+
label="Z Range",
|
471
|
+
min=bounds[4],
|
472
|
+
max=bounds[5],
|
473
|
+
step=(bounds[5] - bounds[4]) / 100,
|
474
|
+
hide_details=True,
|
475
|
+
dense=True,
|
476
|
+
thumb_label=False,
|
477
|
+
)
|
cardio/utils.py
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
"""Utility functions shared across cardio classes."""
|
2
|
+
|
3
|
+
import enum
|
4
|
+
|
5
|
+
import itk
|
6
|
+
import numpy as np
|
7
|
+
|
8
|
+
|
9
|
+
class InterpolatorType(enum.Enum):
|
10
|
+
"""Interpolation methods for image resampling."""
|
11
|
+
|
12
|
+
LINEAR = "linear"
|
13
|
+
NEAREST = "nearest"
|
14
|
+
|
15
|
+
|
16
|
+
def reset_direction(
|
17
|
+
image, interpolator_type: InterpolatorType = InterpolatorType.LINEAR
|
18
|
+
):
|
19
|
+
"""Reset image direction to identity matrix, preserving origin.
|
20
|
+
|
21
|
+
This function handles the VTK reader issue where origin is not retained
|
22
|
+
by using ITK to properly transform the image coordinates.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
image: ITK image object
|
26
|
+
interpolator_type: InterpolatorType enum for interpolation method
|
27
|
+
"""
|
28
|
+
origin = np.asarray(itk.origin(image))
|
29
|
+
spacing = np.asarray(itk.spacing(image))
|
30
|
+
size = np.asarray(itk.size(image))
|
31
|
+
direction = np.asarray(image.GetDirection())
|
32
|
+
|
33
|
+
direction[direction == 1] = 0
|
34
|
+
origin += np.dot(size, np.dot(np.diag(spacing), direction))
|
35
|
+
direction = np.identity(3)
|
36
|
+
|
37
|
+
origin = itk.Point[itk.F, 3](origin)
|
38
|
+
spacing = itk.spacing(image)
|
39
|
+
size = itk.size(image)
|
40
|
+
direction = itk.matrix_from_array(direction)
|
41
|
+
|
42
|
+
# Select interpolator based on type
|
43
|
+
match interpolator_type:
|
44
|
+
case InterpolatorType.NEAREST:
|
45
|
+
interpolator = itk.NearestNeighborInterpolateImageFunction.New(image)
|
46
|
+
case InterpolatorType.LINEAR:
|
47
|
+
interpolator = itk.LinearInterpolateImageFunction.New(image)
|
48
|
+
case _:
|
49
|
+
raise ValueError(f"Unsupported interpolator type: {interpolator_type}")
|
50
|
+
|
51
|
+
output = itk.resample_image_filter(
|
52
|
+
image,
|
53
|
+
size=size,
|
54
|
+
interpolator=interpolator,
|
55
|
+
output_spacing=spacing,
|
56
|
+
output_origin=origin,
|
57
|
+
output_direction=direction,
|
58
|
+
)
|
59
|
+
|
60
|
+
return output
|
61
|
+
|
62
|
+
|
63
|
+
def calculate_combined_bounds(actors):
|
64
|
+
"""Calculate combined bounds encompassing all VTK actors.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
actors: List of VTK actors or list of lists/dicts of actors
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
List of 6 floats: [xmin, xmax, ymin, ymax, zmin, zmax]
|
71
|
+
"""
|
72
|
+
if not actors:
|
73
|
+
return [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
74
|
+
|
75
|
+
# Flatten actors if nested (for segmentations with frame_actors dict)
|
76
|
+
flat_actors = []
|
77
|
+
for item in actors:
|
78
|
+
if isinstance(item, dict):
|
79
|
+
flat_actors.extend(item.values())
|
80
|
+
elif isinstance(item, list):
|
81
|
+
flat_actors.extend(item)
|
82
|
+
else:
|
83
|
+
flat_actors.append(item)
|
84
|
+
|
85
|
+
if not flat_actors:
|
86
|
+
return [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
87
|
+
|
88
|
+
# Start with first actor's bounds
|
89
|
+
combined = list(flat_actors[0].GetBounds())
|
90
|
+
|
91
|
+
# Expand to encompass all actors
|
92
|
+
for actor in flat_actors[1:]:
|
93
|
+
bounds = actor.GetBounds()
|
94
|
+
combined[0] = min(combined[0], bounds[0]) # xmin
|
95
|
+
combined[1] = max(combined[1], bounds[1]) # xmax
|
96
|
+
combined[2] = min(combined[2], bounds[2]) # ymin
|
97
|
+
combined[3] = max(combined[3], bounds[3]) # ymax
|
98
|
+
combined[4] = min(combined[4], bounds[4]) # zmin
|
99
|
+
combined[5] = max(combined[5], bounds[5]) # zmax
|
100
|
+
|
101
|
+
return combined
|