Skip to content

Commit bcd9b12

Browse files
Merge pull request #220 from naplab/brain_sulc_alpha
Create sulcus alpha and light source for brain plots
2 parents 21d517a + ffa3acc commit bcd9b12

3 files changed

Lines changed: 60 additions & 7 deletions

File tree

naplib/localization/freesurfer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def __init__(
124124
except Exception as e:
125125
logger.warning(f'No {hemi}.sulc file found. No sulcus information will be used.')
126126
self.sulc = None
127+
self.sulc_alpha = 1.0
127128

128129

129130
self.load_labels()

naplib/utils/surfdist.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import gdist
77
import matplotlib.pyplot as plt
8+
from matplotlib.colors import LightSource
89
import numpy as np
910
from nibabel.freesurfer.io import read_annot
1011

@@ -124,17 +125,19 @@ def surfdist_viz(
124125
alpha="auto",
125126
bg_map=None,
126127
bg_on_stat=False,
128+
bg_alpha=1.0,
127129
figsize=None,
128130
ax=None,
129131
vmin=None,
130132
vmax=None,
133+
light_source=None,
131134
):
132135
"""Visualize results on cortical surface using matplotlib.
133136
134137
Parameters
135138
----------
136139
coords : numpy array of shape (n_nodes,3), each row specifying the x,y,z
137-
coordinates of one node of surface mesh
140+
coordinates of one node of surface mesh
138141
faces : numpy array of shape (n_faces, 3), each row specifying the indices
139142
of the three nodes building one node of the surface mesh
140143
stat_map : numpy array of shape (n_nodes,) containing the values to be
@@ -158,9 +161,16 @@ def surfdist_viz(
158161
multiplied with the background map for shadowing. Otherwise,
159162
only areas that are not covered by the statsitical map after
160163
thresholding will show shadows.
164+
bg_alpha : float, determines the opacity of the background map.
165+
bg_alpha defaults to 1.0 and is only relevant if bg_on_stat
161166
figsize : tuple of intergers, dimensions of the figure that is produced.
162167
ax : Axis
163168
Axis to plot on, with 3d projection.
169+
light_source: None, bool, or tuple of int, optional
170+
Whether to apply a light source for shading. If True, the light
171+
source position is inferred from `elev` and `azim`. If a tuple of
172+
(alt, az), these values will be used to specify the light source
173+
position. If None or False, no shading is applied. Default is None.
164174
165175
Returns
166176
-------
@@ -226,7 +236,7 @@ def surfdist_viz(
226236
bg_faces = np.mean(bg_data[faces], axis=1)
227237
bg_faces = bg_faces - bg_faces.min()
228238
bg_faces = bg_faces / bg_faces.max()
229-
face_colors = plt.cm.gray_r(bg_faces)
239+
face_colors = plt.cm.gray_r(bg_faces * bg_alpha)
230240

231241
# modify alpha values of background
232242
face_colors[:, 3] = alpha * face_colors[:, 3]
@@ -260,6 +270,41 @@ def surfdist_viz(
260270
else:
261271
face_colors = cmap(stat_map_faces)
262272

273+
if light_source:
274+
if hasattr(light_source, '__len__'):
275+
if len(light_source) == 2:
276+
ls = LightSource(azdeg=light_source[1], altdeg=light_source[0])
277+
else:
278+
# Apply lighting to the face colors for shading
279+
ls = LightSource(azdeg=azim, altdeg=elev)
280+
281+
# Manually calculate the light vector since the 'light_vector'
282+
# attribute is not accessible in some matplotlib versions.
283+
az = np.radians(ls.azdeg)
284+
alt = np.radians(ls.altdeg)
285+
light_vec = np.array([
286+
np.cos(az) * np.cos(alt),
287+
np.sin(az) * np.cos(alt),
288+
np.sin(alt)
289+
])
290+
291+
# Calculate face normals
292+
v0 = coords[faces[:, 0]]
293+
v1 = coords[faces[:, 1]]
294+
v2 = coords[faces[:, 2]]
295+
face_normals = np.cross(v1 - v0, v2 - v0)
296+
face_normals /= np.linalg.norm(face_normals, axis=1)[:, np.newaxis]
297+
298+
# The shade is the dot product of the light vector and face normals
299+
shade = np.dot(face_normals, light_vec)
300+
301+
# Modulate the RGB colors by the shade, keeping the alpha channel
302+
# Use np.clip to keep shade values between 0 and 1
303+
illuminated_rgb = face_colors[:, :3] * np.clip(shade, 0, 1)[:, np.newaxis]
304+
305+
# Combine illuminated RGB with the original alpha channel
306+
face_colors = np.hstack((illuminated_rgb, face_colors[:, 3:]))
307+
263308
p3dcollec.set_facecolors(face_colors)
264309

265310
if not premade_ax:

naplib/visualization/brain_plots.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def _view(hemi, mode: str = "lateral", backend: str = "mpl"):
9090
raise ValueError(f"Unknown `mode`: {mode}.")
9191

9292

93-
def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin=None, vmax=None):
93+
def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin=None, vmax=None, light_source=False):
9494
if isinstance(view, tuple):
9595
elev, azim = view
9696
else:
@@ -105,16 +105,18 @@ def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin
105105
alpha=hemi.alpha,
106106
bg_map=hemi.sulc,
107107
bg_on_stat=True,
108+
bg_alpha=hemi.sulc_alpha,
108109
ax=ax,
109110
vmin=vmin,
110-
vmax=vmax
111+
vmax=vmax,
112+
light_source=light_source
111113
)
112114
ax.axes.set_axis_off()
113115
ax.grid(False)
114116

115117

116118
def plot_brain_overlay(
117-
brain, cmap="coolwarm", ax=None, hemi='both', view="best", vmin=None, vmax=None, cmap_quantile=1.0, threshold=None, **kwargs
119+
brain, cmap="coolwarm", ax=None, hemi='both', view="best", vmin=None, vmax=None, cmap_quantile=1.0, threshold=None, light_source=False, **kwargs
118120
):
119121
"""
120122
Plot brain overlay on the 3D cortical surface using matplotlib.
@@ -149,6 +151,11 @@ def plot_brain_overlay(
149151
threshold : positive float, optional
150152
If given, then only values on the overlay which are less -threshold or greater than threshold will
151153
be shown.
154+
light_source: None, bool, or tuple of int, optional
155+
Whether to apply a light source for shading. If True, the light
156+
source position is inferred from `elev` and `azim`. If a tuple of
157+
(alt, az), these values will be used to specify the light source
158+
position. If None or False, no shading is applied. Default is True.
152159
**kwargs : kwargs
153160
Any other kwargs to pass to matplotlib.pyplot.figure (such as figsize)
154161
@@ -216,9 +223,9 @@ def plot_brain_overlay(
216223

217224

218225
if ax[0] is not None:
219-
_plot_hemi(brain.lh, cmap, ax[0], view=view, vmin=vmin, vmax=vmax, threshold=threshold)
226+
_plot_hemi(brain.lh, cmap, ax[0], view=view, vmin=vmin, vmax=vmax, threshold=threshold, light_source=light_source)
220227
if ax[1] is not None:
221-
_plot_hemi(brain.rh, cmap, ax[1], view=view, vmin=vmin, vmax=vmax, threshold=threshold)
228+
_plot_hemi(brain.rh, cmap, ax[1], view=view, vmin=vmin, vmax=vmax, threshold=threshold, light_source=light_source)
222229

223230
return fig, ax
224231

0 commit comments

Comments
 (0)