bsplot 0.0.2__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.
Files changed (89) hide show
  1. bsplot/__init__.py +23 -0
  2. bsplot/anat.py +55 -0
  3. bsplot/animate.py +194 -0
  4. bsplot/bioicons.py +777 -0
  5. bsplot/brain.py +188 -0
  6. bsplot/colors.py +1935 -0
  7. bsplot/data/HCP_avg-SC.txt +379 -0
  8. bsplot/data/MNI152.rh.pial +0 -0
  9. bsplot/data/STN_lh.nii.gz +0 -0
  10. bsplot/data/STN_rh.nii.gz +0 -0
  11. bsplot/data/__init__.py +22 -0
  12. bsplot/data/left_electrode.ply +257270 -0
  13. bsplot/data/parcellations/HCP-MMP1.L.label.gii +442 -0
  14. bsplot/data/parcellations/HCP-MMP1.R.label.gii +442 -0
  15. bsplot/data/parcellations/code/split_hcpmmp1.sh +4 -0
  16. bsplot/data/right_electrode.ply +257270 -0
  17. bsplot/data/surface.py +112 -0
  18. bsplot/data/tpl-MNI152NLin2009b_atlas-hcpmmp1_desc-ordered_dseg.nii.gz +0 -0
  19. bsplot/figure.py +177 -0
  20. bsplot/graph/__init__.py +31 -0
  21. bsplot/graph/edges.py +312 -0
  22. bsplot/graph/flowchart.py +762 -0
  23. bsplot/graph/layout.py +201 -0
  24. bsplot/graph/network.py +974 -0
  25. bsplot/graph/nodes.py +452 -0
  26. bsplot/panels.py +110 -0
  27. bsplot/scientific_color_maps/__init__.py +0 -0
  28. bsplot/scientific_color_maps/_sync_from_zenodo_download.sh +1 -0
  29. bsplot/scientific_color_maps/batlowK.txt +256 -0
  30. bsplot/scientific_color_maps/batlowW.txt +256 -0
  31. bsplot/scientific_color_maps/cyclic/bamO.txt +256 -0
  32. bsplot/scientific_color_maps/cyclic/brocO.txt +256 -0
  33. bsplot/scientific_color_maps/cyclic/corkO.txt +256 -0
  34. bsplot/scientific_color_maps/cyclic/romaO.txt +256 -0
  35. bsplot/scientific_color_maps/cyclic/vikO.txt +256 -0
  36. bsplot/scientific_color_maps/diverging/bam.txt +256 -0
  37. bsplot/scientific_color_maps/diverging/berlin.txt +256 -0
  38. bsplot/scientific_color_maps/diverging/broc.txt +256 -0
  39. bsplot/scientific_color_maps/diverging/cork.txt +256 -0
  40. bsplot/scientific_color_maps/diverging/lisbon.txt +256 -0
  41. bsplot/scientific_color_maps/diverging/managua.txt +256 -0
  42. bsplot/scientific_color_maps/diverging/roma.txt +256 -0
  43. bsplot/scientific_color_maps/diverging/tofino.txt +256 -0
  44. bsplot/scientific_color_maps/diverging/vanimo.txt +256 -0
  45. bsplot/scientific_color_maps/diverging/vik.txt +256 -0
  46. bsplot/scientific_color_maps/multisequential/bukavu.txt +256 -0
  47. bsplot/scientific_color_maps/multisequential/fes.txt +256 -0
  48. bsplot/scientific_color_maps/multisequential/oleron.txt +256 -0
  49. bsplot/scientific_color_maps/naviaW.txt +256 -0
  50. bsplot/scientific_color_maps/sequential/acton.txt +256 -0
  51. bsplot/scientific_color_maps/sequential/bamako.txt +256 -0
  52. bsplot/scientific_color_maps/sequential/batlow.txt +256 -0
  53. bsplot/scientific_color_maps/sequential/bilbao.txt +256 -0
  54. bsplot/scientific_color_maps/sequential/buda.txt +256 -0
  55. bsplot/scientific_color_maps/sequential/davos.txt +256 -0
  56. bsplot/scientific_color_maps/sequential/devon.txt +256 -0
  57. bsplot/scientific_color_maps/sequential/glasgow.txt +256 -0
  58. bsplot/scientific_color_maps/sequential/grayC.txt +256 -0
  59. bsplot/scientific_color_maps/sequential/hawaii.txt +256 -0
  60. bsplot/scientific_color_maps/sequential/imola.txt +256 -0
  61. bsplot/scientific_color_maps/sequential/lajolla.txt +256 -0
  62. bsplot/scientific_color_maps/sequential/lapaz.txt +256 -0
  63. bsplot/scientific_color_maps/sequential/lipari.txt +256 -0
  64. bsplot/scientific_color_maps/sequential/navia.txt +256 -0
  65. bsplot/scientific_color_maps/sequential/nuuk.txt +256 -0
  66. bsplot/scientific_color_maps/sequential/oslo.txt +256 -0
  67. bsplot/scientific_color_maps/sequential/tokyo.txt +256 -0
  68. bsplot/scientific_color_maps/sequential/turku.txt +256 -0
  69. bsplot/streamlines.py +314 -0
  70. bsplot/style.py +791 -0
  71. bsplot/styles/black.mplstyle +78 -0
  72. bsplot/styles/bss.mplstyle +50 -0
  73. bsplot/styles/bwcomp.mplstyle +30 -0
  74. bsplot/styles/nature.mplstyle +57 -0
  75. bsplot/styles/transparent.mplstyle +16 -0
  76. bsplot/styles/tvbo.mplstyle +57 -0
  77. bsplot/surface.py +2370 -0
  78. bsplot/templates.py +270 -0
  79. bsplot/text.py +199 -0
  80. bsplot/text2obj.py +59 -0
  81. bsplot/timeseries.py +10 -0
  82. bsplot/utils.py +13 -0
  83. bsplot/volume.py +958 -0
  84. bsplot-0.0.2.dist-info/METADATA +60 -0
  85. bsplot-0.0.2.dist-info/RECORD +89 -0
  86. bsplot-0.0.2.dist-info/WHEEL +5 -0
  87. bsplot-0.0.2.dist-info/entry_points.txt +2 -0
  88. bsplot-0.0.2.dist-info/licenses/LICENSE +193 -0
  89. bsplot-0.0.2.dist-info/top_level.txt +1 -0
