JDG Lab Notebook

TVB Surface Vizualizations with Matplotlib

I like pysurfer, but at this point in time mayavi is still painful to install and unreliable. So for a while now I have been in need of a fairly basic, but still reasonably pretty matplotlib-based brain surface plotting function. I've been using a few basic snippets for this for a while, but following some inspiration from MMW, I've written a more complete one and added it to tvb-library.

The following are some extended usage notes and examples:

Notebook setup

Enable matlab in the notebook

Define some variables

# define system-specific filepaths etc
%run ~/set_localenv_vars.py

nb_name = 'mpl_surface_viz'

# this is where I look for things
outdir = '%s/notebooks/%s' %(le['data_dir'],nb_name)

# this is where things actually live
outdir_loc = '%s/%s' %('/mnt/titania-hecuba/mcintosh_lab/john/Data/notebooks', nb_name)
import os; 
if not os.path.isdir(outdir_loc): os.makedirs(outdir_loc)
if not os.path.isdir(outdir): os.system('ln -s %s %s' %(outdir_loc,outdir))

        
# stuff for workdocs-cloudfiles

aws_key = 'drowssaperucesyreva'
aws_secret = '?teytidettopsuoyevah'  


# Analysis stuff

em_dir = le['code_dir'] + '/libraries_of_mine/github/eigenmodes'
tvb_lib_dir = le['code_dir'] + '/libraries_of_mine/github/tvb-library'
tvb_dat_dir = le['code_dir'] + '/libraries_of_mine/github/tvb-data'

Importage

# Generic stuff

import os,sys,inspect
from  scipy.io import loadmat,savemat


# Visualization stuff

%matplotlib inline
from matplotlib import cm,pyplot as plt
from IPython.display import Image,display as d

# Neuroimaging stuff

import nibabel as nib


# TVB stuff

nbso,nbse = sys.stdout,sys.stderr # hack part 1/2 to keep output printing properly
sys.path.append(tvb_lib_dir)
sys.path.append(tvb_dat_dir)
from tvb.simulator.lab import *
sys.stdout,sys.stderr = nbso,nbse  # ...hack part 2/2

from tvb.datatypes.surfaces import Surface
from tvb.simulator.plot.tools import (plot_surface_mpl,plot_surface_mpl_mv,
                                      get_rotation_matrix,get_combined_rotation_matrix)


# Workdocs-cloudfiles stuff

sys.path.append(le['ipynb_workdocs_dir'])
from new_utils import nb_fig,cloudfiles_nb

Initialize aws api and workdocs-cloudfiles folder

cnb = cloudfiles_nb('aws', [aws_key,aws_secret])
cnb.initialize_folder(nb_name)

Calico document tools

%%javascript
IPython.load_extensions('calico-spell-check', 'calico-document-tools',
                        'calico-cell-tools');

Go to output folder

os.chdir(outdir)

Ok, let's get cracking!

Plotting Function

First for completeness, let's show the full code of the main plotting functions:

