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/utils.py CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  import enum
4
4
 
5
- import itk
6
- import numpy as np
7
-
8
5
 
9
6
  class InterpolatorType(enum.Enum):
10
7
  """Interpolation methods for image resampling."""
@@ -13,51 +10,11 @@ class InterpolatorType(enum.Enum):
13
10
  NEAREST = "nearest"
14
11
 
15
12
 
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.
13
+ class AngleUnit(enum.Enum):
14
+ """Units for angle measurements."""
23
15
 
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
16
+ DEGREES = "degrees"
17
+ RADIANS = "radians"
61
18
 
62
19
 
63
20
  def calculate_combined_bounds(actors):
cardio/volume.py CHANGED
@@ -6,8 +6,14 @@ import pydantic as pc
6
6
  import vtk
7
7
 
8
8
  from .object import Object
9
- from .transfer_functions import load_preset
10
- from .utils import InterpolatorType, reset_direction
9
+ from .orientation import (
10
+ EulerAxis,
11
+ axcode_transform_matrix,
12
+ create_vtk_reslice_matrix,
13
+ euler_angle_to_rotation_matrix,
14
+ reset_direction,
15
+ )
16
+ from .volume_property_presets import load_volume_property_preset
11
17
 
12
18
 
13
19
  class Volume(Object):
@@ -17,8 +23,13 @@ class Volume(Object):
17
23
  default="${frame}.nii.gz",
18
24
  description="Filename pattern with $frame placeholder",
19
25
  )
20
- transfer_function_preset: str = pc.Field(description="Transfer function preset key")
26
+ transfer_function_preset: str = pc.Field(
27
+ default="bone", description="Transfer function preset key"
28
+ )
21
29
  _actors: list[vtk.vtkVolume] = pc.PrivateAttr(default_factory=list)
30
+ _mpr_actors: dict[str, list[vtk.vtkImageActor]] = pc.PrivateAttr(
31
+ default_factory=dict
32
+ )
22
33
 
23
34
  @pc.model_validator(mode="after")
24
35
  def initialize_volume(self):
@@ -27,7 +38,7 @@ class Volume(Object):
27
38
  logging.info(f"{self.label}: Loading frame {frame}.")
28
39
 
29
40
  image = itk.imread(path)
30
- image = reset_direction(image, InterpolatorType.LINEAR)
41
+ image = reset_direction(image)
31
42
  image = itk.vtk_image_from_image(image)
32
43
 
33
44
  mapper = vtk.vtkGPUVolumeRayCastMapper()
@@ -47,7 +58,7 @@ class Volume(Object):
47
58
  @property
48
59
  def preset(self):
49
60
  """Load preset based on transfer_function_preset."""
50
- return load_preset(self.transfer_function_preset)
61
+ return load_volume_property_preset(self.transfer_function_preset)
51
62
 
52
63
  def configure_actors(self):
53
64
  """Configure volume properties without adding to renderer."""
@@ -59,3 +70,214 @@ class Volume(Object):
59
70
  """Apply the current preset to all actors."""
60
71
  for actor in self._actors:
61
72
  actor.SetProperty(self.preset.vtk_property)
