From c576770754b09d68f685cc29c4ae2b579a9c510b Mon Sep 17 00:00:00 2001 From: Vinay Raghavan Date: Fri, 29 Aug 2025 15:16:45 -0400 Subject: [PATCH 1/4] Create alpha transparency for sulcus background --- naplib/localization/freesurfer.py | 1 + naplib/utils/surfdist.py | 5 ++++- naplib/visualization/brain_plots.py | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 51be56e..eb89c27 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -124,6 +124,7 @@ def __init__( except Exception as e: logger.warning(f'No {hemi}.sulc file found. No sulcus information will be used.') self.sulc = None + self.sulc_alpha = 1.0 self.load_labels() diff --git a/naplib/utils/surfdist.py b/naplib/utils/surfdist.py index d8e1897..0d0485f 100644 --- a/naplib/utils/surfdist.py +++ b/naplib/utils/surfdist.py @@ -124,6 +124,7 @@ def surfdist_viz( alpha="auto", bg_map=None, bg_on_stat=False, + bg_alpha=1.0, figsize=None, ax=None, vmin=None, @@ -158,6 +159,8 @@ def surfdist_viz( multiplied with the background map for shadowing. Otherwise, only areas that are not covered by the statsitical map after thresholding will show shadows. + bg_alpha : float, determines the opacity of the background map. + bg_alpha defaults to 1.0 and is only relevant if bg_on_stat figsize : tuple of intergers, dimensions of the figure that is produced. ax : Axis Axis to plot on, with 3d projection. @@ -226,7 +229,7 @@ def surfdist_viz( bg_faces = np.mean(bg_data[faces], axis=1) bg_faces = bg_faces - bg_faces.min() bg_faces = bg_faces / bg_faces.max() - face_colors = plt.cm.gray_r(bg_faces) + face_colors = plt.cm.gray_r(bg_faces * bg_alpha) # modify alpha values of background face_colors[:, 3] = alpha * face_colors[:, 3] diff --git a/naplib/visualization/brain_plots.py b/naplib/visualization/brain_plots.py index 5ec0de4..3c8c031 100644 --- a/naplib/visualization/brain_plots.py +++ b/naplib/visualization/brain_plots.py @@ -105,6 +105,7 @@ def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin alpha=hemi.alpha, bg_map=hemi.sulc, bg_on_stat=True, + bg_alpha=hemi.sulc_alpha, ax=ax, vmin=vmin, vmax=vmax From 16145ab72569f84e88c1ca9a703bfa29799ea040 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan Date: Fri, 29 Aug 2025 16:10:29 -0400 Subject: [PATCH 2/4] Add custom light source to brain plot --- naplib/utils/surfdist.py | 44 ++++++++++++++++++++++++++++- naplib/visualization/brain_plots.py | 16 +++++++---- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/naplib/utils/surfdist.py b/naplib/utils/surfdist.py index 0d0485f..cc7e1b1 100644 --- a/naplib/utils/surfdist.py +++ b/naplib/utils/surfdist.py @@ -5,6 +5,7 @@ import gdist import matplotlib.pyplot as plt +from matplotlib.colors import LightSource import numpy as np from nibabel.freesurfer.io import read_annot @@ -129,13 +130,14 @@ def surfdist_viz( ax=None, vmin=None, vmax=None, + light_source=None, ): """Visualize results on cortical surface using matplotlib. Parameters ---------- coords : numpy array of shape (n_nodes,3), each row specifying the x,y,z - coordinates of one node of surface mesh + coordinates of one node of surface mesh faces : numpy array of shape (n_faces, 3), each row specifying the indices of the three nodes building one node of the surface mesh stat_map : numpy array of shape (n_nodes,) containing the values to be @@ -164,6 +166,11 @@ def surfdist_viz( figsize : tuple of intergers, dimensions of the figure that is produced. ax : Axis Axis to plot on, with 3d projection. + light_source: None, bool, or tuple of int, optional + Whether to apply a light source for shading. If True, the light + source position is inferred from `elev` and `azim`. If a tuple of + (alt, az), these values will be used to specify the light source + position. If None or False, no shading is applied. Default is None. Returns ------- @@ -263,6 +270,41 @@ def surfdist_viz( else: face_colors = cmap(stat_map_faces) + if light_source: + if hasattr(light_source, '__len__'): + if len(light_source) == 2: + ls = LightSource(azdeg=light_source[1], altdeg=light_source[0]) + else: + # Apply lighting to the face colors for shading + ls = LightSource(azdeg=azim, altdeg=elev) + + # Manually calculate the light vector since the 'light_vector' + # attribute is not accessible in some matplotlib versions. + az = np.radians(ls.azdeg) + alt = np.radians(ls.altdeg) + light_vec = np.array([ + np.cos(az) * np.cos(alt), + np.sin(az) * np.cos(alt), + np.sin(alt) + ]) + + # Calculate face normals + v0 = coords[faces[:, 0]] + v1 = coords[faces[:, 1]] + v2 = coords[faces[:, 2]] + face_normals = np.cross(v1 - v0, v2 - v0) + face_normals /= np.linalg.norm(face_normals, axis=1)[:, np.newaxis] + + # The shade is the dot product of the light vector and face normals + shade = np.dot(face_normals, light_vec) + + # Modulate the RGB colors by the shade, keeping the alpha channel + # Use np.clip to keep shade values between 0 and 1 + illuminated_rgb = face_colors[:, :3] * np.clip(shade, 0, 1)[:, np.newaxis] + + # Combine illuminated RGB with the original alpha channel + face_colors = np.hstack((illuminated_rgb, face_colors[:, 3:])) + p3dcollec.set_facecolors(face_colors) if not premade_ax: diff --git a/naplib/visualization/brain_plots.py b/naplib/visualization/brain_plots.py index 3c8c031..75c3f7c 100644 --- a/naplib/visualization/brain_plots.py +++ b/naplib/visualization/brain_plots.py @@ -90,7 +90,7 @@ def _view(hemi, mode: str = "lateral", backend: str = "mpl"): raise ValueError(f"Unknown `mode`: {mode}.") -def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin=None, vmax=None): +def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin=None, vmax=None, light_source=True): if isinstance(view, tuple): elev, azim = view else: @@ -108,14 +108,15 @@ def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin bg_alpha=hemi.sulc_alpha, ax=ax, vmin=vmin, - vmax=vmax + vmax=vmax, + light_source=light_source ) ax.axes.set_axis_off() ax.grid(False) def plot_brain_overlay( - brain, cmap="coolwarm", ax=None, hemi='both', view="best", vmin=None, vmax=None, cmap_quantile=1.0, threshold=None, **kwargs + brain, cmap="coolwarm", ax=None, hemi='both', view="best", vmin=None, vmax=None, cmap_quantile=1.0, threshold=None, light_source=True, **kwargs ): """ Plot brain overlay on the 3D cortical surface using matplotlib. @@ -150,6 +151,11 @@ def plot_brain_overlay( threshold : positive float, optional If given, then only values on the overlay which are less -threshold or greater than threshold will be shown. + light_source: None, bool, or tuple of int, optional + Whether to apply a light source for shading. If True, the light + source position is inferred from `elev` and `azim`. If a tuple of + (alt, az), these values will be used to specify the light source + position. If None or False, no shading is applied. Default is True. **kwargs : kwargs Any other kwargs to pass to matplotlib.pyplot.figure (such as figsize) @@ -217,9 +223,9 @@ def plot_brain_overlay( if ax[0] is not None: - _plot_hemi(brain.lh, cmap, ax[0], view=view, vmin=vmin, vmax=vmax, threshold=threshold) + _plot_hemi(brain.lh, cmap, ax[0], view=view, vmin=vmin, vmax=vmax, threshold=threshold, light_source=light_source) if ax[1] is not None: - _plot_hemi(brain.rh, cmap, ax[1], view=view, vmin=vmin, vmax=vmax, threshold=threshold) + _plot_hemi(brain.rh, cmap, ax[1], view=view, vmin=vmin, vmax=vmax, threshold=threshold, light_source=light_source) return fig, ax From 9bc396cedc63ab9e84c464e0338b58325891ea17 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:27:57 -0400 Subject: [PATCH 3/4] Including lightsource, keep original behavior --- naplib/localization/freesurfer.py | 2 +- naplib/visualization/brain_plots.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index eb89c27..03e6304 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -121,10 +121,10 @@ def __init__( try: self.sulc = read_morph_data(self.surf_file(f"{hemi}.sulc")) + self.sulc_alpha = 1.0 except Exception as e: logger.warning(f'No {hemi}.sulc file found. No sulcus information will be used.') self.sulc = None - self.sulc_alpha = 1.0 self.load_labels() diff --git a/naplib/visualization/brain_plots.py b/naplib/visualization/brain_plots.py index 75c3f7c..a9808a0 100644 --- a/naplib/visualization/brain_plots.py +++ b/naplib/visualization/brain_plots.py @@ -90,7 +90,7 @@ def _view(hemi, mode: str = "lateral", backend: str = "mpl"): raise ValueError(f"Unknown `mode`: {mode}.") -def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin=None, vmax=None, light_source=True): +def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin=None, vmax=None, light_source=False): if isinstance(view, tuple): elev, azim = view else: @@ -116,7 +116,7 @@ def _plot_hemi(hemi, cmap="coolwarm", ax=None, view="best", threshold=None, vmin def plot_brain_overlay( - brain, cmap="coolwarm", ax=None, hemi='both', view="best", vmin=None, vmax=None, cmap_quantile=1.0, threshold=None, light_source=True, **kwargs + brain, cmap="coolwarm", ax=None, hemi='both', view="best", vmin=None, vmax=None, cmap_quantile=1.0, threshold=None, light_source=False, **kwargs ): """ Plot brain overlay on the 3D cortical surface using matplotlib. From ffa3acc980de0de8aa54548ac3ffa9c633213757 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan Date: Mon, 1 Sep 2025 09:42:11 -0400 Subject: [PATCH 4/4] Always define sulc_alpha --- naplib/localization/freesurfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 03e6304..eb89c27 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -121,10 +121,10 @@ def __init__( try: self.sulc = read_morph_data(self.surf_file(f"{hemi}.sulc")) - self.sulc_alpha = 1.0 except Exception as e: logger.warning(f'No {hemi}.sulc file found. No sulcus information will be used.') self.sulc = None + self.sulc_alpha = 1.0 self.load_labels()