cardio 2026.1.3__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ Apache Software License 2.0
2
+
3
+ Copyright (c) 2023, Davis Marc Vigneault
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.3
2
+ Name: cardio
3
+ Version: 2026.1.3
4
+ Summary: A simple web-based viewer for 3D and 4D ('cine') medical imaging data.
5
+ Keywords: Medical,Imaging,3D,4D,Visualization
6
+ Author: Davis Marc Vigneault
7
+ Author-email: Davis Marc Vigneault <davis.vigneault@gmail.com>
8
+ License: Apache Software License 2.0
9
+
10
+ Copyright (c) 2023, Davis Marc Vigneault
11
+
12
+ Licensed under the Apache License, Version 2.0 (the "License");
13
+ you may not use this file except in compliance with the License.
14
+ You may obtain a copy of the License at
15
+
16
+ http://www.apache.org/licenses/LICENSE-2.0
17
+
18
+ Unless required by applicable law or agreed to in writing, software
19
+ distributed under the License is distributed on an "AS IS" BASIS,
20
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ See the License for the specific language governing permissions and
22
+ limitations under the License.
23
+ Classifier: Development Status :: 3 - Alpha
24
+ Classifier: Environment :: Web Environment
25
+ Classifier: Intended Audience :: Healthcare Industry
26
+ Classifier: Intended Audience :: Science/Research
27
+ Classifier: License :: OSI Approved :: Apache Software License
28
+ Classifier: Natural Language :: English
29
+ Classifier: Operating System :: OS Independent
30
+ Classifier: Programming Language :: Python :: 3 :: Only
31
+ Classifier: Programming Language :: JavaScript
32
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
33
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
34
+ Classifier: Topic :: Scientific/Engineering :: Visualization
35
+ Requires-Dist: trame-vuetify>=3.0.2
36
+ Requires-Dist: trame-vtk>=2.9.1
37
+ Requires-Dist: itk>=5.4.4.post1
38
+ Requires-Dist: vtk>=9.5.0
39
+ Requires-Dist: tomlkit>=0.13.3
40
+ Requires-Dist: numpy>=2.2.6
41
+ Requires-Dist: pydantic>=2.11.7
42
+ Requires-Dist: pydantic-settings>=2.0.0
43
+ Requires-Dist: trame>=3.12.0
44
+ Requires-Dist: trame-client>=3.10.1
45
+ Requires-Dist: ruff>=0.12.10 ; extra == 'dev'
46
+ Requires-Dist: isort>=6.0.1 ; extra == 'dev'
47
+ Requires-Dist: bumpver>=2025.1131 ; extra == 'dev'
48
+ Requires-Dist: pytest>=8.4.1 ; extra == 'dev'
49
+ Requires-Dist: coverage>=7.10.4 ; extra == 'dev'
50
+ Maintainer: Davis Marc Vigneault
51
+ Maintainer-email: Davis Marc Vigneault <davis.vigneault@gmail.com>
52
+ Requires-Python: >=3.10
53
+ Project-URL: repository, https://github.com/sudomakeinstall/cardio
54
+ Provides-Extra: dev
55
+ Description-Content-Type: text/markdown
56
+
57
+ # cardio
58
+
59
+ `cardio` is a simple web-based viewer for 3D and 4D ('cine') medical imaging data,
60
+ built primarily on [trame](https://github.com/kitware/trame),
61
+ [vtk](https://github.com/kitware/vtk), and
62
+ [itk](https://github.com/insightsoftwareconsortium/itk). `cardio` can render sequences
63
+ of mesh files (e.g., `*.obj` files), segmentation files (e.g., `*.nii.gz` files with
64
+ discrete labels) and volume renderings of grayscale images (e.g., `*.nii.gz` files with
65
+ continuous values). `cardio` is launched from the commandline and may be configured via
66
+ commandline arguments, a static TOML configuration file, or a combination of the two.
67
+
68
+ ## Quickstart
69
+
70
+ ### Installation
71
+
72
+ ```bash
73
+ $ cd /path/to/your/project
74
+ $ uv init
75
+ $ uv add cardio
76
+ $ . ./.venv/bin/activate
77
+ (project) cardio --version
78
+ cardio 2026.1.3
79
+ ```
80
+
81
+ ### Developing
82
+
83
+ Ensuring you have all required dependencies:
84
+
85
+ ```bash
86
+ $ uv sync --all-extras
87
+ ```
88
+
89
+ Pre-commit checklist:
90
+
91
+ ```bash
92
+ $ isort .
93
+ $ ruff format
94
+ $ pytest -v
95
+ ```
96
+
97
+ Uploading:
98
+
99
+ ```bash
100
+ $ bumpver update
101
+ $ uv build --no-sources
102
+ $ uv publish --token <pypi_api_key>
103
+ ```
@@ -0,0 +1,47 @@
1
+ # cardio
2
+
3
+ `cardio` is a simple web-based viewer for 3D and 4D ('cine') medical imaging data,
4
+ built primarily on [trame](https://github.com/kitware/trame),
5
+ [vtk](https://github.com/kitware/vtk), and
6
+ [itk](https://github.com/insightsoftwareconsortium/itk). `cardio` can render sequences
7
+ of mesh files (e.g., `*.obj` files), segmentation files (e.g., `*.nii.gz` files with
8
+ discrete labels) and volume renderings of grayscale images (e.g., `*.nii.gz` files with
9
+ continuous values). `cardio` is launched from the commandline and may be configured via
10
+ commandline arguments, a static TOML configuration file, or a combination of the two.
11
+
12
+ ## Quickstart
13
+
14
+ ### Installation
15
+
16
+ ```bash
17
+ $ cd /path/to/your/project
18
+ $ uv init
19
+ $ uv add cardio
20
+ $ . ./.venv/bin/activate
21
+ (project) cardio --version
22
+ cardio 2026.1.3
23
+ ```
24
+
25
+ ### Developing
26
+
27
+ Ensuring you have all required dependencies:
28
+
29
+ ```bash
30
+ $ uv sync --all-extras
31
+ ```
32
+
33
+ Pre-commit checklist:
34
+
35
+ ```bash
36
+ $ isort .
37
+ $ ruff format
38
+ $ pytest -v
39
+ ```
40
+
41
+ Uploading:
42
+
43
+ ```bash
44
+ $ bumpver update
45
+ $ uv build --no-sources
46
+ $ uv publish --token <pypi_api_key>
47
+ ```
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.8.13,<0.9.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "cardio"
7
+ version = "2026.1.3"
8
+ authors = [
9
+ {name = "Davis Marc Vigneault", email = "davis.vigneault@gmail.com"}
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Web Environment",
14
+ "Intended Audience :: Healthcare Industry",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Natural Language :: English",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: JavaScript",
21
+ "Topic :: Scientific/Engineering :: Image Processing",
22
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
23
+ "Topic :: Scientific/Engineering :: Visualization",
24
+ ]
25
+ dependencies = [
26
+ "trame-vuetify>=3.0.2",
27
+ "trame-vtk>=2.9.1",
28
+ "itk>=5.4.4.post1",
29
+ "vtk>=9.5.0",
30
+ "tomlkit>=0.13.3",
31
+ "numpy>=2.2.6",
32
+ "pydantic>=2.11.7",
33
+ "pydantic-settings>=2.0.0",
34
+ "trame>=3.12.0",
35
+ "trame-client>=3.10.1",
36
+ ]
37
+ description = "A simple web-based viewer for 3D and 4D ('cine') medical imaging data."
38
+ keywords = ["Medical", "Imaging", "3D", "4D", "Visualization"]
39
+ license = {file = "LICENSE"}
40
+ maintainers = [
41
+ {name = "Davis Marc Vigneault", email = "davis.vigneault@gmail.com"}
42
+ ]
43
+ readme = "README.md"
44
+ requires-python = ">=3.10"
45
+
46
+ [project.optional-dependencies]
47
+ dev = [
48
+ "ruff>=0.12.10",
49
+ "isort>=6.0.1",
50
+ "bumpver>=2025.1131",
51
+ "pytest>=8.4.1",
52
+ "coverage>=7.10.4",
53
+ ]
54
+
55
+ [project.gui-scripts]
56
+ cardio = "cardio.app:main"
57
+
58
+ [project.urls]
59
+ repository = "https://github.com/sudomakeinstall/cardio"
60
+
61
+ [tool.isort]
62
+ profile = "black"
63
+
64
+ [tool.bumpver]
65
+ current_version = "2026.1.3"
66
+ version_pattern = "YYYY.MM.INC0"
67
+ commit_message = "ENH: Bump version from {old_version} => {new_version}"
68
+ commit = true
69
+ tag = true
70
+ push = false
71
+
72
+ [tool.bumpver.file_patterns]
73
+ "pyproject.toml" = [
74
+ 'current_version = "{version}"',
75
+ 'version = "{version}"',
76
+ ]
77
+ "src/cardio/__init__.py" = [
78
+ '__version__ = "{version}"',
79
+ ]
80
+ "README.md" = [
81
+ 'cardio {version}',
82
+ ]
@@ -0,0 +1,29 @@
1
+ from . import window_level
2
+ from .logic import Logic
3
+ from .mesh import Mesh
4
+ from .object import Object
5
+ from .scene import Scene
6
+ from .screenshot import Screenshot
7
+ from .segmentation import Segmentation
8
+ from .ui import UI
9
+ from .volume import Volume
10
+ from .volume_property_presets import (
11
+ list_volume_property_presets,
12
+ load_volume_property_preset,
13
+ )
14
+
15
+ __all__ = [
16
+ "Object",
17
+ "Mesh",
18
+ "Volume",
19
+ "Segmentation",
20
+ "Scene",
21
+ "Screenshot",
22
+ "UI",
23
+ "Logic",
24
+ "load_volume_property_preset",
25
+ "list_volume_property_presets",
26
+ "window_level",
27
+ ]
28
+
29
+ __version__ = "2026.1.3"
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python
2
+
3
+ import pydantic_settings as ps
4
+ import trame as tm
5
+ import trame.decorators
6
+
7
+ from . import __version__
8
+ from .logic import Logic
9
+ from .scene import Scene
10
+ from .ui import UI
11
+
12
+
13
+ @tm.decorators.TrameApp()
14
+ class CardioApp:
15
+ def __init__(self, server=None):
16
+ self.server = tm.app.get_server(server, client_type="vue3")
17
+
18
+ self.server.cli.add_argument(
19
+ "--config", help="TOML configuration file.", dest="cfg_file", required=False
20
+ )
21
+
22
+ self.server.cli.add_argument(
23
+ "--version", action="version", version=f"cardio {__version__}"
24
+ )
25
+
26
+ cli_settings = ps.CliSettingsSource(
27
+ Scene, root_parser=self.server.cli, cli_parse_args=True
28
+ )
29
+
30
+ args, unknown = self.server.cli.parse_known_args()
31
+ config_file = getattr(args, "cfg_file", None)
32
+
33
+ Scene._cli_source = cli_settings
34
+ Scene._config_file = config_file
35
+
36
+ try:
37
+ scene = Scene()
38
+ finally:
39
+ if hasattr(Scene, "_cli_source"):
40
+ delattr(Scene, "_cli_source")
41
+ if hasattr(Scene, "_config_file"):
42
+ delattr(Scene, "_config_file")
43
+
44
+ Logic(self.server, scene)
45
+ UI(self.server, scene)
46
+
47
+
48
+ def main():
49
+ app = CardioApp()
50
+ app.server.start(open_browser=False)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,42 @@
1
+ name = "Bone"
2
+ description = "Bone"
3
+
4
+ ambient = 1.0
5
+ diffuse = 1.0
6
+ specular = 0.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = -1024.0
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = 54.0
17
+ y = 0.0
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 536.0
21
+ y = 0.1
22
+
23
+ [[transfer_functions.opacity.points]]
24
+ x = 3071.0
25
+ y = 0.1
26
+
27
+ [transfer_functions.color]
28
+ [[transfer_functions.color.points]]
29
+ x = -1024.0
30
+ color = [0.0, 0.0, 0.0]
31
+
32
+ [[transfer_functions.color.points]]
33
+ x = 54.0
34
+ color = [0.0, 0.0, 0.0]
35
+
36
+ [[transfer_functions.color.points]]
37
+ x = 536.0
38
+ color = [1.0, 1.0, 1.0]
39
+
40
+ [[transfer_functions.color.points]]
41
+ x = 3071.0
42
+ color = [1.0, 1.0, 1.0]
@@ -0,0 +1,78 @@
1
+ name = "Vascular_Closed"
2
+ description = "Vascular (Closed)"
3
+
4
+ ambient = 0.7
5
+ diffuse = 0.4
6
+ specular = 1.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = 150
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = 650
17
+ y = 1.0
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 3071
21
+ y = 1.0
22
+
23
+ [transfer_functions.color]
24
+ [[transfer_functions.color.points]]
25
+ x = 150
26
+ color = [1.0, 1.0, 1.0]
27
+
28
+ [[transfer_functions.color.points]]
29
+ x = 3071
30
+ color = [1.0, 1.0, 1.0]
31
+
32
+ [[transfer_functions]]
33
+
34
+ [transfer_functions.opacity]
35
+ [[transfer_functions.opacity.points]]
36
+ x = -96.5
37
+ y = 0.0
38
+
39
+ [[transfer_functions.opacity.points]]
40
+ x = 269.0
41
+ y = 0.05
42
+
43
+ [[transfer_functions.opacity.points]]
44
+ x = 474.5
45
+ y = 0.0
46
+
47
+ [transfer_functions.color]
48
+ [[transfer_functions.color.points]]
49
+ x = -96.5
50
+ color = [1.0, 1.0, 1.0]
51
+
52
+ [[transfer_functions.color.points]]
53
+ x = 474.5
54
+ color = [1.0, 1.0, 1.0]
55
+
56
+ [[transfer_functions]]
57
+
58
+ [transfer_functions.opacity]
59
+ [[transfer_functions.opacity.points]]
60
+ x = 120
61
+ y = 0.0
62
+
63
+ [[transfer_functions.opacity.points]]
64
+ x = 258.0
65
+ y = 0.8
66
+
67
+ [[transfer_functions.opacity.points]]
68
+ x = 320
69
+ y = 0.0
70
+
71
+ [transfer_functions.color]
72
+ [[transfer_functions.color.points]]
73
+ x = 120
74
+ color = [1.0, 0.0, 0.0]
75
+
76
+ [[transfer_functions.color.points]]
77
+ x = 320
78
+ color = [1.0, 1.0, 0.0]
@@ -0,0 +1,54 @@
1
+ name = "Vascular_Open"
2
+ description = "Vascular (Open)"
3
+
4
+ ambient = 0.7
5
+ diffuse = 0.4
6
+ specular = 1.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = -96.5
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = 269.0
17
+ y = 0.05
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 474.5
21
+ y = 0.0
22
+
23
+ [transfer_functions.color]
24
+ [[transfer_functions.color.points]]
25
+ x = -96.5
26
+ color = [1.0, 1.0, 1.0]
27
+
28
+ [[transfer_functions.color.points]]
29
+ x = 474.5
30
+ color = [1.0, 1.0, 1.0]
31
+
32
+ [[transfer_functions]]
33
+
34
+ [transfer_functions.opacity]
35
+ [[transfer_functions.opacity.points]]
36
+ x = 117
37
+ y = 0.0
38
+
39
+ [[transfer_functions.opacity.points]]
40
+ x = 246.0
41
+ y = 0.87
42
+
43
+ [[transfer_functions.opacity.points]]
44
+ x = 319
45
+ y = 0.0
46
+
47
+ [transfer_functions.color]
48
+ [[transfer_functions.color.points]]
49
+ x = 117
50
+ color = [1.0, 0.0, 0.0]
51
+
52
+ [[transfer_functions.color.points]]
53
+ x = 319
54
+ color = [1.0, 1.0, 0.0]
@@ -0,0 +1,42 @@
1
+ name = "Radiograph"
2
+ description = "Radiograph"
3
+
4
+ ambient = 1.0
5
+ diffuse = 1.0
6
+ specular = 0.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = -1024.0
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = -200.0
17
+ y = 0.0
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 300.0
21
+ y = 0.01
22
+
23
+ [[transfer_functions.opacity.points]]
24
+ x = 3071.0
25
+ y = 0.01
26
+
27
+ [transfer_functions.color]
28
+ [[transfer_functions.color.points]]
29
+ x = -1024.0
30
+ color = [0.0, 0.0, 0.0]
31
+
32
+ [[transfer_functions.color.points]]
33
+ x = -200.0
34
+ color = [0.0, 0.0, 0.0]
35
+
36
+ [[transfer_functions.color.points]]
37
+ x = 300.0
38
+ color = [1.0, 1.0, 1.0]
39
+
40
+ [[transfer_functions.color.points]]
41
+ x = 3071.0
42
+ color = [1.0, 1.0, 1.0]
@@ -0,0 +1,81 @@
1
+ import numpy as np
2
+ import vtk
3
+
4
+
5
+ def blend_transfer_functions(tfs, scalar_range=(-2000, 2000), num_samples=512):
6
+ """
7
+ Blend multiple transfer functions using volume rendering emission-absorption model.
8
+
9
+ Based on the volume rendering equation from:
10
+ - Levoy, M. "Display of Surfaces from Volume Data" IEEE Computer Graphics and Applications, 1988
11
+ - Kajiya, J.T. & Von Herzen, B.P. "Ray tracing volume densities" ACM SIGGRAPH Computer Graphics, 1984
12
+ - Engel, K. et al. "Real-time Volume Graphics" A K Peters, 2006, Chapter 2
13
+
14
+ The volume rendering integral: I = ∫ C(s) * μ(s) * T(s) ds
15
+ where C(s) = emission color, μ(s) = opacity, T(s) = transmission
16
+
17
+ For discrete transfer functions, this becomes:
18
+ - Total emission = Σ(color_i * opacity_i)
19
+ - Total absorption = Σ(opacity_i)
20
+ - Final color = total_emission / total_absorption
21
+ """
22
+ if len(tfs) == 1:
23
+ return tfs[0]
24
+
25
+ sample_points = np.linspace(
26
+ start=scalar_range[0],
27
+ stop=scalar_range[1],
28
+ num=num_samples,
29
+ )
30
+
31
+ # Initialize arrays to store blended values
32
+ blended_opacity = []
33
+ blended_color = []
34
+
35
+ for scalar_val in sample_points:
36
+ # Accumulate emission and absorption for volume rendering
37
+ total_emission = [0.0, 0.0, 0.0]
38
+ total_absorption = 0.0
39
+
40
+ for otf, ctf in tfs:
41
+ # Get opacity and color for this scalar value
42
+ layer_opacity = otf.GetValue(scalar_val)
43
+ layer_color = [0.0, 0.0, 0.0]
44
+ ctf.GetColor(scalar_val, layer_color)
45
+
46
+ # Volume rendering accumulation:
47
+ # Emission = color * opacity (additive)
48
+ # Absorption = opacity (multiplicative through transmission)
49
+ for i in range(3):
50
+ total_emission[i] += layer_color[i] * layer_opacity
51
+
52
+ total_absorption += layer_opacity
53
+
54
+ # Clamp values to reasonable ranges
55
+ total_absorption = min(total_absorption, 1.0)
56
+ for i in range(3):
57
+ total_emission[i] = min(total_emission[i], 1.0)
58
+
59
+ # For the final color, normalize emission by absorption if absorption > 0
60
+ if total_absorption > 0.001: # Avoid division by zero
61
+ final_color = [total_emission[i] / total_absorption for i in range(3)]
62
+ else:
63
+ final_color = [0.0, 0.0, 0.0]
64
+
65
+ # Clamp final colors
66
+ final_color = [min(c, 1.0) for c in final_color]
67
+
68
+ blended_opacity.append(total_absorption)
69
+ blended_color.append(final_color)
70
+
71
+ # Create new VTK transfer functions with blended values
72
+ blended_otf = vtk.vtkPiecewiseFunction()
73
+ blended_ctf = vtk.vtkColorTransferFunction()
74
+
75
+ for i, scalar_val in enumerate(sample_points):
76
+ blended_otf.AddPoint(scalar_val, blended_opacity[i])
77
+ blended_ctf.AddRGBPoint(
78
+ scalar_val, blended_color[i][0], blended_color[i][1], blended_color[i][2]
79
+ )
80
+
81
+ return blended_otf, blended_ctf
@@ -0,0 +1,27 @@
1
+ import pydantic as pc
2
+ import vtk
3
+
4
+ from .types import RGBColor
5
+
6
+
7
+ class ColorTransferFunctionPoint(pc.BaseModel):
8
+ """A single point in a color transfer function."""
9
+
10
+ x: float = pc.Field(description="Scalar value")
11
+ color: RGBColor
12
+
13
+
14
+ class ColorTransferFunctionConfig(pc.BaseModel):
15
+ """Configuration for a VTK color transfer function."""
16
+
17
+ points: list[ColorTransferFunctionPoint] = pc.Field(
18
+ min_length=1, description="Points defining the color transfer function"
19
+ )
20
+
21
+ @property
22
+ def vtk_function(self) -> vtk.vtkColorTransferFunction:
23
+ """Create VTK color transfer function from this configuration."""
24
+ ctf = vtk.vtkColorTransferFunction()
25
+ for point in self.points:
26
+ ctf.AddRGBPoint(point.x, *point.color)
27
+ return ctf