print inspect.getsource(plot_surface_mpl)
def plot_surface_mpl(vtx,tri,data=None,rm=None,reorient='tvb',view='superior',
                     shaded=False,ax=None,figsize=(6,4), title=None,
                     lthr=None,uthr=None, nz_thr = 1E-20,
                     shade_kwargs = {'edgecolors': 'k', 'linewidth': 0.1,
                                     'alpha': None, 'cmap': 'coolwarm',
                                     'vmin': None, 'vmax': None}):
                        
  r"""Plot surfaces, surface patterns, and region patterns with matplotlib
    
  This is a general-use function for neuroimaging surface-based data, and 
  does not necessarily require construction of or interaction with tvb 
  datatypes. 

  See also:  plot_surface_mpl_mv



  Parameters
  ----------
  
  vtx           : N vertices x 3 array of surface vertex xyz coordinates 

  tri           : N faces x 3 array of surface faces

  data          : array of numbers to colour surface with. Can be either 
                  a pattern across surface vertices (N vertices x 1 array),
                  or a pattern across the surface's region mapping 
                  (N regions x 1 array), in which case the region mapping 
                  bust also be given as an argument. 
                  
  rm            : region mapping - N vertices x 1 array with (up to) N 
                  regions unique values; each element specifies which 
                  region the corresponding surface vertex is mapped to 

  reorient      : modify the vertex coordinate frame and/or orientation 
                  so that the same default rotations can subsequently be 
                  used for image views. The standard coordinate frame is 
                  xyz; i.e. first,second,third axis = left-right, 
                  front-back, and up-down, respectively. The standard 
                  starting orientation is axial view; i.e. looking down on
                  the brain in the x-y plane.
                  
                  Options: 

                    tvb (default)   : swaps the first 2 axes and applies a rotation
                                              
                    fs              : for the standard freesurfer (RAS) orientation; 
                                      e.g. fsaverage lh.orig. 
                                      No transformations needed for this; so is 
                                      gives same result as reorient=None

  view          : specify viewing angle. 
  
                  This can be done in one of two ways: by specifying a string 
                  corresponding to a standard viewing angle, or by providing 
                  a tuple or list of tuples detailing exact rotations to apply 
                  around each axis. 
                  
                  Standard view options are:
    
                  lh_lat / lh_med / rh_lat / rh_med / 
                  superior / inferior / posterior / anterior

                  (Note: if the surface contains both hemispheres, then medial 
                   surfaces will not be visible, so e.g. 'rh_med' will look the 
                   same as 'lh_lat')
                   
                  Arbitrary rotations can be specied by a tuple or a list of 
                  tuples, each with two elements, the first defining the axis 
                  to rotate around [0,1,2], the second specifying the angle in 
                  degrees. When a list is given the rotations are applied 
                  sequentially in the order given. 
                  
                  Example: rotations = [(0,45),(1,-45)] applies 45 degrees 
                  rotation around the first axis, followed by 45 degrees rotate 
                  around the second axis. 

  lthr/uthr     : lower/upper thresholds - set to zero any datapoints below / 
                  above these values
  
  nz_thr        : near-zero threshold - set to zero all datapoints with absolute 
                  values smaller than this number. Default is a very small 
                  number (1E-20), which unless your data has very small numbers, 
                  will only mask out actual zeros. 

  shade_kwargs  : dictionary specifiying shading options

                  Most relevant options (see matplotlib 'tripcolor' for full details):
                  
                    - 'shading'        (either 'gourand' or omit; 
                                        default is 'flat')
                    - 'edgecolors'     'k' = black is probably best
                    - 'linewidth'      0.1 works well; note that the visual 
                                       effect of this will depend on both the 
                                       surface density and the figure size 
                    - 'cmap'           colormap
                    - 'vmin'/'vmax'    scale colormap to these values
                    - 'alpha'          surface opacity
                  
  ax            : figure axis
  
  figsize       : figure size (ignore if ax provided)
  
  title         : text string to place above figure
  
  
  
                  
  Usage
  -----
       

  Basic freesurfer example:

  import nibabel as nib
  vtx,tri = nib.freesurfer.read_geometry('subjects/fsaverage/surf/lh.orig')
  plot_surface_mpl(vtx,tri,view='lh_lat',reorient='fs')



  Basic tvb example:
  
  ctx = cortex.Cortex.from_file(source_file = ctx_file,
                                region_mapping_file =rm_file)
  vtx,tri,rm = ctx.vertices,ctx.triangles,ctx.region_mapping
  conn = connectivity.Connectivity.from_file(conn_file); conn.configure()
  isrh_reg = conn.is_right_hemisphere(range(conn.number_of_regions))
  isrh_vtx = np.array([isrh_reg[r] for r in rm])
  dat = conn.tract_lengths[:,5]

  plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,view='inferior',title='inferior')

  fig, ax = plt.subplots()
  plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat, view=[(0,-90),(1,55)],ax=ax,
                   title='lh angle',shade_kwargs={'shading': 'gouraud', 'cmap': 'rainbow'})

   
  """
    
  # Copy things to make sure we don't modify things 
  # in the namespace inadvertently. 
    
  vtx,tri = vtx.copy(),tri.copy()
  if data is not None: data = data.copy()

  # 1. Set the viewing angle 
  
  if reorient == 'tvb':
    # The tvb default brain has coordinates in the order 
    # yxz for some reason. So first change that:   
    vtx = np.array([vtx[:,1],vtx[:,0],vtx[:,2]]).T.copy()
    
    # Also need to reflect in the x axis
    vtx[:,0]*=-1

  # (reorient == 'fs' is same as reorient=None; so not strictly needed
  #  but is included for clarity)
   


  # ...get rotations for standard view options
    
  if   view == 'lh_lat'    : rots =  [(0,-90),(1,90)  ]
  elif view == 'lh_med'    : rots =  [(0,-90),(1,-90) ] 
  elif view == 'rh_lat'    : rots =  [(0,-90),(1,-90) ]
  elif view == 'rh_med'    : rots =  [(0,-90),(1,90)  ]
  elif view == 'superior'  : rots =   None
  elif view == 'inferior'  : rots =   (1,180)
  elif view == 'anterior'  : rots =   (0,-90)
  elif view == 'posterior' : rots =  [(0, -90),(1,180)]
  elif (type(view) == tuple) or (type(view) == list): rots = view 

  # (rh_lat is the default 'view' argument because no rotations are 
  #  for that one; so if no view is specified when the function is called, 
  #  the 'rh_lat' option is chose here and the surface is shown 'as is' 
                            
                            
  # ...apply rotations                          
     
  if rots is None: rotmat = np.eye(3)
  else:            rotmat = get_combined_rotation_matrix(rots)
  vtx = np.dot(vtx,rotmat)

                                    
      
  # 2. Sort out the data
                                    
                                    
  # ...if no data is given, plot a vector of 1s. 
  #    if using region data, create corresponding surface vector 
  if data is None: 
    data = np.ones(vtx.shape[0]) 
  elif data.shape[0] != vtx.shape[0]: 
    data = np.array([data[r] for r in rm])
    
  # ...apply thresholds
  if uthr: data *= (data < uthr)
  if lthr: data *= (data > lthr)
  data *= (np.abs(data) > nz_thr)

                                    
  # 3. Create the surface triangulation object 
  
  x,y,z = vtx.T
  tx,ty,tz = vtx[tri].mean(axis=1).T
  tr = Triangulation(x,y,tri[np.argsort(tz)])
                
  # 4. Make the figure 

  if ax is None: fig, ax = plt.subplots(figsize=figsize)  
  
  #if shade = 'gouraud': shade_opts['shade'] = 
  tc = ax.tripcolor(tr, np.squeeze(data), **shade_kwargs)
                        
  ax.set_aspect('equal')
  ax.axis('off')
    
  if title is not None: ax.set_title(title)

