danjgale / surfplot

A package for publication-ready brain surface figures
https://surfplot.readthedocs.io/en/latest/
Other
53 stars 14 forks source link

`TypeError` when using custom cmap in `Plot.add_layer()` #12

Closed alyssadai closed 1 year ago

alyssadai commented 2 years ago

Hi Dan,

Thanks for your great work on surfplot!

I am trying to plot a custom parcellation following Tutorial 6, using a custom LinearSegmentedColormap for the regions, but am encountering an error on the build() step. I am using JupyterLab 3.2.1 locally with Python 3.8.3 and matplotlib 3.5.0.

Relevant part of my code:

from surfplot import Plot
from matplotlib.colors import LinearSegmentedColormap

colors = ['#3588D1', '#92A654', '#7053D9', '#FA7922', '#D725A3', '#8E6041',
       '#E8A2DE', '#D8A06C']
cmap = LinearSegmentedColormap.from_list('NMF_k8', colors, N=8)

p = Plot(surf_lh = lh_gray, surf_rh = rh_gray)
p.add_layer({'left': k8_parc_L, 'right': k8_parc_R}, cmap = cmap, cbar=False)
fig = p.build()

Error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_23008/2633573736.py in <module>
      1 p = Plot(surf_lh = lh_gray, surf_rh = rh_gray)
      2 p.add_layer({'left': k8_parc_L, 'right': k8_parc_R}, cmap = cmap, cbar=False)
----> 3 fig = p.build()