bsplot/streamlines.py ADDED
@@ -0,0 +1,314 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import matplotlib.pyplot as plt
5
+ import nibabel as nib
6
+ import numpy as np
7
+ from matplotlib import colormaps
8
+ from matplotlib.collections import LineCollection
9
+ from matplotlib.colors import Normalize, TwoSlopeNorm
10
+ from bsplot.colors import Colormap as _resolve_cmap
11
+
12
+
13
+ def sample_streamlines(tractogram, N=100000, values=None):
14
+ total_streamlines = len(tractogram.streamlines)
15
+ subsample_indices = np.linspace(0, total_streamlines - 1, N, dtype=int)
16
+ subsampled_streamlines = [tractogram.streamlines[i] for i in subsample_indices]
17
+ if values is not None:
18
+ subsampled_values = [values[i] for i in subsample_indices]
19
+ return subsampled_streamlines, subsampled_values
20
+ return subsampled_streamlines
21
+
22
+
23
+ def find_optimal_slice(streamlines, affine, plane="coronal"):
24
+ """
25
+ Find the optimal slice position for visualizing streamlines on a 2D plane.
26
+
27
+ Parameters:
28
+ streamlines (list of ndarray): List of streamlines, where each streamline is an ndarray of shape (n_points, 3).
29
+ affine (ndarray): Affine transformation matrix for converting voxel coordinates to real-world coordinates.
30
+ plane (str, optional): The plane to plot ("sagittal", "coronal", "horizontal").
31
+
32
+ Returns:
33
+ optimal_slice (float): The optimal slice position in millimeters.
34
+ """
35
+ all_coords = []
36
+
37
+ for s_coords in streamlines:
38
+ s_coords = nib.affines.apply_affine(affine, s_coords)
39
+ all_coords.append(s_coords)
40
+
41
+ all_coords = np.concatenate(all_coords, axis=0)
42
+
43
+ if plane == "sagittal":
44
+ axis_coords = all_coords[:, 0]
45
+ elif plane == "coronal":
46
+ axis_coords = all_coords[:, 1]
47
+ elif plane == "horizontal":
48
+ axis_coords = all_coords[:, 2]
49
+
50
+ # Find the slice with the maximum number of streamlines crossing it
51
+ hist, bin_edges = np.histogram(axis_coords, bins=50)
52
+ optimal_slice = bin_edges[np.argmax(hist)]
53
+
54
+ return optimal_slice
55
+
56
+
57
+ def get_streamline_color(s_coords, cmap=None):
58
+ """
59
+ Calculate the color of a streamline based on its principal direction,
60
+ where x, y, and z components correspond to red, green, and blue channels respectively.
61
+
62
+ Parameters:
63
+ s_coords (ndarray): Streamline coordinates of shape (n_points, 3).
64
+
65
+ Returns:
66
+ color (tuple): RGB color tuple for the streamline.
67
+ """
68
+ # Compute the mean direction vector from the start to the end of the streamline
69
+ mean_direction = s_coords[-1] - s_coords[0]
70
+ mean_direction = mean_direction / np.linalg.norm(
71
+ mean_direction
72
+ ) # Normalize the direction vector
73
+
74
+ if cmap:
75
+ if isinstance(cmap, str):
76
+ colormap = _resolve_cmap(cmap)
77
+ else:
78
+ colormap = cmap
79
+ scalar_value = np.abs(mean_direction[0])
80
+
81
+ color = colormap(scalar_value) # Returns an RGBA tuple
82
+ else:
83
+ # Convert the direction vector into an RGB color
84
+ # Each component of the mean direction corresponds to a color channel
85
+ # Ensure the direction is within the range [0, 1] for color mapping
86
+ r = np.abs(mean_direction[0]) # Red corresponds to x-direction
87
+ g = np.abs(mean_direction[1]) # Green corresponds to y-direction
88
+ b = np.abs(mean_direction[2]) # Blue corresponds to z-direction
89
+
90
+ # Combine the components into an RGB color
91
+ color = (r, g, b)
92
+
93
+ return color
94
+
95
+
96
+ def load_leaddbs_mat(path):
97
+ """Load a Lead-DBS / DBS-Tractography-Atlas ``.mat`` fiber file.
98
+
99
+ The format stores all fiber coordinates in a ``fibers`` matrix of shape
100
+ ``(N, 4)`` — columns 0-2 are x/y/z in MNI mm space, column 3 is the
101
+ 1-based fiber index — and a ``idx`` vector giving the number of points in
102
+ each fiber.
103
+
104
+ Returns a ``nibabel.streamlines.Tractogram`` with coordinates in RAS mm.
105
+ """
106
+ import scipy.io as sio
107
+
108
+ d = sio.loadmat(str(path))
109
+ fibers = d["fibers"].astype(np.float32)
110
+ lengths = d["idx"].squeeze().astype(int)
111
+ streamlines = np.split(fibers[:, :3], np.cumsum(lengths)[:-1])
112
+ return nib.streamlines.Tractogram(
113
+ streamlines=streamlines,
114
+ affine_to_rasmm=np.eye(4),
115
+ )
116
+
117
+
118
+ def plot_tractogram(
119
+ tractogram,
120
+ ax,
121
+ cmap=None,
122
+ view="sagittal",
123
+ norm=None,
124
+ vmin=None,
125
+ vmax=None,
126
+ alpha=0.8,
127
+ adjustable="box",
128
+ values=None,
129
+ subsample=None,
130
+ threshold=None,
131
+ datatype="data_per_streamline",
132
+ datakey=None,
133
+ flip_lr=False, # flip x-coordinate for radiological convention
134
+ rotate=0, # rotation in degrees (0, 90, 180, 270) for 2D projection
135
+ **kwargs,
136
+ ):
137
+ from tqdm import tqdm
138
+ import importlib
139
+
140
+ try:
141
+ dipy = importlib.import_module("dipy")
142
+ except ImportError as e:
143
+ raise ImportError(
144
+ "plot_tractogram requires 'dipy'. Please install it: pip install dipy"
145
+ ) from e
146
+ from matplotlib.cm import ScalarMappable
147
+
148
+ # Load tractogram from file if path is provided
149
+ if isinstance(tractogram, (str, os.PathLike, Path)):
150
+ path = Path(tractogram)
151
+ if path.suffix.lower() == ".mat":
152
+ tractogram = load_leaddbs_mat(path)
153
+ else:
154
+ tractogram = nib.streamlines.load(str(path))
155
+
156
+ # Prepare streamlines
157
+ streamlines = tractogram.streamlines.copy()
158
+
159
+ # Prepare values if requested via datakey
160
+ if values is None and datakey is not None:
161
+ data = getattr(tractogram, datatype)
162
+ if datakey not in data:
163
+ raise ValueError(f"Data key '{datakey}' not found in streamlines data.")
164
+ values = data[datakey]
165
+
166
+ def _is_pointwise_values(values_in, streamlines_in):
167
+ if values_in is None:
168
+ return False
169
+ if not isinstance(values_in, (list, tuple, np.ndarray)):
170
+ return False
171
+ if len(values_in) != len(streamlines_in):
172
+ return False
173
+ try:
174
+ return all(
175
+ hasattr(v, "__len__") and len(v) == len(streamlines_in[i])
176
+ for i, v in enumerate(values_in)
177
+ )
178
+ except Exception:
179
+ return False
180
+
181
+ pointwise_values = _is_pointwise_values(values, streamlines)
182
+
183
+ # Threshold by values if provided
184
+ if values is not None and threshold is not None:
185
+ if pointwise_values:
186
+ mask = [
187
+ np.nanmax(np.abs(np.asarray(v))) >= threshold for v in values
188
+ ]
189
+ streamlines = [s for s, keep in zip(streamlines, mask) if keep]
190
+ values = [v for v, keep in zip(values, mask) if keep]
191
+ else:
192
+ values = np.asarray(values).ravel()
193
+ mask = np.abs(values) >= threshold
194
+ streamlines = streamlines[mask.ravel()]
195
+ values = values[mask]
196
+
197
+ # Subsample consistently from the current set
198
+ if subsample:
199
+ n = len(streamlines)
200
+ N = min(int(subsample), n)
201
+ idx = np.linspace(0, n - 1, N, dtype=int)
202
+ # Keep ordering stable; slice both streamlines and values if present
203
+ streamlines = [streamlines[i] for i in idx]
204
+ if values is not None:
205
+ if pointwise_values:
206
+ values = [values[i] for i in idx]
207
+ else:
208
+ values = np.asarray(values).ravel()[idx]
209
+
210
+ # Resolve colormap
211
+ if isinstance(cmap, str):
212
+ cmap = _resolve_cmap(cmap)
213
+
214
+ mappable = None
215
+ # Build normalization only when we have values
216
+ if values is not None and norm is None:
217
+ if pointwise_values:
218
+ data_min = np.inf
219
+ data_max = -np.inf
220
+ for v in values:
221
+ v_arr = np.asarray(v)
222
+ if v_arr.size == 0:
223
+ continue
224
+ data_min = min(data_min, np.nanmin(v_arr))
225
+ data_max = max(data_max, np.nanmax(v_arr))
226
+ if not np.isfinite(data_min) or not np.isfinite(data_max):
227
+ data_min, data_max = 0.0, 1.0
228
+ else:
229
+ vals_array = np.asarray(values).ravel()
230
+ data_min = np.nanmin(vals_array)
231
+ data_max = np.nanmax(vals_array)
232
+ vmin_eff = data_min if vmin is None else vmin
233
+ vmax_eff = data_max if vmax is None else vmax
234
+ # Auto-center on zero when the effective range crosses zero
235
+ if vmin_eff < 0 and vmax_eff > 0:
236
+ norm = TwoSlopeNorm(vmin=vmin_eff, vcenter=0.0, vmax=vmax_eff)
237
+ else:
238
+ norm = Normalize(vmin=vmin_eff, vmax=vmax_eff)
239
+
240
+ # Prepare a ScalarMappable for colorbars when we have values
241
+ if values is not None:
242
+ if cmap is None:
243
+ cmap = _resolve_cmap("viridis")
244
+ mappable = ScalarMappable(norm=norm, cmap=cmap)
245
+ # Optional: set_array helps some colorbar behaviors
246
+ if pointwise_values:
247
+ mappable.set_array(np.array([vmin_eff, vmax_eff]))
248
+ else:
249
+ mappable.set_array(np.asarray(values).ravel())
250
+
251
+ for i, streamline in tqdm(enumerate(streamlines), total=len(streamlines)):
252
+ # Coordinates
253
+ x, y, z = streamline[:, :3].T
254
+
255
+ # Flip x-coordinate for radiological convention if requested
256
+ if flip_lr:
257
+ x = -x
258
+
259
+ # 2D projection
260
+ if view.lower() in ["sagittal", "lateral"]:
261
+ x2d, y2d = y, z
262
+ elif view.lower() in ["coronal", "frontal"]:
263
+ x2d, y2d = x, z
264
+ elif view.lower() in ["horizontal", "axial"]:
265
+ x2d, y2d = x, y
266
+
267
+ # Apply rotation if specified
268
+ if rotate != 0:
269
+ angle_rad = np.radians(rotate)
270
+ cos_a, sin_a = np.cos(angle_rad), np.sin(angle_rad)
271
+ x2d_rot = x2d * cos_a - y2d * sin_a
272
+ y2d_rot = x2d * sin_a + y2d * cos_a
273
+ x2d, y2d = x2d_rot, y2d_rot
274
+
275
+ if values is None:
276
+ color = get_streamline_color(streamline, cmap=cmap)
277
+ ax.plot(x2d, y2d, color=color, alpha=alpha, **kwargs)
278
+ elif pointwise_values:
279
+ v_arr = np.asarray(values[i]).astype(float)
280
+ if v_arr.ndim != 1 or len(v_arr) != len(x2d):
281
+ raise ValueError(
282
+ "Pointwise values must match streamline point count."
283
+ )
284
+ v_seg = 0.5 * (v_arr[:-1] + v_arr[1:])
285
+ segments = np.stack(
286
+ [
287
+ np.column_stack([x2d[:-1], y2d[:-1]]),
288
+ np.column_stack([x2d[1:], y2d[1:]]),
289
+ ],
290
+ axis=1,
291
+ )
292
+ colors = cmap(norm(v_seg))
293
+ if alpha == "relative":
294
+ colors[:, 3] = colors[:, 3] * norm(v_seg)
295
+ elif alpha is not None:
296
+ colors[:, 3] = alpha
297
+
298
+ lc_kwargs = dict(kwargs)
299
+ if "linewidth" in lc_kwargs and "linewidths" not in lc_kwargs:
300
+ lc_kwargs["linewidths"] = lc_kwargs.pop("linewidth")
301
+ if "lw" in lc_kwargs and "linewidths" not in lc_kwargs:
302
+ lc_kwargs["linewidths"] = lc_kwargs.pop("lw")
303
+ lc_kwargs.pop("alpha", None)
304
+ lc = LineCollection(segments, colors=colors, **lc_kwargs)
305
+ ax.add_collection(lc)
306
+ else:
307
+ v = float(values[i])
308
+ color = cmap(norm(v))
309
+ alpha_i = float(norm(v)) if alpha == "relative" else alpha
310
+ ax.plot(x2d, y2d, color=color, alpha=alpha_i, **kwargs)
311
+
312
+ ax.set_aspect("equal", adjustable=adjustable)
313
+
314
+ return mappable