print inspect.getsource(plot_surface_mpl_mv)
def plot_surface_mpl_mv(vtx=None,tri=None,data=None,rm=None,hemi=None,   # Option 1
                        vtx_lh=None,tri_lh=None,data_lh=None,rm_lh=None, # Option 2
                        vtx_rh=None,tri_rh=None,data_rh=None,rm_rh=None,
                        title=None,**kwargs):

  r"""Convenience wrapper on plot_surface_mpl for multiple views 
   
  This function calls plot_surface_mpl five times to give a complete 
  picture of a surface- or region-based spatial pattern. 

  As with plot_surface_mpl, this function is written so as to be 
  generally usable with neuroimaging surface-based data, and does not 
  require construction of of interaction with tvb datatype objects. 

  In order for the medial surfaces to be displayed properly, it is 
  necessary to separate the left and right hemispheres. This can be 
  done in one of two ways: 

  1. Provide single arrays for vertices, faces, data, and 
     region mappings, and addition provide arrays of indices for 
     each of these (vtx_inds,tr_inds,rm_inds) with 0/False 
     indicating left hemisphere vertices/faces/regions, and 1/True 
     indicating right hemisphere. 

     Note: this requires that 

  2. Provide separate vertices,faces,data,and region mappings for 
     each hemisphere (vtx_lh,tri_lh; vtx_rh,tri_rh,etc...)


 
  Parameters
  ----------

  (see also plot_surface_mpl parameters info for more details)

  (Option 1)

  vtx               :  surface vertices
 
  tri               : surface faces

  data              : spatial pattern to plot

  rm                : surface vertex to region mapping

  hemi              : hemisphere labels for each vertex
                      (1/True = right, 0/False = left) - 
      

  OR

  (Option 2)

  vtx_lh            : left hemisphere surface_vertices
  vtx_rh            : right ``      ``    ``     ``
  
  tri_lh            : left hemisphere surface faces 
  tri_rh            : right ``      ``    ``     ``

  data_lh          : left hemisphere surface_vertices
  data_rh          : right ``      ``    ``     ``

  rm_lh            : left hemisphere region_mapping
  rm_rh            : right ``      ``    ``     ``


  title            : title to show above middle plot
 
  kwargs           : additional tripcolor kwargs; see plot_surface_mpl

 

  Examples
  ----------

  # TVB default data

  # Plot one column of the region-based tract lengths 
  # connectivity matrix. The corresponding region is 
  # right auditory cortex ('rA1')

  ctx = cortex.Cortex.from_file(source_file = ctx_file,
                                region_mapping_file =rm_file)
  vtx,tri,rm = ctx.vertices,ctx.triangles,ctx.region_mapping
  conn = connectivity.Connectivity.from_file(conn_file); conn.configure()
  isrh_reg = conn.is_right_hemisphere(range(conn.number_of_regions))
  isrh_vtx = np.array([isrh_reg[r] for r in rm])
  dat = conn.tract_lengths[:,5]

  plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                      hemi=isrh_vtx,title=u'rA1 \ntract length')

  plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                    hemi=isrh_vtx,title=u'rA1 \ntract length',
                    shade_kwargs = {'shading': 'gouraud',
                                    'cmap': 'rainbow'}) 


  """
   

 
  if vtx is not None:                                    # Option 1
    tri_hemi = hemi[tri].any(axis=1)
    tri_lh,tri_rh = tri[tri_hemi==0],tri[tri_hemi==1]
  elif vtx_lh is not None:                               # Option 2
    vtx = np.vstack([vtx_lh,vtx_rh])
    tri = np.vstack([tri_lh,tri_rh+tri_lh.max()+1])

  if data_lh is not None:                                # Option 2
    data = np.hstack([data_lh,data_rh])
    
  if rm_lh is not None:                                  # Option 2 
    rm = np.hstack([rm_lh,rm_rh + rm_lh.max() + 1])
    
 

  # 2. Now do the plots for each view

  # (Note: for the single hemispheres we only need lh/rh arrays for the 
  #  faces (tri); the full vertices, region mapping, and data arrays
  #  can be given as arguments, they just won't be shown if they aren't 
  #  connected by the faces in tri )
  
  # LH lateral
  plot_surface_mpl(vtx,tri_lh,data=data,rm=rm,view='lh_lat',
                   ax=subplot(2,3,1),**kwargs)
    
  # LH medial
  plot_surface_mpl(vtx,tri_lh, data=data,rm=rm,view='lh_med',
                   ax=subplot(2,3,4),**kwargs)
    
  # RH lateral
  plot_surface_mpl(vtx,tri_rh, data=data,rm=rm,view='rh_lat',
                   ax=subplot(2,3,3),**kwargs)
    
  # RH medial
  plot_surface_mpl(vtx,tri_rh, data=data,rm=rm,view='rh_med',
                   ax=subplot(2,3,6),**kwargs)
    
  # Both superior
  plot_surface_mpl(vtx,tri, data=data,rm=rm,view='superior',
                   ax=subplot(1,3,2),title=title,**kwargs)
    
  plt.subplots_adjust(left=0.0, right=1.0, bottom=0.0,
                      top=1.0, wspace=0, hspace=0)    

