pytractoviz 0.2.14__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.
- pytractoviz/__init__.py +11 -0
- pytractoviz/__main__.py +14 -0
- pytractoviz/_internal/__init__.py +0 -0
- pytractoviz/_internal/cli.py +59 -0
- pytractoviz/_internal/debug.py +110 -0
- pytractoviz/html.py +845 -0
- pytractoviz/py.typed +0 -0
- pytractoviz/utils.py +220 -0
- pytractoviz/viz.py +4272 -0
- pytractoviz-0.2.14.dist-info/METADATA +53 -0
- pytractoviz-0.2.14.dist-info/RECORD +14 -0
- pytractoviz-0.2.14.dist-info/WHEEL +4 -0
- pytractoviz-0.2.14.dist-info/entry_points.txt +5 -0
- pytractoviz-0.2.14.dist-info/licenses/LICENSE +21 -0
pytractoviz/py.typed
ADDED
|
File without changes
|
pytractoviz/utils.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Utility functions for tractography visualization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from dipy.tracking.streamline import Streamlines
|
|
11
|
+
from fury import window
|
|
12
|
+
|
|
13
|
+
# Standard anatomical view angles (elevation, azimuth, roll)
|
|
14
|
+
ANATOMICAL_VIEW_ANGLES = {
|
|
15
|
+
"coronal": (-90.0, 0.0, 0.0), # Front view
|
|
16
|
+
"axial": (0.0, 180.0, 0.0), # Top-down view
|
|
17
|
+
"sagittal": (-90.0, 0.0, 90.0), # Side view (right)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def calculate_centroid(streamlines: Streamlines) -> np.ndarray:
|
|
22
|
+
"""Calculate the centroid of streamlines.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
streamlines : Streamlines
|
|
27
|
+
The streamlines to calculate centroid for.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
np.ndarray
|
|
32
|
+
The centroid coordinates (3D).
|
|
33
|
+
"""
|
|
34
|
+
all_points = np.vstack([np.array(sl) for sl in streamlines])
|
|
35
|
+
return np.mean(all_points, axis=0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def calculate_bbox_size(streamlines: Streamlines) -> np.ndarray:
|
|
39
|
+
"""Calculate bounding box size of streamlines.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
streamlines : Streamlines
|
|
44
|
+
The streamlines to calculate bbox for.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
np.ndarray
|
|
49
|
+
The bounding box size (3D).
|
|
50
|
+
"""
|
|
51
|
+
all_points = np.vstack([np.array(sl) for sl in streamlines])
|
|
52
|
+
return np.max(all_points, axis=0) - np.min(all_points, axis=0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def calculate_direction_colors(streamlines: Streamlines) -> np.ndarray:
|
|
56
|
+
"""Calculate colors for streamlines based on their diffusion direction.
|
|
57
|
+
|
|
58
|
+
Standard RGB mapping:
|
|
59
|
+
- Red = X-axis (left/right)
|
|
60
|
+
- Green = Y-axis (anterior/posterior)
|
|
61
|
+
- Blue = Z-axis (superior/inferior)
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
streamlines : Streamlines
|
|
66
|
+
The streamlines to calculate colors for.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
np.ndarray
|
|
71
|
+
Array of RGB colors, one per streamline (N x 3).
|
|
72
|
+
"""
|
|
73
|
+
streamline_colors = []
|
|
74
|
+
max_range = 1e-10
|
|
75
|
+
sl_len = 2
|
|
76
|
+
for sl in streamlines:
|
|
77
|
+
sl_array = np.array(sl)
|
|
78
|
+
if len(sl_array) < sl_len:
|
|
79
|
+
# Degenerate streamline, use default color
|
|
80
|
+
streamline_colors.append([0.5, 0.5, 0.5])
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Calculate direction vector (from start to end)
|
|
84
|
+
direction = sl_array[-1] - sl_array[0]
|
|
85
|
+
direction_norm = np.linalg.norm(direction)
|
|
86
|
+
|
|
87
|
+
if direction_norm < max_range:
|
|
88
|
+
# Degenerate direction, use default color
|
|
89
|
+
streamline_colors.append([0.5, 0.5, 0.5])
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Normalize direction to unit vector
|
|
93
|
+
direction = direction / direction_norm
|
|
94
|
+
|
|
95
|
+
# Map direction components to RGB using absolute values
|
|
96
|
+
# X -> Red (left/right), Y -> Green (anterior/posterior), Z -> Blue (superior/inferior)
|
|
97
|
+
r = abs(direction[0]) # X component -> Red
|
|
98
|
+
g = abs(direction[1]) # Y component -> Green
|
|
99
|
+
b = abs(direction[2]) # Z component -> Blue
|
|
100
|
+
|
|
101
|
+
# Normalize by the maximum component to ensure colors are in 0-1 range
|
|
102
|
+
# This preserves the relative direction while ensuring valid RGB values
|
|
103
|
+
max_component = max(r, g, b)
|
|
104
|
+
if max_component > max_range:
|
|
105
|
+
r = r / max_component
|
|
106
|
+
g = g / max_component
|
|
107
|
+
b = b / max_component
|
|
108
|
+
else:
|
|
109
|
+
# Fallback for edge case
|
|
110
|
+
r, g, b = 0.5, 0.5, 0.5
|
|
111
|
+
|
|
112
|
+
streamline_colors.append([r, g, b])
|
|
113
|
+
|
|
114
|
+
return np.array(streamline_colors)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def calculate_combined_centroid(*streamlines_groups: Streamlines) -> np.ndarray:
|
|
118
|
+
"""Calculate the centroid of multiple groups of streamlines combined.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
*streamlines_groups : Streamlines
|
|
123
|
+
One or more Streamlines objects to combine.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
np.ndarray
|
|
128
|
+
The combined centroid coordinates (3D).
|
|
129
|
+
"""
|
|
130
|
+
all_points_list = []
|
|
131
|
+
for streamlines in streamlines_groups:
|
|
132
|
+
all_points_list.append(np.vstack([np.array(sl) for sl in streamlines]))
|
|
133
|
+
all_points = np.vstack(all_points_list)
|
|
134
|
+
return np.mean(all_points, axis=0)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def calculate_combined_bbox_size(*streamlines_groups: Streamlines) -> np.ndarray:
|
|
138
|
+
"""Calculate bounding box size of multiple groups of streamlines combined.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
*streamlines_groups : Streamlines
|
|
143
|
+
One or more Streamlines objects to combine.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
np.ndarray
|
|
148
|
+
The combined bounding box size (3D).
|
|
149
|
+
"""
|
|
150
|
+
all_points_list = []
|
|
151
|
+
for streamlines in streamlines_groups:
|
|
152
|
+
all_points_list.append(np.vstack([np.array(sl) for sl in streamlines]))
|
|
153
|
+
all_points = np.vstack(all_points_list)
|
|
154
|
+
return np.max(all_points, axis=0) - np.min(all_points, axis=0)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def set_anatomical_camera(
|
|
158
|
+
scene: window.Scene,
|
|
159
|
+
centroid: np.ndarray,
|
|
160
|
+
view_name: str,
|
|
161
|
+
*,
|
|
162
|
+
camera_distance: float | None = None,
|
|
163
|
+
bbox_size: np.ndarray | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Set camera position for standard anatomical views.
|
|
166
|
+
|
|
167
|
+
This function positions the camera for coronal, axial, or sagittal views
|
|
168
|
+
without rotating the streamlines, ensuring colors stay aligned.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
scene : window.Scene
|
|
173
|
+
The FURY scene to set the camera on.
|
|
174
|
+
centroid : np.ndarray
|
|
175
|
+
The centroid of the streamlines (3D coordinates).
|
|
176
|
+
view_name : str
|
|
177
|
+
Name of the view: "coronal", "axial", or "sagittal".
|
|
178
|
+
camera_distance : float | None, optional
|
|
179
|
+
Distance of camera from centroid. If None, calculated from bbox_size.
|
|
180
|
+
bbox_size : np.ndarray | None, optional
|
|
181
|
+
Bounding box size of streamlines. Used to calculate camera_distance if not provided.
|
|
182
|
+
|
|
183
|
+
Raises
|
|
184
|
+
------
|
|
185
|
+
ValueError
|
|
186
|
+
If view_name is not one of the standard anatomical views.
|
|
187
|
+
"""
|
|
188
|
+
# Calculate camera distance if not provided
|
|
189
|
+
if camera_distance is None:
|
|
190
|
+
max_dim = np.max(bbox_size) if bbox_size is not None else 100.0
|
|
191
|
+
camera_distance = max_dim * 2.5
|
|
192
|
+
|
|
193
|
+
# Define camera positions and view_up vectors for each anatomical view
|
|
194
|
+
if view_name == "coronal":
|
|
195
|
+
# Coronal: front view (anterior), looking posterior
|
|
196
|
+
# Camera from posterior (-Y) to flip left/right, looking at centroid
|
|
197
|
+
camera_position = centroid + np.array([0, -camera_distance, 0])
|
|
198
|
+
view_up = np.array([0, 0, 1]) # Superior is up
|
|
199
|
+
elif view_name == "axial":
|
|
200
|
+
# Axial: top-down view (superior), looking inferior
|
|
201
|
+
# Camera from superior (+Z), looking at centroid
|
|
202
|
+
# Use -Y as view_up to flip left/right (instead of +Y)
|
|
203
|
+
camera_position = centroid + np.array([0, 0, camera_distance])
|
|
204
|
+
view_up = np.array([0, -1, 0]) # Posterior as up (flips left/right)
|
|
205
|
+
elif view_name == "sagittal":
|
|
206
|
+
# Sagittal: side view (left), looking right
|
|
207
|
+
# Camera from left (-X), looking at centroid
|
|
208
|
+
camera_position = centroid + np.array([-camera_distance, 0, 0])
|
|
209
|
+
view_up = np.array([0, 0, 1]) # Superior is up
|
|
210
|
+
else:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"Invalid view name: {view_name}. Must be one of: coronal, axial, sagittal",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Set camera
|
|
216
|
+
scene.set_camera(
|
|
217
|
+
position=camera_position,
|
|
218
|
+
focal_point=centroid,
|
|
219
|
+
view_up=view_up,
|
|
220
|
+
)
|