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/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
+ )