print inspect.getsource(get_rotation_matrix)
def get_rotation_matrix(rotation_axis, deg):
    
  '''Return rotation matrix in the x,y,or z plane'''
   
  
 
  # (note make deg minus to change from anticlockwise to clockwise rotation)
  th = -deg * (pi/180) # convert degrees to radians
    
  if rotation_axis == 0:
    return np.array( [[    1,         0,         0    ],
                      [    0,      cos(th),   -sin(th)],
                      [    0,      sin(th),    cos(th)]])
  elif rotation_axis ==1:
    return np.array( [[   cos(th),    0,        sin(th)],
                      [    0,         1,          0    ],
                      [  -sin(th),    0,        cos(th)]])
  elif rotation_axis ==2:
    return np.array([[   cos(th),  -sin(th),     0    ],
                     [    sin(th),   cos(th),     0   ],
                     [     0,         0,          1   ]])

print inspect.getsource(get_combined_rotation_matrix)
def get_combined_rotation_matrix(rotations):
  '''Return a combined rotation matrix from a dictionary of rotations around 
     the x,y,or z axes'''
  rotmat = np.eye(3)
    
  if type(rotations) is tuple: rotations = [rotations] 
  for r in rotations:
    newrot = get_rotation_matrix(r[0],r[1])
    rotmat = np.dot(rotmat,newrot)
  return rotmat

