Skip to content

Commit a1beaed

Browse files
authored
Merge pull request #95 from rmarkello/plot_vertex
[ENH] Creates plot_fsvertex() function
2 parents afa3af3 + bbe22db commit a1beaed

File tree

5 files changed

+281
-93
lines changed

5 files changed

+281
-93
lines changed

.github/workflows/tests.yml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
python-version: 3.8
4949
install: setup
5050
check: test
51-
optional-depends: numba
51+
optional-depends: 'numba pyqt5 mayavi pysurfer'
5252
- os: ubuntu-latest
5353
python-version: 3.8
5454
install: sdist
@@ -69,21 +69,22 @@ jobs:
6969
CHECK_TYPE: ${{ matrix.check }}
7070
OPTIONAL_DEPENDS: ${{ matrix.optional-depends }}
7171
steps:
72-
- uses: actions/checkout@v2
72+
- name: Checkout code
73+
uses: actions/checkout@v2
7374
with:
7475
submodules: recursive
7576
fetch-depth: 0
7677
- name: Set up Python ${{ matrix.python-version }}
7778
uses: actions/setup-python@v2
7879
with:
79-
python-version: ${{ matrix.python-version }}
80-
- name: 'Display Python version'
80+
python-version: ${{ matrix.python-version }}
81+
- name: Display Python version
8182
run: python -c "import sys; print(sys.version)"
82-
- name: 'Install dependencies'
83+
- name: Install dependencies
8384
run: ./tools/install_dependencies.sh
84-
- name: 'Install netneurotools'
85+
- name: Install netneurotools
8586
run: ./tools/install_package.sh
86-
- name: 'Run tests'
87+
- name: Run tests
8788
run: ./tools/run_checks.sh
8889
- uses: codecov/codecov-action@v1
8990
with:

netneurotools/plotting.py

Lines changed: 188 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import numpy as np
1414
from scipy.stats import zscore
1515

16+
from .freesurfer import FSIGNORE, _decode_list
17+
1618

1719
def _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

Comments
 (0)