~\anaconda3\lib\site-packages\surfplot\plotting.py in build(self, figsize, colorbar, cbar_kws, scale)
    496             Surface plot figure
    497         """
--> 498         p = self.render()
    499         p._check_offscreen()
    500         x = p.to_numpy(transparent_bg=True, scale=scale)

~\anaconda3\lib\site-packages\surfplot\plotting.py in render(self, offscreen)
    389             color_range = [crange]
    390 
--> 391         return plot_surf(surfs=self.surfaces, layout=hemi_layout,
    392                          array_name=names, cmap=cmap, color_bar=False,
    393                          color_range=color_range, view=view_layout,

~\anaconda3\lib\site-packages\surfplot\surf.py in plot_surf(surfs, layout, array_name, view, color_bar, color_range, share, label_text, cmap, nan_color, zoom, background, size, embed_nb, interactive, scale, transparent_bg, screenshot, filename, return_plotter, **kwargs)
    429         kwargs.update({'offscreen': True})
    430 
--> 431     p = build_plotter(surfs, layout, array_name=array_name, view=view,
    432                       color_bar=color_bar, color_range=color_range,
    433                       share=share, label_text=label_text, cmap=cmap,

~\anaconda3\lib\site-packages\surfplot\surf.py in build_plotter(surfs, layout, array_name, view, color_bar, color_range, share, label_text, cmap, nan_color, zoom, background, size, **kwargs)
    284             cm = cmap[i, j][ia]
    285             if cm is not None:
--> 286                 if cm in colormaps:
    287                     table = colormaps[cm]
    288                 else:

TypeError: unhashable type: 'LinearSegmentedColormap'

Any thoughts on what went wrong/how to troubleshoot this would be much appreciated!

danjgale commented 2 years ago

Hi @alyssadai, thanks for sharing this! I can replicate this with matplotlib 3.5.0, but not the minimum required matplotlib version (3.4.2; this is also the version for the tutorial builds). This is with python 3.9.7, and the only thing changing is indeed the matplotlib version. So, I am guessing that something changed regarding LinearSegmentedColormap in more recent matplotlib versions.

I can investigate this further and how to handle it -- matplotlib versioning is a known issue already (see #5). For now, though, could you try downgrading your matplotlib version to 3.4.2? This is likely the best immediate fix while the underlying issue gets solved!

alyssadai commented 2 years ago

Hi Dan, thanks for the info! Downgrading to matplotlib 3.4.2 does indeed allow me to now use custom colormaps; matplotlib 3.4.3 seems to work as well.

A follow up question on plotting parcellations. I'm not sure if this is expected behaviour, but I noticed that when trying to plot a layer of parcel values only (without region outlines) some of the region borders take on a different color than the rest of the parcel. e.g., (in the central sulcus below there actually seems to be two adjacent outlines, one blue and one bright green) civet_parc_test_hsv

Code to replicate above figure: (I am also able to replicate the result using flsr surfaces and Schaefer400)

# Python 3.8.3

import nibabel as nib # 3.2.1
from surfplot import Plot
import numpy as np # 1.21.2
import matplotlib.colors as mcolors # matplotlib 3.4.2
from neuromaps.datasets import fetch_civet
from brainspace.datasets import load_parcellation

surfaces = fetch_civet()
lh, rh = surfaces['inflated']
print('left', nib.load(lh).darrays[0].dims) # 40962

dkt_L = np.loadtxt("../PS_Dimensions/CIVET_2.1.0_dkt_left_short.txt")
dkt_R = np.loadtxt("../PS_Dimensions/CIVET_2.1.0_dkt_right_short.txt")
print(len(dkt_L)) # 40962

p = Plot(surf_lh = lh, views='lateral', size=(1000,800))
p.add_layer(dkt_L, cmap="hsv", as_outline=False, cbar=False)

fig = p.build()

Here is the left DKT parcellation file, for reference: CIVET_2.1.0_dkt_left_short.txt

The erroneous region outlines persist for these data when I try to plot >=2 ROIs; in addition, it seems like using a custom LinearSegmentedColormap or ListedColormap for ROIs places a (I suppose unsurprising) constraint on the numerical distance between the selected region numbers, where, if the region numbers aren't equally spaced, not all the colors in the colormap are used: civet_dkt_3reg_test

Code to replicate above figure:

region_numbers = [3,4,23]
regions_L = np.where(np.isin(dkt_L, region_numbers), dkt_L, 0)
regions_R = np.where(np.isin(dkt_R, region_numbers), dkt_R, 0)

colors = ['green','red','yellow']
cmap_reg = mcolors.LinearSegmentedColormap.from_list('regions', colors, N=3)

p = Plot(surf_lh = lh_gray, views='lateral', size=(1000,800))
p.add_layer(regions_L, cmap=cmap_reg, as_outline=False, cbar=False)
fig = p.build()

For now I can get around this linear spacing issue by replacing the ROI numbers with random consecutive numbers in the parcellation array (that aren't used in the original region-to-number mapping), but I'm still unsure of how to debug the different-color outlines. Any thoughts on what might be causing this?

Sorry for the long reply--let me know also if this would work better in a separate issue!

danjgale commented 2 years ago

Is it possible that the parcellation array accidentally includes slight overlap between adjacent regions? For example, in your last image, could it be that the red strip is equal to the sum of the green and yellow region (7)? I've seen this before when I was customizing a parcellation at one point and accidentally had overlapping regions.

Edit: Nvm, looking more closely at the code, a sum of adjacent regions isn't possible.

danjgale commented 2 years ago

What was your code to replicate this with fs_LR surfaces and Schaefer parcellations?

alyssadai commented 2 years ago

Hi Dan,

Here's the code that reproduced the different-color borders with fsLR + Schaefer for me:

import nibabel as nib
from surfplot import Plot
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

from neuromaps.datasets import fetch_fslr
from brainspace.datasets import load_parcellation

surfaces = fetch_fslr()
lh, rh = surfaces['inflated']
lh_parc, rh_parc = load_parcellation('schaefer')

p = Plot(lh, views='lateral', size=(1000,800))
p.add_layer(lh_parc, cmap='hsv', cbar=False) # I also see the borders with viridis and Spectral, but I think they're most obvious with hsv given the wider color range

fig = p.build()

The corresponding plot: fsLR_schaefer_parc_test Let me know if you are able to replicate this on your end, or spot an error anywhere.

danjgale commented 2 years ago

Hmm, I'm a bit stumped on this one. I can reproduce these examples, and just noticed some cases in my own plots. Admittedly, I didn't notice them before because I've been mostly plotting with perceptually linear colormaps using fairly autocorrelated data, so it did a good job masking these adjacent-region-effects. Or, I've been using borders/outlines.

Probing further, it seems a) consistent across different surfaces and b) to be happening at the rendering stage before it's embedded in matplotlib, which would suggest a brainspace issue. I'll investigate this further throughout this week

hptaylor commented 2 years ago

Is there a way to provide an array of RGB values of size (N_vert x 3) to directly color points on the surface? I have been trying to create a custom colormap using a list of 10242 RGB tuples and the ListedColormap function, but cant seem to get it to work quite right.