Plotting generic brain surfaces

The functions work pretty well for generic surfaces such as those produced by freesurfer.

Load data

lho_file = '/software/freesurfer/subjects/fsaverage/surf/lh.orig'
lho_vtx,lho_tri = nib.freesurfer.read_geometry(lho_file)

rho_file = '/software/freesurfer/subjects/fsaverage/surf/rh.orig'
rho_vtx,rho_tri = nib.freesurfer.read_geometry(rho_file)

Simple example

# <!-- collapse=False-->

f = '%s/simple_fs_example.png' %outdir
# make figure

fig, ax = plt.subplots(nrows=2,ncols=2,figsize=(8,6))
plot_surface_mpl(lho_vtx,lho_tri,view='lh_lat',reorient='fs',title='lh lat',ax=ax[0,0])
plot_surface_mpl(lho_vtx,lho_tri,view='lh_med',reorient='fs',title='lh med',ax=ax[1,0])
plot_surface_mpl(rho_vtx,rho_tri,view='rh_lat',reorient='fs',title='rh lat',ax=ax[0,1])
plot_surface_mpl(rho_vtx,rho_tri,view='rh_med',reorient='fs',title='rh med',ax=ax[1,1])

plt.tight_layout()
plt.savefig(f, bbox_inches='tight')
plt.close()

# upload to cloud and embed link in notebook 
cap = ''; label = 'Simple freesurfer surface plotting example'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link Simple freesurfer surface plotting example.

A few notes on plotting freesurfer:

  • In general these are denser than the surface used in tvb, so the renders take a little longer to produce, and if the vertex lines are shown on the figures, they will look somewhat darker because denser surfaces have more vertex lines to plot.

TVB Surfaces

Load data

Most of the tvb default connectivities can generally be loaded without having to specify full file paths.

Going to load some of these by specifying the filenames anyway demonstrate the process:

ctx_file = tvb_dat_dir + '/tvb_data/surfaceData/cortex_16384.zip'
rm_file = tvb_dat_dir  + '/tvb_data/regionMapping/regionMapping_16k_76.bz2'
conn_file = tvb_dat_dir + '/tvb_data/connectivity/connectivity_76.zip'

