Repository: mne-tools/mne-python

Issue:
ENH: Convienance methods for labels
It would be good if `stc.extract_label_time_course` worked on stcs that are already confined to labels

Similarly it would be useful to have a method for putting derived measures back in to stc form for visualization.

Below is a use-case although more meaning full uses would come from `apply_inverse_raw` and `apply_inverse_epochs`. 

I don't know my way around labels and sourcespaces, but if someone would like to give me a quick break down on how these could be resolved I'd be happy to take a shot at it.


```
import mne
from mne.datasets import sample
from mne.minimum_norm import read_inverse_operator, apply_inverse

data_path = sample.data_path()
label = 'Aud-lh'
label_fname = data_path + '/MEG/sample/labels/%s.label' % label
fname_inv = data_path + '/MEG/sample/sample_audvis-meg-oct-6-meg-inv.fif'
fname_evoked = data_path + '/MEG/sample/sample_audvis-ave.fif'
subjects_dir = data_path + '/subjects'

snr = 3.0
lambda2 = 1.0 / snr ** 2
method = "dSPM"  # use dSPM method (could also be MNE or sLORETA)

# Load data
evoked = mne.read_evokeds(fname_evoked, condition=0, baseline=(None, 0))
inverse_operator = read_inverse_operator(fname_inv)
src = inverse_operator['src']

label = mne.read_label(label_fname)

# Compute inverse solution
pick_ori = "normal"  # Get signed values to see the effect of sign filp
stc = apply_inverse(evoked, inverse_operator, lambda2, method,
                      pick_ori=pick_ori, label=label)

stc_full = apply_inverse(evoked, inverse_operator, lambda2, method,
                         pick_ori=pick_ori)

stc_in_label = stc_full.in_label(label)

# these fails!
mean = stc.extract_label_time_course(label, src, mode='mean')
mean_2 = stc_in_label.extract_label_time_course(label, src, mode='mean')

# it would then be good to be able to put it (or some derived measure) back
stc_label = mne.label_data_to_stc(label, mean, src)

```


Discussion:
if label.hemi == 'lh' basically you need to find the intersection between label.vertices and src[0]['vertno'] then the matching vertices give you the vertices[0] to be passed to the SourceEstimate constructor.

you do the same for right hemi 'rh' and replace 0 by 1.

makes sense?
Are there any labels based on volume source spaces that I need to worry about?

not AFAIK or at least I never used such. For sure they don't have an
hemi attribute

> It would be good if stc.extract_label_time_course worked on stcs that are already confined to labels

Agreed these should work and give the same result (I'll look into it):
```
stc = apply_inverse(evoked, inverse_operator, lambda2, method,
                      pick_ori=pick_ori, label=label)
stc_full = apply_inverse(evoked, inverse_operator, lambda2, method,
                         pick_ori=pick_ori)
stc_in_label = stc_full.in_label(label)
mean = stc.extract_label_time_course(label, src, mode='mean')
mean_2 = stc_in_label.extract_label_time_course(label, src, mode='mean')
```

> Similarly it would be useful to have a method for putting derived measures back in to stc form for visualization.

We have this now with http://mne.tools/dev/generated/mne.labels_to_stc.html


Resolving patch:
diff --git a/mne/source_estimate.py b/mne/source_estimate.py
--- a/mne/source_estimate.py
+++ b/mne/source_estimate.py
@@ -5,6 +5,7 @@
 #
 # License: BSD (3-clause)
 
+import contextlib
 import copy
 import os.path as op
 import numpy as np
@@ -2496,22 +2497,6 @@ def _get_ico_tris(grade, verbose=None, return_surf=False):
         return ico
 
 
-def _get_label_flip(labels, label_vertidx, src):
-    """Get sign-flip for labels."""
-    # do the import here to avoid circular dependency
-    from .label import label_sign_flip
-    # get the sign-flip vector for every label
-    label_flip = list()
-    for label, vertidx in zip(labels, label_vertidx):
-        if vertidx is not None:
-            flip = label_sign_flip(label, src)[:, None]
-        else:
-            flip = None
-        label_flip.append(flip)
-
-    return label_flip
-
-
 def _pca_flip(flip, data):
     U, s, V = linalg.svd(data, full_matrices=False)
     # determine sign-flip
@@ -2529,37 +2514,39 @@ def _pca_flip(flip, data):
 }
 
 