73
+
74
+ def create_mpr_actors(self, frame: int = 0):
75
+ """Create MPR (reslice) actors for axial, sagittal, and coronal views."""
76
+ if frame >= len(self._actors):
77
+ frame = 0
78
+
79
+ # Get the image data from the volume actor
80
+ volume_actor = self._actors[frame]
81
+ image_data = volume_actor.GetMapper().GetInput()
82
+
83
+ # Create reslice actors for each orientation
84
+ mpr_actors = {}
85
+
86
+ for orientation in ["axial", "sagittal", "coronal"]:
87
+ # Create reslice filter
88
+ reslice = vtk.vtkImageReslice()
89
+ reslice.SetInputData(image_data)
90
+ reslice.SetOutputDimensionality(2)
91
+ reslice.SetInterpolationModeToLinear()
92
+ reslice.SetBackgroundLevel(-1000.0)
93
+
94
+ # Create image actor
95
+ actor = vtk.vtkImageActor()
96
+ actor.GetMapper().SetInputConnection(reslice.GetOutputPort())
97
+ actor.SetVisibility(False)
98
+
99
+ mpr_actors[orientation] = {"reslice": reslice, "actor": actor}
100
+
101
+ # Store actors for this frame
102
+ if frame not in self._mpr_actors:
103
+ self._mpr_actors[frame] = {}
104
+ self._mpr_actors[frame] = mpr_actors
105
+
106
+ # Set up initial reslice matrices for center slices
107
+ self._setup_center_slices(image_data, frame)
108
+
109
+ return mpr_actors
110
+
111
+ def _setup_center_slices(self, image_data, frame: int):
112
+ """Set up reslice matrices to show center slices using axcode-based coordinate systems."""
113
+ center = image_data.GetCenter()
114
+ actors = self._mpr_actors[frame]
115
+
116
+ # Get coordinate system transformations for each MPR view
117
+ transforms = self._get_mpr_coordinate_systems()
118
+
119
+ # Create reslice matrices directly from transforms
120
+ origin = [center[0], center[1], center[2]]
121
+
122
+ for orientation in ["axial", "sagittal", "coronal"]:
123
+ mat = create_vtk_reslice_matrix(transforms[orientation], origin)
124
+ actors[orientation]["reslice"].SetResliceAxes(mat)
125
+
126
+ @property
127
+ def mpr_actors(self) -> dict[str, list[vtk.vtkImageActor]]:
128
+ """Get MPR actors for all frames."""
129
+ return self._mpr_actors
130
+
131
+ def get_mpr_actors_for_frame(self, frame: int) -> dict:
132
+ """Get MPR actors for a specific frame."""
133
+ if frame not in self._mpr_actors:
134
+ return self.create_mpr_actors(frame)
135
+ return self._mpr_actors[frame]
136
+
137
+ def _get_mpr_coordinate_systems(self):
138
+ """Get coordinate system transformation matrices for MPR views."""
139
+ view_axcodes = {
140
+ "axial": "LAS", # Left-Anterior-Superior
141
+ "sagittal": "ASL", # Anterior-Superior-Left
142
+ "coronal": "LSA", # Left-Superior-Anterior
143
+ }
144
+
145
+ transforms = {}
146
+ for view, target_axcode in view_axcodes.items():
147
+ transforms[view] = axcode_transform_matrix("LPS", target_axcode)
148
+
149
+ return transforms
150
+
151
+ def get_physical_bounds(
152
+ self, frame: int = 0
153
+ ) -> tuple[float, float, float, float, float, float]:
154
+ """Get physical coordinate bounds for the volume.
155
+
156
+ Returns:
157
+ (x_min, x_max, y_min, y_max, z_min, z_max) in LAS coordinate system
158
+ """
159
+ if not self._actors:
160
+ raise RuntimeError(f"No actors configured for volume '{self.label}'")
161
+ if frame >= len(self._actors):
162
+ raise IndexError(
163
+ f"Frame {frame} out of range for volume '{self.label}' (max: {len(self._actors) - 1})"
164
+ )
165
+
166
+ volume_actor = self._actors[frame]
167
+ image_data = volume_actor.GetMapper().GetInput()
168
+
169
+ # Get VTK image metadata
170
+ origin = np.array(image_data.GetOrigin())
171
+ spacing = np.array(image_data.GetSpacing())
172
+ dimensions = np.array(image_data.GetDimensions())
173
+ direction_matrix = np.array(
174
+ [
175
+ [image_data.GetDirectionMatrix().GetElement(i, j) for j in range(3)]
176
+ for i in range(3)
177
+ ]
178
+ )
179
+
180
+ # Calculate antiorigin using direction matrix
181
+ antiorigin = origin + direction_matrix @ (spacing * (dimensions - 1))
182
+
183
+ # Transform both corners from LPS to LAS
184
+ transform = axcode_transform_matrix("LPS", "LAS")
185
+ origin_las = origin @ transform.T
186
+ antiorigin_las = antiorigin @ transform.T
187
+
188
+ # Interleave coordinates directly without min/max
189
+ bounds = (
190
+ origin_las[0],
191
+ antiorigin_las[0], # x bounds
192
+ origin_las[1],
193
+ antiorigin_las[1], # y bounds
194
+ origin_las[2],
195
+ antiorigin_las[2], # z bounds
196
+ )
197
+
198
+ return bounds
199
+
200
+ def update_slice_positions(
201
+ self,
202
+ frame: int,
203
+ axial_pos: float,
204
+ sagittal_pos: float,
205
+ coronal_pos: float,
206
+ rotation_sequence: list = None,
207
+ rotation_angles: dict = None,
208
+ ):
209
+ """Update slice positions for MPR views with optional rotation.
210
+
211
+ Args:
212
+ frame: Frame index
213
+ axial_pos: Physical position along Z axis (LAS Superior)
214
+ sagittal_pos: Physical position along X axis (LAS Left)
215
+ coronal_pos: Physical position along Y axis (LAS Anterior)
216
+ """
217
+ if frame not in self._mpr_actors:
218
+ return
219
+
220
+ volume_actor = self._actors[frame]
221
+ image_data = volume_actor.GetMapper().GetInput()
222
+ bounds = image_data.GetBounds()
223
+
224
+ actors = self._mpr_actors[frame]
225
+
226
+ # Clamp positions to volume bounds
227
+ axial_pos = max(bounds[4], min(bounds[5], axial_pos)) # Z bounds
228
+ sagittal_pos = max(bounds[0], min(bounds[1], sagittal_pos)) # X bounds
229
+ coronal_pos = max(bounds[2], min(bounds[3], coronal_pos)) # Y bounds
230
+
231
+ # Get coordinate system transformations for each MPR view
232
+ transforms = self._get_mpr_coordinate_systems()
233
+
234
+ center = image_data.GetCenter()
235
+
236
+ # Step 1: Apply translation to determine slice origins in physical space
237
+ axial_origin = [center[0], center[1], axial_pos]
238
+ sagittal_origin = [sagittal_pos, center[1], center[2]]
239
+ coronal_origin = [center[0], coronal_pos, center[2]]
240
+
241
+ # Step 2: Apply cumulative rotation around the translated origins
242
+ if rotation_sequence and rotation_angles:
243
+ cumulative_rotation = np.eye(3)
244
+ for i, rotation in enumerate(rotation_sequence):
245
+ angle = rotation_angles.get(i, 0)
246
+ rotation_matrix = euler_angle_to_rotation_matrix(
247
+ EulerAxis(rotation["axis"]), angle
248
+ )
249
+ cumulative_rotation = cumulative_rotation @ rotation_matrix
250
+
251
+ # Apply rotation to base transforms
252
+ axial_transform = cumulative_rotation @ transforms["axial"]
253
+ sagittal_transform = cumulative_rotation @ transforms["sagittal"]
254
+ coronal_transform = cumulative_rotation @ transforms["coronal"]
255
+ else:
256
+ # Use base transforms without rotation
257
+ axial_transform = transforms["axial"]
258
+ sagittal_transform = transforms["sagittal"]
259
+ coronal_transform = transforms["coronal"]
260
+
261
+ # Update slices with translated origins and rotated transforms
262
+ axial_matrix = create_vtk_reslice_matrix(axial_transform, axial_origin)
263
+ actors["axial"]["reslice"].SetResliceAxes(axial_matrix)
264
+
265
+ sagittal_matrix = create_vtk_reslice_matrix(sagittal_transform, sagittal_origin)
266
+ actors["sagittal"]["reslice"].SetResliceAxes(sagittal_matrix)
267
+
268
+ coronal_matrix = create_vtk_reslice_matrix(coronal_transform, coronal_origin)
269
+ actors["coronal"]["reslice"].SetResliceAxes(coronal_matrix)
270
+
271
+ def update_mpr_window_level(self, frame: int, window: float, level: float):
272
+ """Update window/level properties for MPR actors."""
273
+ if frame not in self._mpr_actors:
274
+ return
275
+
276
+ actors = self._mpr_actors[frame]
277
+
278
+ for orientation in ["axial", "sagittal", "coronal"]:
279
+ if orientation in actors:
280
+ actor = actors[orientation]["actor"]
281
+ property_obj = actor.GetProperty()
282
+ property_obj.SetColorWindow(window)
283
+ property_obj.SetColorLevel(level)
@@ -0,0 +1,46 @@
1
+ # Third Party
2
+ import pydantic as pc
3
+ import vtk
4
+
5
+ # Internal
6
+ from .blend_transfer_functions import blend_transfer_functions
7
+ from .transfer_function_pair import TransferFunctionPairConfig
8
+ from .types import ScalarComponent
9
+
10
+
11
+ class VolumePropertyConfig(pc.BaseModel):
12
+ """Configuration for volume rendering properties and transfer functions."""
13
+
14
+ name: str = pc.Field(description="Display name of the preset")
15
+ description: str = pc.Field(description="Description of the preset")
16
+
17
+ # Lighting parameters
18
+ ambient: ScalarComponent
19
+ diffuse: ScalarComponent
20
+ specular: ScalarComponent
21
+
22
+ # Transfer functions
23
+ transfer_functions: list[TransferFunctionPairConfig] = pc.Field(
24
+ min_length=1, description="List of transfer function pairs to blend"
25
+ )
26
+
27
+ @property
28
+ def vtk_property(self) -> vtk.vtkVolumeProperty:
29
+ """Create a fully configured VTK volume property from this configuration."""
30
+ # Get VTK transfer functions from each pair config
31
+ tfs = [pair.vtk_functions for pair in self.transfer_functions]
32
+
33
+ # Blend all transfer functions into a single composite
34
+ blended_otf, blended_ctf = blend_transfer_functions(tfs)
35
+
36
+ # Create and configure the volume property
37
+ _vtk_property = vtk.vtkVolumeProperty()
38
+ _vtk_property.SetScalarOpacity(blended_otf)
39
+ _vtk_property.SetColor(blended_ctf)
40
+ _vtk_property.ShadeOn()
41
+ _vtk_property.SetInterpolationTypeToLinear()
42
+ _vtk_property.SetAmbient(self.ambient)
43
+ _vtk_property.SetDiffuse(self.diffuse)
44
+ _vtk_property.SetSpecular(self.specular)
45
+
46
+ return _vtk_property
@@ -0,0 +1,53 @@
1
+ # System
2
+ import pathlib as pl
3
+
4
+ # Third Party
5
+ import pydantic as pc
6
+ import tomlkit as tk
7
+
8
+ # Internal
9
+ from .volume_property import VolumePropertyConfig
10
+
11
+
12
+ def load_volume_property_preset(preset_name: str) -> VolumePropertyConfig:
13
+ """Load a specific volume property preset from its individual file."""
14
+ assets_dir = pl.Path(__file__).parent / "assets"
15
+ preset_file = assets_dir / f"{preset_name}.toml"
16
+
17
+ if not preset_file.exists():
18
+ available = list(list_volume_property_presets().keys())
19
+ raise KeyError(
20
+ f"Volume property preset '{preset_name}' not found. "
21
+ f"Available presets: {available}"
22
+ )
23
+
24
+ try:
25
+ with preset_file.open("rt", encoding="utf-8") as fp:
26
+ raw_data = tk.load(fp)
27
+ return VolumePropertyConfig.model_validate(raw_data)
28
+ except (pc.ValidationError, Exception) as e:
29
+ raise ValueError(f"Invalid preset file '{preset_name}.toml': {e}") from e
30
+
31
+
32
+ def list_volume_property_presets() -> dict[str, str]:
33
+ """
34
+ List all available volume property presets.
35
+
36
+ Returns:
37
+ Dictionary mapping preset names to descriptions
38
+ """
39
+ assets_dir = pl.Path(__file__).parent / "assets"
40
+ preset_files = assets_dir.glob("*.toml")
41
+
42
+ presets = {}
43
+ for preset_file in preset_files:
44
+ preset_name = preset_file.stem
45
+ try:
46
+ with preset_file.open("rt", encoding="utf-8") as fp:
47
+ preset_data = tk.load(fp)
48
+ presets[preset_name] = preset_data["description"]
49
+ except (KeyError, OSError):
50
+ # Skip files that don't have the expected structure
51
+ continue
52
+
53
+ return presets
cardio/window_level.py ADDED
@@ -0,0 +1,35 @@
1
+ # System
2
+ import functools as ft
3
+
4
+ # Third Party
5
+ import pydantic as pc
6
+
7
+
8
+ @pc.dataclasses.dataclass(config=dict(frozen=True))
9
+ class WindowLevel:
10
+ name: str
11
+ window: float
12
+ level: float
13
+
14
+ @pc.computed_field
15
+ @ft.cached_property
16
+ def lower(self) -> float:
17
+ return self.level - self.window / 2
18
+
19
+ @pc.computed_field
20
+ @ft.cached_property
21
+ def upper(self) -> float:
22
+ return self.level + self.window / 2
23
+
24
+
25
+ presets = {
26
+ 1: WindowLevel("Abdomen", 400, 40),
27
+ 2: WindowLevel("Lung", 1500, -700),
28
+ 3: WindowLevel("Liver", 100, 110),
29
+ 4: WindowLevel("Bone", 1500, 500),
30
+ 5: WindowLevel("Brain", 85, 42),
31
+ 6: WindowLevel("Stroke", 36, 28),
32
+ 7: WindowLevel("Vascular", 800, 200),
33
+ 8: WindowLevel("Subdural", 160, 60),
34
+ 9: WindowLevel("Normalized", 0, 1),
35
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cardio
3
- Version: 2025.8.1
3
+ Version: 2025.10.0
4
4
  Summary: A simple web-based viewer for 3D and 4D ('cine') medical imaging data.
5
5
  Keywords: Medical,Imaging,3D,4D,Visualization
6
6
  Author: Davis Marc Vigneault
@@ -34,17 +34,19 @@ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
34
34
  Classifier: Topic :: Scientific/Engineering :: Visualization
35
35
  Requires-Dist: trame-vuetify>=3.0.2
36
36
  Requires-Dist: trame-vtk>=2.9.1
37
- Requires-Dist: trame>=3.11.0
38
37
  Requires-Dist: itk>=5.4.4.post1
39
38
  Requires-Dist: vtk>=9.5.0
40
39
  Requires-Dist: tomlkit>=0.13.3
41
40
  Requires-Dist: numpy>=2.2.6
42
41
  Requires-Dist: pydantic>=2.11.7
43
42
  Requires-Dist: pydantic-settings>=2.0.0
43
+ Requires-Dist: trame>=3.12.0
44
+ Requires-Dist: trame-client>=3.10.1
44
45
  Requires-Dist: ruff>=0.12.10 ; extra == 'dev'
45
46
  Requires-Dist: isort>=6.0.1 ; extra == 'dev'
46
47
  Requires-Dist: bumpver>=2025.1131 ; extra == 'dev'
47
48
  Requires-Dist: pytest>=8.4.1 ; extra == 'dev'
49
+ Requires-Dist: coverage>=7.10.4 ; extra == 'dev'
48
50
  Maintainer: Davis Marc Vigneault
49
51
  Maintainer-email: Davis Marc Vigneault <davis.vigneault@gmail.com>
50
52
  Requires-Python: >=3.10
@@ -74,7 +76,7 @@ $ uv init
74
76
  $ uv add cardio
75
77
  $ . ./.venv/bin/activate
76
78
  (project) cardio --version
77
- cardio 2025.8.1
79
+ cardio 2025.10.0
78
80
  ```
79
81
 
80
82
  ### Developing
@@ -0,0 +1,29 @@
1
+ cardio/__init__.py,sha256=5lINeD9qQQm_u9eJfPIzdEMfYW62UZ2asOmQIS4PcFk,602
2
+ cardio/app.py,sha256=TEzgA03EAgI7HSCHhYwYb8tsnAptsrpcysV5CytfAq4,1379
3
+ cardio/assets/bone.toml,sha256=vv8uVYSHIoKuHkNCoBOkGe2_qoEbXMvQO6ypm3mMOtA,675
4
+ cardio/assets/vascular_closed.toml,sha256=XtaZS_Zd6NSAtY3ZlUfiog3T86u9Ii0oSutU2wBQy78,1267
5
+ cardio/assets/vascular_open.toml,sha256=1M3sV1IGt3zh_3vviysKEk9quKfjF9xUBcIq3kxVHFM,879
6
+ cardio/assets/xray.toml,sha256=siPem0OZ2OkWH0e5pizftpItJKGJgxKJ_S2K0316ubQ,693
7
+ cardio/blend_transfer_functions.py,sha256=s5U4hO810oE434wIkPmAP2mrAfqFb4xxxi3hHf_k8og,2982
8
+ cardio/color_transfer_function.py,sha256=KV4j11AXYeaYGeJWBc9I-WZf7Shrm5xjQVq-0bq9Qc8,817
9
+ cardio/logic.py,sha256=a73JJ2b8t-TYggYMGI1IGpn0Q6AFD8I1JNz9fn6QeDk,37557
10
+ cardio/mesh.py,sha256=xL4hadrVF3GVtMFxpq79DKApbmstZEI_HEwTAqc4ZMI,9391
11
+ cardio/object.py,sha256=fvLSZtWf1zDbYMh-AMpgVLwD-9S1LCzuRt7HmkXxb3A,6215
12
+ cardio/orientation.py,sha256=J3bqZbv8vfl4loGl7ksmuyqWb3zFAz-TVSIahKcg0pc,6145
13
+ cardio/piecewise_function.py,sha256=bwtwgrAMGkgu1krnvsOF9gRMaZb6smsS9jLrgBecSbo,789
14
+ cardio/property_config.py,sha256=XJYcKeRcq8s9W9jqxzVer75r5jBLuvebv780FYdPV8U,1723
15
+ cardio/scene.py,sha256=9jskdEARyJjk7QBIcMt5X2HsnikTAJdoRoLSI0LxcJE,14301
16
+ cardio/screenshot.py,sha256=l8bLgxnU5O0FlnmsyVAzKwM9Y0401IzcdnDP0WqFSTY,640
17
+ cardio/segmentation.py,sha256=QqeG2C3BVbO9fUJDSWf0mZXzgfrTx4LHWwaz4vtv8ZM,6677
18
+ cardio/transfer_function_pair.py,sha256=90PQXByCL6mMaODo7Yfd-lmdFtCKhcBZbNaiH-PTds0,805
19
+ cardio/types.py,sha256=DYDgA5QmYdU3QQrEgZMouEbMEIf40DJCeXo4V7cDXtg,356
20
+ cardio/ui.py,sha256=wsRujwiNtMeq65R4bx-myxSPDhJT0RskwvBWlGenmHk,42901
21
+ cardio/utils.py,sha256=tFUQ4FxfidTH6GjEIKQwguqhO9T_wJ2Vk0IhbEfxRGA,1616
22
+ cardio/volume.py,sha256=TxUfOvEoQw-kbEAQLSTfVOU3DhJjnjB8iy4qygCmVXA,10378
23
+ cardio/volume_property.py,sha256=6T2r67SSIDl8F6ZlQvgMCZESLxuXVVAUjOC50lgQEmk,1644
24
+ cardio/volume_property_presets.py,sha256=U2a2MnyCjryzOLEADs3OLSMMmAUnXq82mYK7OVXbQV0,1659
25
+ cardio/window_level.py,sha256=gjk39Iv6jMJ52y6jydOjxBZBsI1nZQMs2CdWWTshQoE,807
26
+ cardio-2025.10.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
27
+ cardio-2025.10.0.dist-info/entry_points.txt,sha256=xRd1otqKtW9xmidJ4CFRX1V9KWmsBbtxrgMtDColq3w,40
28
+ cardio-2025.10.0.dist-info/METADATA,sha256=VIqGgz8ueoxxEahbi1hshAnK90zWx_IUlyihRJHdwH8,3540
29
+ cardio-2025.10.0.dist-info/RECORD,,