ctx = cortex.Cortex.from_file(source_file = ctx_file,
                              region_mapping_file =rm_file)
vtx,tri,rm = ctx.vertices,ctx.triangles,ctx.region_mapping
conn = connectivity.Connectivity.from_file(conn_file); conn.configure()
isrh_reg = conn.is_right_hemisphere(range(conn.number_of_regions))
isrh_vtx = np.array([isrh_reg[r] for r in rm])
dat = conn.tract_lengths[:,0] # 'dat' is one column of the region-based tract lengths 
                              # connectivity matrix. The corresponding region is 
                              # right auditory cortex ('rA1')
# <!-- collapse=False-->

f = '%s/tvb_rA1_tract_lengths_multiview_1.png' %outdir
# make figure

plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                    hemi=isrh_vtx,title=u'rA1 \ntract length')

plt.savefig(f, bbox_inches='tight')
plt.close()

# upload to cloud and embed link in notebook 
cap = ''; label = 'TVB default data rA1 tract lengths multiview example 1'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link TVB default data rA1 tract lengths multiview example 1.
# <!-- collapse=False-->

f = '%s/tvb_rA1_tract_lengths_multiview_2.png' %outdir
# make figure

plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                    hemi=isrh_vtx,title=u'rA1 \ntract length',
                    shade_kwargs =  {'edgecolors': 'white',
                                     'linewidth': 0.1},
                    figsize=(20,20))

plt.savefig(f, bbox_inches='tight')
plt.close()

# upload to cloud and embed link in notebook 
cap = ''; label = 'TVB default data rA1 tract lengths multiview example 2'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link TVB default data rA1 tract lengths multiview example 2.
# <!-- collapse=False-->

f = '%s/tvb_rA1_tract_lengths_multiview_3.png' %outdir
# make figure

plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                    hemi=isrh_vtx,title=u'rA1 \ntract length',
                    shade_kwargs = {'shading': 'gouraud',
                                    'cmap': 'rainbow'})

plt.savefig(f, bbox_inches='tight')
plt.close()

# upload to cloud and embed link in notebook 
cap = ''; label = 'TVB default data rA1 tract lengths multiview example 3'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link TVB default data rA1 tract lengths multiview example 3.
# <!-- collapse=False-->

f = '%s/tvb_rA1_tract_lengths_multiview_4.png' %outdir
# make figure

plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                    hemi=isrh_vtx,title=u'ghostly!',
                    shade_kwargs = {'shading': 'flat',
                                    'cmap': 'rainbow',
                                    'edgecolors': 'white', 
                                    'linewidth': 0.3})
plt.savefig(f, bbox_inches='tight')
plt.close()

# upload to cloud and embed link in notebook 
cap = ''; label = 'TVB default data rA1 tract lengths multiview example 4'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link TVB default data rA1 tract lengths multiview example 4.

Some other rotations

# <!-- collapse=False-->

f = '%s/tvb_rA1_tract_lengths_other_rotations.png' %outdir
# make figure

fig, ax=  plt.subplots(ncols=3,nrows=2, figsize=(12,6))

plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,view='inferior',ax=ax[0][0],title='inferior')
plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,view='posterior',ax=ax[0][1], title='posterior')
plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,view='anterior',ax=ax[0][2], title='anterior')


plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,
                 view=[(0,-90),(1,55)],ax=ax[1][0],title='lh angle')

plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,
                 view=[(0,-90),(1,-25)],ax=ax[1][1],title='rh angle')

plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,
                 view=[(0,-110),(1,-90)],ax=ax[1][2], title='oops')


plt.tight_layout(); plt.savefig(f, bbox_inches='tight')
plt.close()

# upload to cloud and embed link in notebook 
cap = ''; label = 'TVB default data rA1 tract lengths - other rotations'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link TVB default data rA1 tract lengths - other rotations.

misc TVB