-@verbose
-def _gen_extract_label_time_course(stcs, labels, src, mode='mean',
-                                   allow_empty=False, verbose=None):
-    """Generate extract_label_time_course."""
+@contextlib.contextmanager
+def _temporary_vertices(src, vertices):
+    orig_vertices = [s['vertno'] for s in src]
+    for s, v in zip(src, vertices):
+        s['vertno'] = v
+    try:
+        yield
+    finally:
+        for s, v in zip(src, orig_vertices):
+            s['vertno'] = v
+
+
+def _prepare_label_extraction(stc, labels, src, mode, allow_empty):
+    """Prepare indices and flips for extract_label_time_course."""
     # if src is a mixed src space, the first 2 src spaces are surf type and
     # the other ones are vol type. For mixed source space n_labels will be the
     # given by the number of ROIs of the cortical parcellation plus the number
     # of vol src space
-
-    if mode not in _label_funcs:
-        raise ValueError('%s is an invalid mode' % mode)
-    func = _label_funcs[mode]
-
-    if len(src) > 2:
-        if src[0]['type'] != 'surf' or src[1]['type'] != 'surf':
-            raise ValueError('The first 2 source spaces have to be surf type')
-        if any(np.any(s['type'] != 'vol') for s in src[2:]):
-            raise ValueError('source spaces have to be of vol type')
-
-        n_aparc = len(labels)
-        n_aseg = len(src[2:])
-        n_labels = n_aparc + n_aseg
-    else:
-        n_labels = len(labels)
+    from .label import label_sign_flip
 
     # get vertices from source space, they have to be the same as in the stcs
-    vertno = [s['vertno'] for s in src]
+    vertno = stc.vertices
     nvert = [len(vn) for vn in vertno]
 
     # do the initialization
     label_vertidx = list()
+    label_flip = list()
+    for s, v, hemi in zip(src, stc.vertices, ('left', 'right')):
+        n_missing = (~np.in1d(v, s['vertno'])).sum()
+        if n_missing:
+            raise ValueError('%d/%d %s hemisphere stc vertices missing from '
+                             'the source space, likely mismatch'
+                             % (n_missing, len(v), hemi))
     for label in labels:
         if label.hemi == 'both':
             # handle BiHemiLabel
