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/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: pl.Path = pl.Path("%Y-%m-%d-%H-%M-%S")
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