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/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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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 .
|
|
10
|
-
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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,,
|