cardio 2025.8.1__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.1.dist-info → cardio-2025.9.0.dist-info}/METADATA +5 -3
- cardio-2025.9.0.dist-info/RECORD +28 -0
- cardio/transfer_functions.py +0 -272
- cardio-2025.8.1.dist-info/RECORD +0 -22
- {cardio-2025.8.1.dist-info → cardio-2025.9.0.dist-info}/WHEEL +0 -0
- {cardio-2025.8.1.dist-info → cardio-2025.9.0.dist-info}/entry_points.txt +0 -0
cardio/scene.py
CHANGED
@@ -93,7 +93,7 @@ class Scene(ps.BaseSettings):
|
|
93
93
|
project_name: str = "Cardio"
|
94
94
|
current_frame: int = 0
|
95
95
|
screenshot_directory: pl.Path = pl.Path("./data/screenshots")
|
96
|
-
screenshot_subdirectory_format:
|
96
|
+
screenshot_subdirectory_format: str = "%Y-%m-%d-%H-%M-%S"
|
97
97
|
rotation_factor: float = 3.0
|
98
98
|
background: Background = Field(
|
99
99
|
default_factory=Background,
|
@@ -102,6 +102,36 @@ class Scene(ps.BaseSettings):
|
|
102
102
|
meshes: MeshList = Field(default_factory=list)
|
103
103
|
volumes: VolumeList = Field(default_factory=list)
|
104
104
|
segmentations: SegmentationList = Field(default_factory=list)
|
105
|
+
mpr_enabled: bool = Field(
|
106
|
+
default=False,
|
107
|
+
description="Enable multi-planar reconstruction (MPR) mode with quad-view layout",
|
108
|
+
)
|
109
|
+
active_volume_label: str = Field(
|
110
|
+
default="",
|
111
|
+
description="Label of the volume to use for multi-planar reconstruction",
|
112
|
+
)
|
113
|
+
axial_slice: float = Field(
|
114
|
+
default=0.5, description="Axial slice position as fraction (0.0 to 1.0)"
|
115
|
+
)
|
116
|
+
sagittal_slice: float = Field(
|
117
|
+
default=0.5, description="Sagittal slice position as fraction (0.0 to 1.0)"
|
118
|
+
)
|
119
|
+
coronal_slice: float = Field(
|
120
|
+
default=0.5, description="Coronal slice position as fraction (0.0 to 1.0)"
|
121
|
+
)
|
122
|
+
mpr_window: float = Field(
|
123
|
+
default=800.0, description="Window width for MPR image display"
|
124
|
+
)
|
125
|
+
mpr_level: float = Field(
|
126
|
+
default=200.0, description="Window level for MPR image display"
|
127
|
+
)
|
128
|
+
mpr_window_level_preset: int = Field(
|
129
|
+
default=7, description="Window/level preset key for MPR views"
|
130
|
+
)
|
131
|
+
mpr_rotation_sequence: list = Field(
|
132
|
+
default_factory=list,
|
133
|
+
description="Dynamic rotation sequence for MPR views - list of rotation steps",
|
134
|
+
)
|
105
135
|
|
106
136
|
# Field validators for JSON string inputs
|
107
137
|
@field_validator("meshes", mode="before")
|
@@ -134,6 +164,11 @@ class Scene(ps.BaseSettings):
|
|
134
164
|
default_factory=vtk.vtkRenderWindowInteractor
|
135
165
|
)
|
136
166
|
|
167
|
+
# 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)
|
171
|
+
|
137
172
|
@property
|
138
173
|
def renderer(self) -> vtk.vtkRenderer:
|
139
174
|
return self._renderer
|
@@ -146,8 +181,26 @@ class Scene(ps.BaseSettings):
|
|
146
181
|
def renderWindowInteractor(self) -> vtk.vtkRenderWindowInteractor:
|
147
182
|
return self._renderWindowInteractor
|
148
183
|
|
184
|
+
@property
|
185
|
+
def axial_renderWindow(self) -> vtk.vtkRenderWindow:
|
186
|
+
return self._axial_renderWindow
|
187
|
+
|
188
|
+
@property
|
189
|
+
def coronal_renderWindow(self) -> vtk.vtkRenderWindow:
|
190
|
+
return self._coronal_renderWindow
|
191
|
+
|
192
|
+
@property
|
193
|
+
def sagittal_renderWindow(self) -> vtk.vtkRenderWindow:
|
194
|
+
return self._sagittal_renderWindow
|
195
|
+
|
149
196
|
@model_validator(mode="after")
|
150
197
|
def setup_scene(self):
|
198
|
+
# Validate unique labels
|
199
|
+
self._validate_unique_labels()
|
200
|
+
|
201
|
+
# Validate active volume label
|
202
|
+
self._validate_active_volume_label()
|
203
|
+
|
151
204
|
# Configure VTK objects
|
152
205
|
self._renderer.SetBackground(
|
153
206
|
*self.background.light,
|
@@ -173,6 +226,44 @@ class Scene(ps.BaseSettings):
|
|
173
226
|
|
174
227
|
return self
|
175
228
|
|
229
|
+
def _validate_unique_labels(self):
|
230
|
+
mesh_labels = [mesh.label for mesh in self.meshes]
|
231
|
+
volume_labels = [volume.label for volume in self.volumes]
|
232
|
+
segmentation_labels = [seg.label for seg in self.segmentations]
|
233
|
+
|
234
|
+
if len(mesh_labels) != len(set(mesh_labels)):
|
235
|
+
duplicates = [
|
236
|
+
label for label in set(mesh_labels) if mesh_labels.count(label) > 1
|
237
|
+
]
|
238
|
+
raise ValueError(f"Duplicate mesh labels found: {duplicates}")
|
239
|
+
|
240
|
+
if len(volume_labels) != len(set(volume_labels)):
|
241
|
+
duplicates = [
|
242
|
+
label for label in set(volume_labels) if volume_labels.count(label) > 1
|
243
|
+
]
|
244
|
+
raise ValueError(f"Duplicate volume labels found: {duplicates}")
|
245
|
+
|
246
|
+
if len(segmentation_labels) != len(set(segmentation_labels)):
|
247
|
+
duplicates = [
|
248
|
+
label
|
249
|
+
for label in set(segmentation_labels)
|
250
|
+
if segmentation_labels.count(label) > 1
|
251
|
+
]
|
252
|
+
raise ValueError(f"Duplicate segmentation labels found: {duplicates}")
|
253
|
+
|
254
|
+
def _validate_active_volume_label(self):
|
255
|
+
"""Validate that active_volume_label refers to an existing volume."""
|
256
|
+
if self.active_volume_label and self.volumes:
|
257
|
+
volume_labels = [volume.label for volume in self.volumes]
|
258
|
+
if self.active_volume_label not in volume_labels:
|
259
|
+
raise ValueError(
|
260
|
+
f"Active volume label '{self.active_volume_label}' not found in available volumes: {volume_labels}"
|
261
|
+
)
|
262
|
+
elif self.active_volume_label and not self.volumes:
|
263
|
+
raise ValueError(
|
264
|
+
"Active volume label specified but no volumes are available"
|
265
|
+
)
|
266
|
+
|
176
267
|
@property
|
177
268
|
def nframes(self) -> int:
|
178
269
|
ns = []
|
@@ -209,6 +300,46 @@ class Scene(ps.BaseSettings):
|
|
209
300
|
self.show_frame(self.current_frame)
|
210
301
|
self.renderer.ResetCamera()
|
211
302
|
|
303
|
+
# Set default camera elevation to -90 degrees
|
304
|
+
camera = self.renderer.GetActiveCamera()
|
305
|
+
camera.Elevation(-90)
|
306
|
+
|
307
|
+
def setup_mpr_render_windows(self):
|
308
|
+
"""Initialize MPR render windows when MPR mode is enabled."""
|
309
|
+
if self._axial_renderWindow is None:
|
310
|
+
# Create axial render window
|
311
|
+
self._axial_renderWindow = vtk.vtkRenderWindow()
|
312
|
+
axial_renderer = vtk.vtkRenderer()
|
313
|
+
axial_renderer.SetBackground(0.0, 0.0, 0.0) # Black background
|
314
|
+
self._axial_renderWindow.AddRenderer(axial_renderer)
|
315
|
+
self._axial_renderWindow.SetOffScreenRendering(True)
|
316
|
+
|
317
|
+
# Create and set interactor for axial
|
318
|
+
axial_interactor = vtk.vtkRenderWindowInteractor()
|
319
|
+
self._axial_renderWindow.SetInteractor(axial_interactor)
|
320
|
+
|
321
|
+
# Create coronal render window
|
322
|
+
self._coronal_renderWindow = vtk.vtkRenderWindow()
|
323
|
+
coronal_renderer = vtk.vtkRenderer()
|
324
|
+
coronal_renderer.SetBackground(0.0, 0.0, 0.0) # Black background
|
325
|
+
self._coronal_renderWindow.AddRenderer(coronal_renderer)
|
326
|
+
self._coronal_renderWindow.SetOffScreenRendering(True)
|
327
|
+
|
328
|
+
# Create and set interactor for coronal
|
329
|
+
coronal_interactor = vtk.vtkRenderWindowInteractor()
|
330
|
+
self._coronal_renderWindow.SetInteractor(coronal_interactor)
|
331
|
+
|
332
|
+
# Create sagittal render window
|
333
|
+
self._sagittal_renderWindow = vtk.vtkRenderWindow()
|
334
|
+
sagittal_renderer = vtk.vtkRenderer()
|
335
|
+
sagittal_renderer.SetBackground(0.0, 0.0, 0.0) # Black background
|
336
|
+
self._sagittal_renderWindow.AddRenderer(sagittal_renderer)
|
337
|
+
self._sagittal_renderWindow.SetOffScreenRendering(True)
|
338
|
+
|
339
|
+
# Create and set interactor for sagittal
|
340
|
+
sagittal_interactor = vtk.vtkRenderWindowInteractor()
|
341
|
+
self._sagittal_renderWindow.SetInteractor(sagittal_interactor)
|
342
|
+
|
212
343
|
def hide_all_frames(self):
|
213
344
|
for a in self.renderer.GetActors():
|
214
345
|
a.SetVisibility(False)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Third Party
|
2
|
+
import pydantic as pc
|
3
|
+
import vtk
|
4
|
+
|
5
|
+
# Internal
|
6
|
+
from .color_transfer_function import ColorTransferFunctionConfig
|
7
|
+
from .piecewise_function import PiecewiseFunctionConfig
|
8
|
+
|
9
|
+
|
10
|
+
class TransferFunctionPairConfig(pc.BaseModel):
|
11
|
+
"""Configuration for a pair of opacity and color transfer functions."""
|
12
|
+
|
13
|
+
opacity: PiecewiseFunctionConfig = pc.Field(
|
14
|
+
description="Opacity transfer function configuration"
|
15
|
+
)
|
16
|
+
color: ColorTransferFunctionConfig = pc.Field(
|
17
|
+
description="Color transfer function configuration"
|
18
|
+
)
|
19
|
+
|
20
|
+
@property
|
21
|
+
def vtk_functions(
|
22
|
+
self,
|
23
|
+
) -> tuple[vtk.vtkPiecewiseFunction, vtk.vtkColorTransferFunction]:
|
24
|
+
"""Create VTK transfer functions from this pair configuration."""
|
25
|
+
return self.opacity.vtk_function, self.color.vtk_function
|