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.
- bsplot/__init__.py +23 -0
- bsplot/anat.py +55 -0
- bsplot/animate.py +194 -0
- bsplot/bioicons.py +777 -0
- bsplot/brain.py +188 -0
- bsplot/colors.py +1935 -0
- bsplot/data/HCP_avg-SC.txt +379 -0
- bsplot/data/MNI152.rh.pial +0 -0
- bsplot/data/STN_lh.nii.gz +0 -0
- bsplot/data/STN_rh.nii.gz +0 -0
- bsplot/data/__init__.py +22 -0
- bsplot/data/left_electrode.ply +257270 -0
- bsplot/data/parcellations/HCP-MMP1.L.label.gii +442 -0
- bsplot/data/parcellations/HCP-MMP1.R.label.gii +442 -0
- bsplot/data/parcellations/code/split_hcpmmp1.sh +4 -0
- bsplot/data/right_electrode.ply +257270 -0
- bsplot/data/surface.py +112 -0
- bsplot/data/tpl-MNI152NLin2009b_atlas-hcpmmp1_desc-ordered_dseg.nii.gz +0 -0
- bsplot/figure.py +177 -0
- bsplot/graph/__init__.py +31 -0
- bsplot/graph/edges.py +312 -0
- bsplot/graph/flowchart.py +762 -0
- bsplot/graph/layout.py +201 -0
- bsplot/graph/network.py +974 -0
- bsplot/graph/nodes.py +452 -0
- bsplot/panels.py +110 -0
- bsplot/scientific_color_maps/__init__.py +0 -0
- bsplot/scientific_color_maps/_sync_from_zenodo_download.sh +1 -0
- bsplot/scientific_color_maps/batlowK.txt +256 -0
- bsplot/scientific_color_maps/batlowW.txt +256 -0
- bsplot/scientific_color_maps/cyclic/bamO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/brocO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/corkO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/romaO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/vikO.txt +256 -0
- bsplot/scientific_color_maps/diverging/bam.txt +256 -0
- bsplot/scientific_color_maps/diverging/berlin.txt +256 -0
- bsplot/scientific_color_maps/diverging/broc.txt +256 -0
- bsplot/scientific_color_maps/diverging/cork.txt +256 -0
- bsplot/scientific_color_maps/diverging/lisbon.txt +256 -0
- bsplot/scientific_color_maps/diverging/managua.txt +256 -0
- bsplot/scientific_color_maps/diverging/roma.txt +256 -0
- bsplot/scientific_color_maps/diverging/tofino.txt +256 -0
- bsplot/scientific_color_maps/diverging/vanimo.txt +256 -0
- bsplot/scientific_color_maps/diverging/vik.txt +256 -0
- bsplot/scientific_color_maps/multisequential/bukavu.txt +256 -0
- bsplot/scientific_color_maps/multisequential/fes.txt +256 -0
- bsplot/scientific_color_maps/multisequential/oleron.txt +256 -0
- bsplot/scientific_color_maps/naviaW.txt +256 -0
- bsplot/scientific_color_maps/sequential/acton.txt +256 -0
- bsplot/scientific_color_maps/sequential/bamako.txt +256 -0
- bsplot/scientific_color_maps/sequential/batlow.txt +256 -0
- bsplot/scientific_color_maps/sequential/bilbao.txt +256 -0
- bsplot/scientific_color_maps/sequential/buda.txt +256 -0
- bsplot/scientific_color_maps/sequential/davos.txt +256 -0
- bsplot/scientific_color_maps/sequential/devon.txt +256 -0
- bsplot/scientific_color_maps/sequential/glasgow.txt +256 -0
- bsplot/scientific_color_maps/sequential/grayC.txt +256 -0
- bsplot/scientific_color_maps/sequential/hawaii.txt +256 -0
- bsplot/scientific_color_maps/sequential/imola.txt +256 -0
- bsplot/scientific_color_maps/sequential/lajolla.txt +256 -0
- bsplot/scientific_color_maps/sequential/lapaz.txt +256 -0
- bsplot/scientific_color_maps/sequential/lipari.txt +256 -0
- bsplot/scientific_color_maps/sequential/navia.txt +256 -0
- bsplot/scientific_color_maps/sequential/nuuk.txt +256 -0
- bsplot/scientific_color_maps/sequential/oslo.txt +256 -0
- bsplot/scientific_color_maps/sequential/tokyo.txt +256 -0
- bsplot/scientific_color_maps/sequential/turku.txt +256 -0
- bsplot/streamlines.py +314 -0
- bsplot/style.py +791 -0
- bsplot/styles/black.mplstyle +78 -0
- bsplot/styles/bss.mplstyle +50 -0
- bsplot/styles/bwcomp.mplstyle +30 -0
- bsplot/styles/nature.mplstyle +57 -0
- bsplot/styles/transparent.mplstyle +16 -0
- bsplot/styles/tvbo.mplstyle +57 -0
- bsplot/surface.py +2370 -0
- bsplot/templates.py +270 -0
- bsplot/text.py +199 -0
- bsplot/text2obj.py +59 -0
- bsplot/timeseries.py +10 -0
- bsplot/utils.py +13 -0
- bsplot/volume.py +958 -0
- bsplot-0.0.2.dist-info/METADATA +60 -0
- bsplot-0.0.2.dist-info/RECORD +89 -0
- bsplot-0.0.2.dist-info/WHEEL +5 -0
- bsplot-0.0.2.dist-info/entry_points.txt +2 -0
- bsplot-0.0.2.dist-info/licenses/LICENSE +193 -0
- 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
|