cardio 2023.1.2__py3-none-any.whl → 2025.8.1__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.1.dist-info/METADATA +102 -0
- cardio-2025.8.1.dist-info/RECORD +22 -0
- cardio-2025.8.1.dist-info/WHEEL +4 -0
- {cardio-2023.1.2.dist-info → cardio-2025.8.1.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
@@ -0,0 +1,272 @@
|
|
1
|
+
# System
|
2
|
+
import pathlib as pl
|
3
|
+
|
4
|
+
import numpy as np
|
5
|
+
import pydantic as pc
|
6
|
+
|
7
|
+
# Third Party
|
8
|
+
import tomlkit as tk
|
9
|
+
import vtk
|
10
|
+
|
11
|
+
# Internal
|
12
|
+
from .types import RGBColor, ScalarComponent
|
13
|
+
|
14
|
+
|
15
|
+
def blend_transfer_functions(tfs, scalar_range=(-2000, 2000), num_samples=512):
|
16
|
+
"""
|
17
|
+
Blend multiple transfer functions using volume rendering emission-absorption model.
|
18
|
+
|
19
|
+
Based on the volume rendering equation from:
|
20
|
+
- Levoy, M. "Display of Surfaces from Volume Data" IEEE Computer Graphics and Applications, 1988
|
21
|
+
- Kajiya, J.T. & Von Herzen, B.P. "Ray tracing volume densities" ACM SIGGRAPH Computer Graphics, 1984
|
22
|
+
- Engel, K. et al. "Real-time Volume Graphics" A K Peters, 2006, Chapter 2
|
23
|
+
|
24
|
+
The volume rendering integral: I = ∫ C(s) * μ(s) * T(s) ds
|
25
|
+
where C(s) = emission color, μ(s) = opacity, T(s) = transmission
|
26
|
+
|
27
|
+
For discrete transfer functions, this becomes:
|
28
|
+
- Total emission = Σ(color_i * opacity_i)
|
29
|
+
- Total absorption = Σ(opacity_i)
|
30
|
+
- Final color = total_emission / total_absorption
|
31
|
+
"""
|
32
|
+
if len(tfs) == 1:
|
33
|
+
return tfs[0]
|
34
|
+
|
35
|
+
sample_points = np.linspace(
|
36
|
+
start=scalar_range[0],
|
37
|
+
stop=scalar_range[1],
|
38
|
+
num=num_samples,
|
39
|
+
)
|
40
|
+
|
41
|
+
# Initialize arrays to store blended values
|
42
|
+
blended_opacity = []
|
43
|
+
blended_color = []
|
44
|
+
|
45
|
+
for scalar_val in sample_points:
|
46
|
+
# Accumulate emission and absorption for volume rendering
|
47
|
+
total_emission = [0.0, 0.0, 0.0]
|
48
|
+
total_absorption = 0.0
|
49
|
+
|
50
|
+
for otf, ctf in tfs:
|
51
|
+
# Get opacity and color for this scalar value
|
52
|
+
layer_opacity = otf.GetValue(scalar_val)
|
53
|
+
layer_color = [0.0, 0.0, 0.0]
|
54
|
+
ctf.GetColor(scalar_val, layer_color)
|
55
|
+
|
56
|
+
# Volume rendering accumulation:
|
57
|
+
# Emission = color * opacity (additive)
|
58
|
+
# Absorption = opacity (multiplicative through transmission)
|
59
|
+
for i in range(3):
|
60
|
+
total_emission[i] += layer_color[i] * layer_opacity
|
61
|
+
|
62
|
+
total_absorption += layer_opacity
|
63
|
+
|
64
|
+
# Clamp values to reasonable ranges
|
65
|
+
total_absorption = min(total_absorption, 1.0)
|
66
|
+
for i in range(3):
|
67
|
+
total_emission[i] = min(total_emission[i], 1.0)
|
68
|
+
|
69
|
+
# For the final color, normalize emission by absorption if absorption > 0
|
70
|
+
if total_absorption > 0.001: # Avoid division by zero
|
71
|
+
final_color = [total_emission[i] / total_absorption for i in range(3)]
|
72
|
+
else:
|
73
|
+
final_color = [0.0, 0.0, 0.0]
|
74
|
+
|
75
|
+
# Clamp final colors
|
76
|
+
final_color = [min(c, 1.0) for c in final_color]
|
77
|
+
|
78
|
+
blended_opacity.append(total_absorption)
|
79
|
+
blended_color.append(final_color)
|
80
|
+
|
81
|
+
# Create new VTK transfer functions with blended values
|
82
|
+
blended_otf = vtk.vtkPiecewiseFunction()
|
83
|
+
blended_ctf = vtk.vtkColorTransferFunction()
|
84
|
+
|
85
|
+
for i, scalar_val in enumerate(sample_points):
|
86
|
+
blended_otf.AddPoint(scalar_val, blended_opacity[i])
|
87
|
+
blended_ctf.AddRGBPoint(
|
88
|
+
scalar_val, blended_color[i][0], blended_color[i][1], blended_color[i][2]
|
89
|
+
)
|
90
|
+
|
91
|
+
return blended_otf, blended_ctf
|
92
|
+
|
93
|
+
|
94
|
+
class PiecewiseFunctionPoint(pc.BaseModel):
|
95
|
+
"""A single point in a piecewise function."""
|
96
|
+
|
97
|
+
x: float = pc.Field(description="Scalar value")
|
98
|
+
y: ScalarComponent
|
99
|
+
|
100
|
+
|
101
|
+
class ColorTransferFunctionPoint(pc.BaseModel):
|
102
|
+
"""A single point in a color transfer function."""
|
103
|
+
|
104
|
+
x: float = pc.Field(description="Scalar value")
|
105
|
+
color: RGBColor
|
106
|
+
|
107
|
+
|
108
|
+
class PiecewiseFunctionConfig(pc.BaseModel):
|
109
|
+
"""Configuration for a VTK piecewise function (opacity)."""
|
110
|
+
|
111
|
+
points: list[PiecewiseFunctionPoint] = pc.Field(
|
112
|
+
min_length=1, description="Points defining the piecewise function"
|
113
|
+
)
|
114
|
+
|
115
|
+
@property
|
116
|
+
def vtk_function(self) -> vtk.vtkPiecewiseFunction:
|
117
|
+
"""Create VTK piecewise function from this configuration."""
|
118
|
+
otf = vtk.vtkPiecewiseFunction()
|
119
|
+
for point in self.points:
|
120
|
+
otf.AddPoint(point.x, point.y)
|
121
|
+
return otf
|
122
|
+
|
123
|
+
|
124
|
+
class ColorTransferFunctionConfig(pc.BaseModel):
|
125
|
+
"""Configuration for a VTK color transfer function."""
|
126
|
+
|
127
|
+
points: list[ColorTransferFunctionPoint] = pc.Field(
|
128
|
+
min_length=1, description="Points defining the color transfer function"
|
129
|
+
)
|
130
|
+
|
131
|
+
@property
|
132
|
+
def vtk_function(self) -> vtk.vtkColorTransferFunction:
|
133
|
+
"""Create VTK color transfer function from this configuration."""
|
134
|
+
ctf = vtk.vtkColorTransferFunction()
|
135
|
+
for point in self.points:
|
136
|
+
ctf.AddRGBPoint(point.x, *point.color)
|
137
|
+
return ctf
|
138
|
+
|
139
|
+
|
140
|
+
class TransferFunctionConfig(pc.BaseModel):
|
141
|
+
"""Configuration for a single transfer function (legacy format for compatibility)."""
|
142
|
+
|
143
|
+
window: float = pc.Field(gt=0, description="Window width for transfer function")
|
144
|
+
level: float = pc.Field(description="Window level for transfer function")
|
145
|
+
locolor: RGBColor
|
146
|
+
hicolor: RGBColor
|
147
|
+
opacity: ScalarComponent
|
148
|
+
|
149
|
+
@property
|
150
|
+
def vtk_functions(
|
151
|
+
self,
|
152
|
+
) -> tuple[vtk.vtkPiecewiseFunction, vtk.vtkColorTransferFunction]:
|
153
|
+
"""Create VTK transfer functions from this configuration."""
|
154
|
+
# Create opacity transfer function
|
155
|
+
otf = vtk.vtkPiecewiseFunction()
|
156
|
+
otf.AddPoint(self.level - self.window * 0.50, 0.0)
|
157
|
+
otf.AddPoint(self.level + self.window * 0.14, self.opacity)
|
158
|
+
otf.AddPoint(self.level + self.window * 0.50, 0.0)
|
159
|
+
|
160
|
+
# Create color transfer function
|
161
|
+
ctf = vtk.vtkColorTransferFunction()
|
162
|
+
ctf.AddRGBPoint(
|
163
|
+
self.level - self.window / 2,
|
164
|
+
*self.locolor,
|
165
|
+
)
|
166
|
+
ctf.AddRGBPoint(
|
167
|
+
self.level + self.window / 2,
|
168
|
+
*self.hicolor,
|
169
|
+
)
|
170
|
+
|
171
|
+
return otf, ctf
|
172
|
+
|
173
|
+
|
174
|
+
class TransferFunctionPairConfig(pc.BaseModel):
|
175
|
+
"""Configuration for a pair of opacity and color transfer functions."""
|
176
|
+
|
177
|
+
opacity: PiecewiseFunctionConfig = pc.Field(
|
178
|
+
description="Opacity transfer function configuration"
|
179
|
+
)
|
180
|
+
color: ColorTransferFunctionConfig = pc.Field(
|
181
|
+
description="Color transfer function configuration"
|
182
|
+
)
|
183
|
+
|
184
|
+
@property
|
185
|
+
def vtk_functions(
|
186
|
+
self,
|
187
|
+
) -> tuple[vtk.vtkPiecewiseFunction, vtk.vtkColorTransferFunction]:
|
188
|
+
"""Create VTK transfer functions from this pair configuration."""
|
189
|
+
return self.opacity.vtk_function, self.color.vtk_function
|
190
|
+
|
191
|
+
|
192
|
+
class VolumePropertyConfig(pc.BaseModel):
|
193
|
+
"""Configuration for volume rendering properties and transfer functions."""
|
194
|
+
|
195
|
+
name: str = pc.Field(description="Display name of the preset")
|
196
|
+
description: str = pc.Field(description="Description of the preset")
|
197
|
+
|
198
|
+
# Lighting parameters
|
199
|
+
ambient: ScalarComponent
|
200
|
+
diffuse: ScalarComponent
|
201
|
+
specular: ScalarComponent
|
202
|
+
|
203
|
+
# Transfer functions
|
204
|
+
transfer_functions: list[TransferFunctionPairConfig] = pc.Field(
|
205
|
+
min_length=1, description="List of transfer function pairs to blend"
|
206
|
+
)
|
207
|
+
|
208
|
+
@property
|
209
|
+
def vtk_property(self) -> vtk.vtkVolumeProperty:
|
210
|
+
"""Create a fully configured VTK volume property from this configuration."""
|
211
|
+
# Get VTK transfer functions from each pair config
|
212
|
+
tfs = [pair.vtk_functions for pair in self.transfer_functions]
|
213
|
+
|
214
|
+
# Blend all transfer functions into a single composite
|
215
|
+
blended_otf, blended_ctf = blend_transfer_functions(tfs)
|
216
|
+
|
217
|
+
# Create and configure the volume property
|
218
|
+
_vtk_property = vtk.vtkVolumeProperty()
|
219
|
+
_vtk_property.SetScalarOpacity(blended_otf)
|
220
|
+
_vtk_property.SetColor(blended_ctf)
|
221
|
+
_vtk_property.ShadeOn()
|
222
|
+
_vtk_property.SetInterpolationTypeToLinear()
|
223
|
+
_vtk_property.SetAmbient(self.ambient)
|
224
|
+
_vtk_property.SetDiffuse(self.diffuse)
|
225
|
+
_vtk_property.SetSpecular(self.specular)
|
226
|
+
|
227
|
+
return _vtk_property
|
228
|
+
|
229
|
+
|
230
|
+
def load_preset(preset_name: str) -> VolumePropertyConfig:
|
231
|
+
"""Load a specific preset from its individual file."""
|
232
|
+
assets_dir = pl.Path(__file__).parent / "assets"
|
233
|
+
preset_file = assets_dir / f"{preset_name}.toml"
|
234
|
+
|
235
|
+
if not preset_file.exists():
|
236
|
+
available = list(list_available_presets().keys())
|
237
|
+
raise KeyError(
|
238
|
+
f"Transfer function preset '{preset_name}' not found. "
|
239
|
+
f"Available presets: {available}"
|
240
|
+
)
|
241
|
+
|
242
|
+
with preset_file.open("rt", encoding="utf-8") as fp:
|
243
|
+
raw_data = tk.load(fp)
|
244
|
+
|
245
|
+
try:
|
246
|
+
return VolumePropertyConfig.model_validate(raw_data)
|
247
|
+
except pc.ValidationError as e:
|
248
|
+
raise ValueError(f"Invalid preset file '{preset_name}.toml': {e}") from e
|
249
|
+
|
250
|
+
|
251
|
+
def list_available_presets() -> dict[str, str]:
|
252
|
+
"""
|
253
|
+
List all available transfer function presets.
|
254
|
+
|
255
|
+
Returns:
|
256
|
+
Dictionary mapping preset names to descriptions
|
257
|
+
"""
|
258
|
+
assets_dir = pl.Path(__file__).parent / "assets"
|
259
|
+
preset_files = assets_dir.glob("*.toml")
|
260
|
+
|
261
|
+
presets = {}
|
262
|
+
for preset_file in preset_files:
|
263
|
+
preset_name = preset_file.stem
|
264
|
+
try:
|
265
|
+
with preset_file.open("rt", encoding="utf-8") as fp:
|
266
|
+
preset_data = tk.load(fp)
|
267
|
+
presets[preset_name] = preset_data["description"]
|
268
|
+
except (KeyError, OSError):
|
269
|
+
# Skip files that don't have the expected structure
|
270
|
+
continue
|
271
|
+
|
272
|
+
return presets
|
cardio/types.py
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# System
|
2
|
+
import typing
|
3
|
+
|
4
|
+
# Third Party
|
5
|
+
import pydantic as pc
|
6
|
+
|
7
|
+
ScalarComponent: typing.TypeAlias = typing.Annotated[
|
8
|
+
float, pc.Field(ge=0.0, le=1.0, validate_default=True)
|
9
|
+
]
|
10
|
+
|
11
|
+
RGBColor: typing.TypeAlias = typing.Annotated[
|
12
|
+
tuple[
|
13
|
+
ScalarComponent,
|
14
|
+
ScalarComponent,
|
15
|
+
ScalarComponent,
|
16
|
+
],
|
17
|
+
pc.Field(validate_default=True),
|
18
|
+
]
|