@@ -2569,17 +2556,18 @@ def _gen_extract_label_time_course(stcs, labels, src, mode='mean',
         this_vertidx = list()
         for slabel in sub_labels:
             if slabel.hemi == 'lh':
-                this_vertno = np.intersect1d(vertno[0], slabel.vertices)
-                vertidx = np.searchsorted(vertno[0], this_vertno)
+                this_vertices = np.intersect1d(vertno[0], slabel.vertices)
+                vertidx = np.searchsorted(vertno[0], this_vertices)
             elif slabel.hemi == 'rh':
-                this_vertno = np.intersect1d(vertno[1], slabel.vertices)
-                vertidx = nvert[0] + np.searchsorted(vertno[1], this_vertno)
+                this_vertices = np.intersect1d(vertno[1], slabel.vertices)
+                vertidx = nvert[0] + np.searchsorted(vertno[1], this_vertices)
             else:
                 raise ValueError('label %s has invalid hemi' % label.name)
             this_vertidx.append(vertidx)
 
         # convert it to an array
         this_vertidx = np.concatenate(this_vertidx)
+        this_flip = None
         if len(this_vertidx) == 0:
             msg = ('source space does not contain any vertices for label %s'
                    % label.name)
@@ -2588,20 +2576,48 @@ def _gen_extract_label_time_course(stcs, labels, src, mode='mean',
             else:
                 warn_(msg + '. Assigning all-zero time series to label.')
             this_vertidx = None  # to later check if label is empty
+        elif mode not in ('mean', 'max'):  # mode-dependent initialization
+            # label_sign_flip uses two properties:
+            #
+            # - src[ii]['nn']
+            # - src[ii]['vertno']
+            #
+            # So if we override vertno with the stc vertices, it will pick
+            # the correct normals.
+            with _temporary_vertices(src, stc.vertices):
+                this_flip = label_sign_flip(label, src)[:, None]
 
         label_vertidx.append(this_vertidx)
+        label_flip.append(this_flip)
 
-    # mode-dependent initialization
-    if mode not in ('mean', 'max'):
-        # get the sign-flip vector for every label
-        src_flip = _get_label_flip(labels, label_vertidx, src[:2])
-    else:
-        src_flip = [None] * len(labels)
+    return label_vertidx, label_flip
 
+
+def _gen_extract_label_time_course(stcs, labels, src, mode='mean',
+                                   allow_empty=False, verbose=None):
     # loop through source estimates and extract time series
+    _check_option('mode', mode, sorted(_label_funcs.keys()))
+    func = _label_funcs[mode]
+    if len(src) > 2:
+        if src[0]['type'] != 'surf' or src[1]['type'] != 'surf':
+            raise ValueError('The first 2 source spaces have to be surf type')
+        if any(np.any(s['type'] != 'vol') for s in src[2:]):
+            raise ValueError('source spaces have to be of vol type')
+
+        n_aparc = len(labels)
+        n_aseg = len(src[2:])
+        n_labels = n_aparc + n_aseg
+    else:
+        n_labels = len(labels)
+    vertno = None
     for stc in stcs:
+        if vertno is None:
+            vertno = copy.deepcopy(stc.vertices)
+            nvert = [len(v) for v in vertno]
+            label_vertidx, src_flip = _prepare_label_extraction(
+                stc, labels, src, mode, allow_empty)
         # make sure the stc is compatible with the source space
-        for i in range(len(src)):
+        for i in range(len(vertno)):
             if len(stc.vertices[i]) != nvert[i]:
                 raise ValueError('stc not compatible with source space. '
                                  'stc has %s time series but there are %s '


Test patch:
diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py
--- a/mne/tests/test_source_estimate.py
+++ b/mne/tests/test_source_estimate.py
@@ -578,6 +578,27 @@ def test_extract_label_time_course():
     assert (x.size == 0)
 
 
+@testing.requires_testing_data
+def test_extract_label_time_course_equiv():
+    """Test extraction of label time courses from stc equivalences."""
+    label = read_labels_from_annot('sample', 'aparc', 'lh', regexp='transv',
+                                   subjects_dir=subjects_dir)
+    assert len(label) == 1
+    label = label[0]
+    inv = read_inverse_operator(fname_inv)
+    evoked = read_evokeds(fname_evoked, baseline=(None, 0))[0].crop(0, 0.01)
+    stc = apply_inverse(evoked, inv, pick_ori='normal', label=label)
+    stc_full = apply_inverse(evoked, inv, pick_ori='normal')
+    stc_in_label = stc_full.in_label(label)
+    mean = stc.extract_label_time_course(label, inv['src'])
+    mean_2 = stc_in_label.extract_label_time_course(label, inv['src'])
+    assert_allclose(mean, mean_2)
+    inv['src'][0]['vertno'] = np.array([], int)
+    assert len(stc_in_label.vertices[0]) == 22
+    with pytest.raises(ValueError, match='22/22 left hemisphere.*missing'):
+        stc_in_label.extract_label_time_course(label, inv['src'])
+
+
 def _my_trans(data):
     """FFT that adds an additional dimension by repeating result."""
     data_t = fft(data)