1313import numpy as np
1414from scipy .stats import zscore
1515
16+ from .freesurfer import FSIGNORE , _decode_list
17+
1618
1719def _grid_communities (communities ):
1820 """
@@ -29,9 +31,13 @@ def _grid_communities(communities):
2931 Boundaries of communities
3032 """
3133
34+ communities = np .asarray (communities )
35+ if 0 in communities :
36+ communities = communities + 1
37+
3238 comm = communities [np .argsort (communities )]
3339 bounds = []
34- for i in range ( 1 , np .max (comm ) + 1 ):
40+ for i in np .unique (comm ):
3541 ind = np .where (comm == i )
3642 if len (ind ) > 0 :
3743 bounds .append (np .min (ind ))
@@ -58,8 +64,9 @@ def sort_communities(consensus, communities):
5864 Index array for sorting `consensus`
5965 """
6066
67+ communities = np .asarray (communities )
6168 if 0 in communities :
62- communities += 1
69+ communities = communities + 1
6370
6471 bounds = _grid_communities (communities )
6572 inds = np .argsort (communities )
@@ -322,12 +329,48 @@ def plot_conte69(data, lhlabel, rhlabel, surf='midthickness',
322329 return lhplot , rhplot
323330
324331
325- def plot_fsaverage (data , * , lhannot , rhannot , order = 'LR' , surf = 'pial' ,
326- views = 'lat' , vmin = None , vmax = None , center = None , mask = None ,
327- colormap = 'viridis' , colorbar = True , alpha = 0.8 ,
328- label_fmt = '%.2f' , num_labels = 3 , size_per_view = 500 ,
329- subject_id = 'fsaverage' , subjects_dir = None ,
330- noplot = None , data_kws = None , ** kwargs ):
332+ def _get_fs_subjid (subject_id , subjects_dir = None ):
333+ """
334+ Gets fsaverage version `subject_id`, fetching if required
335+
336+ Parameters
337+ ----------
338+ subject_id : str
339+ FreeSurfer subject ID
340+ subjects_dir : str, optional
341+ Path to FreeSurfer subject directory. If not set, will inherit from
342+ the environmental variable $SUBJECTS_DIR. Default: None
343+
344+ Returns
345+ -------
346+ subject_id : str
347+ FreeSurfer subject ID
348+ subjects_dir : str
349+ Path to subject directory with `subject_id`
350+ """
351+
352+ from netneurotools .utils import check_fs_subjid
353+
354+ # check for FreeSurfer install w/fsaverage; otherwise, fetch required
355+ try :
356+ subject_id , subjects_dir = check_fs_subjid (subject_id , subjects_dir )
357+ except FileNotFoundError :
358+ if 'fsaverage' not in subject_id :
359+ raise ValueError ('Provided subject {} does not exist in provided '
360+ 'subjects_dir {}'
361+ .format (subject_id , subjects_dir ))
362+ from netneurotools .datasets import fetch_fsaverage
363+ from netneurotools .datasets .utils import _get_data_dir
364+ fetch_fsaverage (subject_id )
365+ subjects_dir = os .path .join (_get_data_dir (), 'tpl-fsaverage' )
366+ subject_id , subjects_dir = check_fs_subjid (subject_id , subjects_dir )
367+
368+ return subject_id , subjects_dir
369+
370+
371+ def plot_fsaverage (data , * , lhannot , rhannot , order = 'lr' , mask = None ,
372+ noplot = None , subject_id = 'fsaverage' , subjects_dir = None ,
373+ vmin = None , vmax = None , ** kwargs ):
331374 """
332375 Plots `data` to fsaverage brain using `annot` as parcellation
333376
@@ -346,6 +389,121 @@ def plot_fsaverage(data, *, lhannot, rhannot, order='LR', surf='pial',
346389 order : str, optional
347390 Order of the hemispheres in the data vector (either 'LR' or 'RL').
348391 Default: 'LR'
392+ mask : (N,) array_like, optional
393+ Binary array where entries indicate whether values in `data` should be
394+ masked from plotting (True = mask; False = show). Default: None
395+ noplot : list, optional
396+ List of names in `lhannot` and `rhannot` to not plot. It is assumed
397+ these are NOT present in `data`. By default 'unknown' and
398+ 'corpuscallosum' will never be plotted if they are present in the
399+ provided annotation files. Default: None
400+ subject_id : str, optional
401+ Subject ID to use; must be present in `subjects_dir`. Default:
402+ 'fsaverage'
403+ subjects_dir : str, optional
404+ Path to FreeSurfer subject directory. If not set, will inherit from
405+ the environmental variable $SUBJECTS_DIR. Default: None
406+ vmin : float, optional
407+ Minimum value for colorbar. If not provided, a robust estimation will
408+ be used from values in `data`. Default: None
409+ vmax : float, optional
410+ Maximum value for colorbar. If not provided, a robust estimation will
411+ be used from values in `data`. Default: None
412+ kwargs : key-value pairs
413+ Provided directly to :func:`~.plot_fsvertex` without modification.
414+
415+ Returns
416+ -------
417+ brain : surfer.Brain
418+ Plotted PySurfer brain
419+ """
420+
421+ subject_id , subjects_dir = _get_fs_subjid (subject_id , subjects_dir )
422+
423+ # cast data to float (required for NaNs)
424+ data = np .asarray (data , dtype = 'float' )
425+
426+ order = order .lower ()
427+ if order not in ('lr' , 'rl' ):
428+ raise ValueError ('order must be either \' lr\' or \' rl\' ' )
429+
430+ if mask is not None and len (mask ) != len (data ):
431+ raise ValueError ('Provided mask must be the same length as data.' )
432+
433+ if vmin is None :
434+ vmin = np .nanpercentile (data , 2.5 )
435+ if vmax is None :
436+ vmax = np .nanpercentile (data , 97.5 )
437+
438+ # parcels that should not be included in parcellation
439+ drop = FSIGNORE .copy ()
440+ if noplot is not None :
441+ if isinstance (noplot , str ):
442+ noplot = [noplot ]
443+ drop += list (noplot )
444+ drop = _decode_list (drop )
445+
446+ vtx_data = []
447+ for annot , hemi in zip ((lhannot , rhannot ), ('lh' , 'rh' )):
448+ # loads annotation data for hemisphere, including vertex `labels`!
449+ if not annot .startswith (os .path .abspath (os .sep )):
450+ annot = os .path .join (subjects_dir , subject_id , 'label' , annot )
451+ labels , ctab , names = nib .freesurfer .read_annot (annot )
452+ names = _decode_list (names )
453+
454+ # get appropriate data, accounting for hemispheric asymmetry
455+ currdrop = np .intersect1d (drop , names )
456+ if hemi == 'lh' :
457+ if order == 'lr' :
458+ split_id = len (names ) - len (currdrop )
459+ ldata , rdata = np .split (data , [split_id ])
460+ if mask is not None :
461+ lmask , rmask = np .split (mask , [split_id ])
462+ elif order == 'rl' :
463+ split_id = len (data ) - len (names ) + len (currdrop )
464+ rdata , ldata = np .split (data , [split_id ])
465+ if mask is not None :
466+ rmask , lmask = np .split (mask , [split_id ])
467+ hemidata = ldata if hemi == 'lh' else rdata
468+
469+ # our `data` don't include the "ignored" parcels but our `labels` do,
470+ # so we need to account for that. find the label ids that correspond to
471+ # those and set them to NaN in the `data vector`
472+ inds = sorted ([names .index (n ) for n in currdrop ])
473+ for i in inds :
474+ hemidata = np .insert (hemidata , i , np .nan )
475+ vtx = hemidata [labels ]
476+
477+ # let's also mask data, if necessary
478+ if mask is not None :
479+ maskdata = lmask if hemi == 'lh' else rmask
480+ maskdata = np .insert (maskdata , inds - np .arange (len (inds )), np .nan )
481+ vtx [maskdata [labels ] > 0 ] = np .nan
482+
483+ vtx_data .append (vtx )
484+
485+ brain = plot_fsvertex (np .hstack (vtx_data ), order = order , mask = None ,
486+ subject_id = subject_id , subjects_dir = subjects_dir ,
487+ vmin = vmin , vmax = vmax , ** kwargs )
488+
489+ return brain
490+
491+
492+ def plot_fsvertex (data , * , order = 'lr' , surf = 'pial' , views = 'lat' ,
493+ vmin = None , vmax = None , center = None , mask = None ,
494+ colormap = 'viridis' , colorbar = True , alpha = 0.8 ,
495+ label_fmt = '%.2f' , num_labels = 3 , size_per_view = 500 ,
496+ subject_id = 'fsaverage' , subjects_dir = None , data_kws = None ,
497+ ** kwargs ):
498+ """
499+ Plots vertex-wise `data` to fsaverage brain.
500+
501+ Parameters
502+ ----------
503+ data : (N,) array_like
504+ Data for `N` parcels as defined in `annot`
505+ order : {'lr', 'rl'}, optional
506+ Order of the hemispheres in the data vector. Default: 'lr'
349507 surf : str, optional
350508 Surface on which to plot data. Default: 'pial'
351509 views : str or list, optional
@@ -379,11 +537,6 @@ def plot_fsaverage(data, *, lhannot, rhannot, order='LR', surf='pial',
379537 subject : str, optional
380538 Subject ID to use; must be present in `subjects_dir`. Default:
381539 'fsaverage'
382- noplot : list, optional
383- List of names in `lhannot` and `rhannot` to not plot. It is assumed
384- these are NOT present in `data`. By default 'unknown' and
385- 'corpuscallosum' will never be plotted if they are present in the
386- provided annotation files. Default: None
387540 data_kws : dict, optional
388541 Keyword arguments for Brain.add_data()
389542
@@ -394,26 +547,13 @@ def plot_fsaverage(data, *, lhannot, rhannot, order='LR', surf='pial',
394547 """
395548
396549 # hold off on imports until
397- from .utils import check_fs_subjid
398550 try :
399551 from surfer import Brain
400552 except ImportError :
401553 raise ImportError ('Cannot use plot_fsaverage() if pysurfer is not '
402554 'installed. Please install pysurfer and try again.' )
403555
404- # check for FreeSurfer install w/fsaverage; otherwise, fetch required
405- try :
406- subject_id , subjects_dir = check_fs_subjid (subject_id , subjects_dir )
407- except FileNotFoundError :
408- if subject_id != 'fsaverage' :
409- raise ValueError ('Provided subject {} does not exist in provided '
410- 'subjects_dir {}'
411- .format (subject_id , subjects_dir ))
412- from .datasets import fetch_fsaverage
413- from .datasets .utils import _get_data_dir
414- fetch_fsaverage ()
415- subjects_dir = os .path .join (_get_data_dir (), 'tpl-fsaverage' )
416- subject_id , subjects_dir = check_fs_subjid (subject_id , subjects_dir )
556+ subject_id , subjects_dir = _get_fs_subjid (subject_id , subjects_dir )
417557
418558 # cast data to float (required for NaNs)
419559 data = np .asarray (data , dtype = 'float' )
@@ -422,80 +562,46 @@ def plot_fsaverage(data, *, lhannot, rhannot, order='LR', surf='pial',
422562 if data_kws is None :
423563 data_kws = {}
424564
425- if order not in ['LR' , 'RL' ]:
426- raise ValueError ('order must be either \' LR\' or \' RL\' ' )
427-
428565 if mask is not None and len (mask ) != len (data ):
429566 raise ValueError ('Provided mask must be the same length as data.' )
430567
568+ order = order .lower ()
569+ if order not in ['lr' , 'rl' ]:
570+ raise ValueError ('Specified order must be either \' lr\' or \' rl\' ' )
571+
431572 if vmin is None :
432- vmin = np .percentile (data , 2.5 )
573+ vmin = np .nanpercentile (data , 2.5 )
433574 if vmax is None :
434- vmax = np .percentile (data , 97.5 )
435-
436- # parcels that should not be included in parcellation
437- drop = [b'unknown' , b'corpuscallosum' ]
438- if noplot is not None :
439- if isinstance (noplot , str ):
440- noplot = [noplot ]
441- drop += list (noplot )
575+ vmax = np .nanpercentile (data , 97.5 )
442576
443577 # set up brain views
444578 if not isinstance (views , (np .ndarray , list )):
445579 views = [views ]
446580
447581 # size of window will depend on # of views provided
448582 size = (size_per_view * 2 , size_per_view * len (views ))
449- brain_kws = dict (background = 'white' )
583+ brain_kws = dict (background = 'white' , size = size )
450584 brain_kws .update (** kwargs )
451585 brain = Brain (subject_id = subject_id , hemi = 'split' , surf = surf ,
452- subjects_dir = subjects_dir , views = views , size = size ,
453- ** brain_kws )
454-
455- for annot , hemi in zip ([lhannot , rhannot ], ['lh' , 'rh' ]):
456- # loads annotation data for hemisphere, including vertex `labels`!
457- if not annot .startswith (os .path .abspath (os .sep )):
458- annot = os .path .join (subjects_dir , subject_id , 'label' , annot )
459- labels , ctab , names = nib .freesurfer .read_annot (annot )
460-
461- # get appropriate data, accounting for hemispheric asymmetry
462- currdrop = np .intersect1d (drop , names )
463- if hemi == 'lh' :
464- if order == 'LR' :
465- splitID = len (names ) - len (currdrop )
466- ldata , rdata = np .split (data , [splitID ])
467- if mask is not None :
468- lmask , rmask = np .split (mask , [splitID ])
469- elif order == 'RL' :
470- splitID = len (data ) - len (names ) + len (currdrop )
471- rdata , ldata = np .split (data , [splitID ])
472- if mask is not None :
473- rmask , lmask = np .split (mask , [splitID ])
474- hemidata = ldata if hemi == 'lh' else rdata
475-
476- # our `data` don't include unknown / corpuscallosum, but our `labels`
477- # do, so we need to account for that
478- # find the label ids that correspond to those and set them to NaN in
479- # the `data vector`
480- inds = sorted ([names .index (n ) for n in currdrop ])
481- for i in inds :
482- hemidata = np .insert (hemidata , i , np .nan )
483- vtx_data = hemidata [labels ]
484- not_nan = ~ np .isnan (vtx_data )
586+ subjects_dir = subjects_dir , views = views , ** brain_kws )
485587
486- # we don't want the NaN vertices (unknown / corpuscallosum) plotted
487- # let's drop those and set a threshold so they're hidden
488- thresh = vtx_data [not_nan ].min () - 1
489- vtx_data [np .isnan (vtx_data )] = thresh
490- # let's also mask data, if necessary
588+ hemis = ('lh' , 'rh' ) if order == 'lr' else ('rh' , 'lh' )
589+ for n , (hemi , vtx_data ) in enumerate (zip (hemis , np .split (data , 2 ))):
590+ # let's mask data, if necessary
491591 if mask is not None :
492- maskdata = lmask if hemi == 'lh' else rmask
493- maskdata = np .insert (maskdata , inds - np .arange (len (inds )), np .nan )
494- vtx_data [maskdata [labels ] > 0 ] = thresh
592+ maskdata = np .asarray (np .split (mask , 2 )[n ], dtype = bool )
593+ vtx_data [maskdata ] = np .nan
594+
595+ # we don't want NaN values plotted so set a threshold if they exist
596+ thresh , nanmask = None , np .isnan (vtx_data )
597+ if np .any (nanmask ) > 0 :
598+ thresh = np .nanmin (vtx_data ) - 1
599+ vtx_data [nanmask ] = thresh
600+ thresh += 0.5
495601
496602 # finally, add data to this hemisphere!
497603 brain .add_data (vtx_data , vmin , vmax , hemi = hemi , mid = center ,
498- thresh = thresh + 0.5 , alpha = 1.0 , remove_existing = False ,
604+ thresh = thresh , alpha = 1.0 , remove_existing = False ,
499605 colormap = colormap , colorbar = colorbar , verbose = False ,
500606 ** data_kws )
501607
0 commit comments