forked from mne-tools/mne-python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlabel.py
2119 lines (1828 loc) · 78.3 KB
/
label.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Authors: Alexandre Gramfort <[email protected]>
# Martin Luessi <[email protected]>
# Denis Engemann <[email protected]>
#
# License: BSD (3-clause)
from collections import defaultdict
from colorsys import hsv_to_rgb, rgb_to_hsv
from os import path as op
import os
import copy as cp
import re
import numpy as np
from scipy import linalg, sparse
from .fixes import digitize, in1d
from .utils import (get_subjects_dir, _check_subject, logger, verbose, warn,
_check_copy_dep)
from .source_estimate import (morph_data, SourceEstimate, _center_of_mass,
spatial_src_connectivity)
from .source_space import add_source_space_distances
from .surface import read_surface, fast_cross_3d, mesh_edges, mesh_dist
from .source_space import SourceSpaces
from .parallel import parallel_func, check_n_jobs
from .stats.cluster_level import _find_clusters, _get_components
from .externals.six import b, string_types
from .externals.six.moves import zip, xrange
def _blend_colors(color_1, color_2):
"""Blend two colors in HSV space
Parameters
----------
color_1, color_2 : None | tuple
RGBA tuples with values between 0 and 1. None if no color is available.
If both colors are None, the output is None. If only one is None, the
output is the other color.
Returns
-------
color : None | tuple
RGBA tuple of the combined color. Saturation, value and alpha are
averaged, whereas the new hue is determined as angle half way between
the two input colors' hues.
"""
if color_1 is None and color_2 is None:
return None
elif color_1 is None:
return color_2
elif color_2 is None:
return color_1
r_1, g_1, b_1, a_1 = color_1
h_1, s_1, v_1 = rgb_to_hsv(r_1, g_1, b_1)
r_2, g_2, b_2, a_2 = color_2
h_2, s_2, v_2 = rgb_to_hsv(r_2, g_2, b_2)
hue_diff = abs(h_1 - h_2)
if hue_diff < 0.5:
h = min(h_1, h_2) + hue_diff / 2.
else:
h = max(h_1, h_2) + (1. - hue_diff) / 2.
h %= 1.
s = (s_1 + s_2) / 2.
v = (v_1 + v_2) / 2.
r, g, b = hsv_to_rgb(h, s, v)
a = (a_1 + a_2) / 2.
color = (r, g, b, a)
return color
def _split_colors(color, n):
"""Create n colors in HSV space that occupy a gradient in value
Parameters
----------
color : tuple
RGBA tuple with values between 0 and 1.
n : int >= 2
Number of colors on the gradient.
Returns
-------
colors : tuple of tuples, len = n
N RGBA tuples that occupy a gradient in value (low to high) but share
saturation and hue with the input color.
"""
r, g, b, a = color
h, s, v = rgb_to_hsv(r, g, b)
gradient_range = np.sqrt(n / 10.)
if v > 0.5:
v_max = min(0.95, v + gradient_range / 2)
v_min = max(0.05, v_max - gradient_range)
else:
v_min = max(0.05, v - gradient_range / 2)
v_max = min(0.95, v_min + gradient_range)
hsv_colors = ((h, s, v_) for v_ in np.linspace(v_min, v_max, n))
rgb_colors = (hsv_to_rgb(h_, s_, v_) for h_, s_, v_ in hsv_colors)
rgba_colors = ((r_, g_, b_, a,) for r_, g_, b_ in rgb_colors)
return tuple(rgba_colors)
def _n_colors(n, bytes_=False, cmap='hsv'):
"""Produce a list of n unique RGBA color tuples based on a colormap
Parameters
----------
n : int
Number of colors.
bytes : bool
Return colors as integers values between 0 and 255 (instead of floats
between 0 and 1).
cmap : str
Which colormap to use.
Returns
-------
colors : array, shape (n, 4)
RGBA color values.
"""
n_max = 2 ** 10
if n > n_max:
raise NotImplementedError("Can't produce more than %i unique "
"colors" % n_max)
from matplotlib.cm import get_cmap
cm = get_cmap(cmap, n_max)
pos = np.linspace(0, 1, n, False)
colors = cm(pos, bytes=bytes_)
if bytes_:
# make sure colors are unique
for ii, c in enumerate(colors):
if np.any(np.all(colors[:ii] == c, 1)):
raise RuntimeError('Could not get %d unique colors from %s '
'colormap. Try using a different colormap.'
% (n, cmap))
return colors
class Label(object):
"""A FreeSurfer/MNE label with vertices restricted to one hemisphere
Labels can be combined with the ``+`` operator:
* Duplicate vertices are removed.
* If duplicate vertices have conflicting position values, an error
is raised.
* Values of duplicate vertices are summed.
Parameters
----------
vertices : array (length N)
vertex indices (0 based).
pos : array (N by 3) | None
locations in meters. If None, then zeros are used.
values : array (length N) | None
values at the vertices. If None, then ones are used.
hemi : 'lh' | 'rh'
Hemisphere to which the label applies.
comment : str
Kept as information but not used by the object itself.
name : str
Kept as information but not used by the object itself.
filename : str
Kept as information but not used by the object itself.
subject : str | None
Name of the subject the label is from.
color : None | matplotlib color
Default label color and alpha (e.g., ``(1., 0., 0., 1.)`` for red).
verbose : bool, str, int, or None
If not None, override default verbose level (see mne.verbose).
Attributes
----------
color : None | tuple
Default label color, represented as RGBA tuple with values between 0
and 1.
comment : str
Comment from the first line of the label file.
hemi : 'lh' | 'rh'
Hemisphere.
name : None | str
A name for the label. It is OK to change that attribute manually.
pos : array, shape = (n_pos, 3)
Locations in meters.
subject : str | None
Subject name. It is best practice to set this to the proper
value on initialization, but it can also be set manually.
values : array, len = n_pos
Values at the vertices.
verbose : bool, str, int, or None
See above.
vertices : array, len = n_pos
Vertex indices (0 based)
"""
@verbose
def __init__(self, vertices, pos=None, values=None, hemi=None, comment="",
name=None, filename=None, subject=None, color=None,
verbose=None):
# check parameters
if not isinstance(hemi, string_types):
raise ValueError('hemi must be a string, not %s' % type(hemi))
vertices = np.asarray(vertices)
if np.any(np.diff(vertices.astype(int)) <= 0):
raise ValueError('Vertices must be ordered in increasing order.')
if color is not None:
from matplotlib.colors import colorConverter
color = colorConverter.to_rgba(color)
if values is None:
values = np.ones(len(vertices))
else:
values = np.asarray(values)
if pos is None:
pos = np.zeros((len(vertices), 3))
else:
pos = np.asarray(pos)
if not (len(vertices) == len(values) == len(pos)):
raise ValueError("vertices, values and pos need to have same "
"length (number of vertices)")
# name
if name is None and filename is not None:
name = op.basename(filename[:-6])
self.vertices = vertices
self.pos = pos
self.values = values
self.hemi = hemi
self.comment = comment
self.verbose = verbose
self.subject = _check_subject(None, subject, False)
self.color = color
self.name = name
self.filename = filename
def __setstate__(self, state):
self.vertices = state['vertices']
self.pos = state['pos']
self.values = state['values']
self.hemi = state['hemi']
self.comment = state['comment']
self.verbose = state['verbose']
self.subject = state.get('subject', None)
self.color = state.get('color', None)
self.name = state['name']
self.filename = state['filename']
def __getstate__(self):
out = dict(vertices=self.vertices,
pos=self.pos,
values=self.values,
hemi=self.hemi,
comment=self.comment,
verbose=self.verbose,
subject=self.subject,
color=self.color,
name=self.name,
filename=self.filename)
return out
def __repr__(self):
name = 'unknown, ' if self.subject is None else self.subject + ', '
name += repr(self.name) if self.name is not None else "unnamed"
n_vert = len(self)
return "<Label | %s, %s : %i vertices>" % (name, self.hemi, n_vert)
def __len__(self):
return len(self.vertices)
def __add__(self, other):
if isinstance(other, BiHemiLabel):
return other + self
elif isinstance(other, Label):
if self.subject != other.subject:
raise ValueError('Label subject parameters must match, got '
'"%s" and "%s". Consider setting the '
'subject parameter on initialization, or '
'setting label.subject manually before '
'combining labels.' % (self.subject,
other.subject))
if self.hemi != other.hemi:
name = '%s + %s' % (self.name, other.name)
if self.hemi == 'lh':
lh, rh = self.copy(), other.copy()
else:
lh, rh = other.copy(), self.copy()
color = _blend_colors(self.color, other.color)
return BiHemiLabel(lh, rh, name, color)
else:
raise TypeError("Need: Label or BiHemiLabel. Got: %r" % other)
# check for overlap
duplicates = np.intersect1d(self.vertices, other.vertices)
n_dup = len(duplicates)
if n_dup:
self_dup = [np.where(self.vertices == d)[0][0]
for d in duplicates]
other_dup = [np.where(other.vertices == d)[0][0]
for d in duplicates]
if not np.all(self.pos[self_dup] == other.pos[other_dup]):
err = ("Labels %r and %r: vertices overlap but differ in "
"position values" % (self.name, other.name))
raise ValueError(err)
isnew = np.array([v not in duplicates for v in other.vertices])
vertices = np.hstack((self.vertices, other.vertices[isnew]))
pos = np.vstack((self.pos, other.pos[isnew]))
# find position of other's vertices in new array
tgt_idx = [np.where(vertices == v)[0][0] for v in other.vertices]
n_self = len(self.values)
n_other = len(other.values)
new_len = n_self + n_other - n_dup
values = np.zeros(new_len, dtype=self.values.dtype)
values[:n_self] += self.values
values[tgt_idx] += other.values
else:
vertices = np.hstack((self.vertices, other.vertices))
pos = np.vstack((self.pos, other.pos))
values = np.hstack((self.values, other.values))
indcs = np.argsort(vertices)
vertices, pos, values = vertices[indcs], pos[indcs, :], values[indcs]
comment = "%s + %s" % (self.comment, other.comment)
name0 = self.name if self.name else 'unnamed'
name1 = other.name if other.name else 'unnamed'
name = "%s + %s" % (name0, name1)
color = _blend_colors(self.color, other.color)
verbose = self.verbose or other.verbose
label = Label(vertices, pos, values, self.hemi, comment, name, None,
self.subject, color, verbose)
return label
def __sub__(self, other):
if isinstance(other, BiHemiLabel):
if self.hemi == 'lh':
return self - other.lh
else:
return self - other.rh
elif isinstance(other, Label):
if self.subject != other.subject:
raise ValueError('Label subject parameters must match, got '
'"%s" and "%s". Consider setting the '
'subject parameter on initialization, or '
'setting label.subject manually before '
'combining labels.' % (self.subject,
other.subject))
else:
raise TypeError("Need: Label or BiHemiLabel. Got: %r" % other)
if self.hemi == other.hemi:
keep = in1d(self.vertices, other.vertices, True, invert=True)
else:
keep = np.arange(len(self.vertices))
name = "%s - %s" % (self.name or 'unnamed', other.name or 'unnamed')
return Label(self.vertices[keep], self.pos[keep], self.values[keep],
self.hemi, self.comment, name, None, self.subject,
self.color, self.verbose)
def save(self, filename):
"""Write to disk as FreeSurfer \*.label file
Parameters
----------
filename : string
Path to label file to produce.
Notes
-----
Note that due to file specification limitations, the Label's subject
and color attributes are not saved to disk.
"""
write_label(filename, self)
def copy(self):
"""Copy the label instance.
Returns
-------
label : instance of Label
The copied label.
"""
return cp.deepcopy(self)
def fill(self, src, name=None):
"""Fill the surface between sources for a label defined in source space
Parameters
----------
src : SourceSpaces
Source space in which the label was defined. If a source space is
provided, the label is expanded to fill in surface vertices that
lie between the vertices included in the source space. For the
added vertices, ``pos`` is filled in with positions from the
source space, and ``values`` is filled in from the closest source
space vertex.
name : None | str
Name for the new Label (default is self.name).
Returns
-------
label : Label
The label covering the same vertices in source space but also
including intermediate surface vertices.
"""
# find source space patch info
if self.hemi == 'lh':
hemi_src = src[0]
elif self.hemi == 'rh':
hemi_src = src[1]
if not np.all(in1d(self.vertices, hemi_src['vertno'])):
msg = "Source space does not contain all of the label's vertices"
raise ValueError(msg)
nearest = hemi_src['nearest']
if nearest is None:
warn("Computing patch info for source space, this can take "
"a while. In order to avoid this in the future, run "
"mne.add_source_space_distances() on the source space "
"and save it.")
add_source_space_distances(src)
nearest = hemi_src['nearest']
# find new vertices
include = in1d(nearest, self.vertices, False)
vertices = np.nonzero(include)[0]
# values
nearest_in_label = digitize(nearest[vertices], self.vertices, True)
values = self.values[nearest_in_label]
# pos
pos = hemi_src['rr'][vertices]
if name is None:
name = self.name
label = Label(vertices, pos, values, self.hemi, self.comment, name,
None, self.subject, self.color)
return label
@verbose
def smooth(self, subject=None, smooth=2, grade=None,
subjects_dir=None, n_jobs=1, copy=None, verbose=None):
"""Smooth the label
Useful for filling in labels made in a
decimated source space for display.
Parameters
----------
subject : str | None
The name of the subject used. If None, the value will be
taken from self.subject.
smooth : int
Number of iterations for the smoothing of the surface data.
Cannot be None here since not all vertices are used. For a
grade of 5 (e.g., fsaverage), a smoothing of 2 will fill a
label.
grade : int, list (of two arrays), array, or None
Resolution of the icosahedral mesh (typically 5). If None, all
vertices will be used (potentially filling the surface). If a list,
values will be morphed to the set of vertices specified in grade[0]
and grade[1], assuming that these are vertices for the left and
right hemispheres. Note that specifying the vertices (e.g.,
grade=[np.arange(10242), np.arange(10242)] for fsaverage on a
standard grade 5 source space) can be substantially faster than
computing vertex locations. If one array is used, it is assumed
that all vertices belong to the hemisphere of the label. To create
a label filling the surface, use None.
subjects_dir : string, or None
Path to SUBJECTS_DIR if it is not set in the environment.
n_jobs : int
Number of jobs to run in parallel
copy : bool
This parameter has been deprecated and will be removed in 0.14.
Use inst.copy() instead.
Whether to return a new instance or modify in place.
verbose : bool, str, int, or None
If not None, override default verbose level (see mne.verbose).
Defaults to self.verbose.
Returns
-------
label : instance of Label
The smoothed label.
Notes
-----
This function will set label.pos to be all zeros. If the positions
on the new surface are required, consider using mne.read_surface
with label.vertices.
"""
subject = _check_subject(self.subject, subject)
return self.morph(subject, subject, smooth, grade, subjects_dir,
n_jobs, copy=copy)
@verbose
def morph(self, subject_from=None, subject_to=None, smooth=5, grade=None,
subjects_dir=None, n_jobs=1, copy=None, verbose=None):
"""Morph the label
Useful for transforming a label from one subject to another.
Parameters
----------
subject_from : str | None
The name of the subject of the current label. If None, the
initial subject will be taken from self.subject.
subject_to : str
The name of the subject to morph the label to. This will
be put in label.subject of the output label file.
smooth : int
Number of iterations for the smoothing of the surface data.
Cannot be None here since not all vertices are used.
grade : int, list (of two arrays), array, or None
Resolution of the icosahedral mesh (typically 5). If None, all
vertices will be used (potentially filling the surface). If a list,
values will be morphed to the set of vertices specified in grade[0]
and grade[1], assuming that these are vertices for the left and
right hemispheres. Note that specifying the vertices (e.g.,
``grade=[np.arange(10242), np.arange(10242)]`` for fsaverage on a
standard grade 5 source space) can be substantially faster than
computing vertex locations. If one array is used, it is assumed
that all vertices belong to the hemisphere of the label. To create
a label filling the surface, use None.
subjects_dir : string, or None
Path to SUBJECTS_DIR if it is not set in the environment.
n_jobs : int
Number of jobs to run in parallel.
copy : bool
This parameter has been deprecated and will be removed in 0.14.
Use inst.copy() instead.
Whether to return a new instance or modify in place.
verbose : bool, str, int, or None
If not None, override default verbose level (see mne.verbose).
Returns
-------
label : instance of Label
The morphed label.
Notes
-----
This function will set label.pos to be all zeros. If the positions
on the new surface are required, consider using `mne.read_surface`
with `label.vertices`.
"""
subject_from = _check_subject(self.subject, subject_from)
if not isinstance(subject_to, string_types):
raise TypeError('"subject_to" must be entered as a string')
if not isinstance(smooth, int):
raise TypeError('smooth must be an integer')
if np.all(self.values == 0):
raise ValueError('Morphing label with all zero values will result '
'in the label having no vertices. Consider using '
'something like label.values.fill(1.0).')
if(isinstance(grade, np.ndarray)):
if self.hemi == 'lh':
grade = [grade, np.array([], int)]
else:
grade = [np.array([], int), grade]
if self.hemi == 'lh':
vertices = [self.vertices, np.array([], int)]
else:
vertices = [np.array([], int), self.vertices]
data = self.values[:, np.newaxis]
stc = SourceEstimate(data, vertices, tmin=1, tstep=1,
subject=subject_from)
stc = morph_data(subject_from, subject_to, stc, grade=grade,
smooth=smooth, subjects_dir=subjects_dir,
warn=False, n_jobs=n_jobs)
inds = np.nonzero(stc.data)[0]
label = _check_copy_dep(self, copy)
label.values = stc.data[inds, :].ravel()
label.pos = np.zeros((len(inds), 3))
if label.hemi == 'lh':
label.vertices = stc.vertices[0][inds]
else:
label.vertices = stc.vertices[1][inds]
label.subject = subject_to
return label
def split(self, parts=2, subject=None, subjects_dir=None,
freesurfer=False):
"""Split the Label into two or more parts
Parameters
----------
parts : int >= 2 | tuple of str | str
Number of labels to create (default is 2), or tuple of strings
specifying label names for new labels (from posterior to anterior),
or 'contiguous' to split the label into connected components.
If a number or 'contiguous' is specified, names of the new labels
will be the input label's name with div1, div2 etc. appended.
subject : None | str
Subject which this label belongs to (needed to locate surface file;
should only be specified if it is not specified in the label).
subjects_dir : None | str
Path to SUBJECTS_DIR if it is not set in the environment.
freesurfer : bool
By default (``False``) ``split_label`` uses an algorithm that is
slightly optimized for performance and numerical precision. Set
``freesurfer`` to ``True`` in order to replicate label splits from
FreeSurfer's ``mris_divide_parcellation``.
Returns
-------
labels : list of Label (len = n_parts)
The labels, starting from the lowest to the highest end of the
projection axis.
Notes
-----
If using 'contiguous' split, you must ensure that the label being split
uses the same triangular resolution as the surface mesh files in
``subjects_dir`` Also, some small fringe labels may be returned that
are close (but not connected) to the large components.
The spatial split finds the label's principal eigen-axis on the
spherical surface, projects all label vertex coordinates onto this
axis, and divides them at regular spatial intervals.
"""
if isinstance(parts, string_types) and parts == 'contiguous':
return _split_label_contig(self, subject, subjects_dir)
elif isinstance(parts, (tuple, int)):
return split_label(self, parts, subject, subjects_dir, freesurfer)
else:
raise ValueError("Need integer, tuple of strings, or string "
"('contiguous'). Got %s)" % type(parts))
def get_vertices_used(self, vertices=None):
"""Get the source space's vertices inside the label
Parameters
----------
vertices : ndarray of int, shape (n_vertices,) | None
The set of vertices to compare the label to. If None, equals to
``np.arange(10242)``. Defaults to None.
Returns
-------
label_verts : ndarray of in, shape (n_label_vertices,)
The vertices of the label corresponding used by the data.
"""
if vertices is None:
vertices = np.arange(10242)
label_verts = vertices[in1d(vertices, self.vertices)]
return label_verts
def get_tris(self, tris, vertices=None):
"""Get the source space's triangles inside the label
Parameters
----------
tris : ndarray of int, shape (n_tris, 3)
The set of triangles corresponding to the vertices in a
source space.
vertices : ndarray of int, shape (n_vertices,) | None
The set of vertices to compare the label to. If None, equals to
``np.arange(10242)``. Defaults to None.
Returns
-------
label_tris : ndarray of int, shape (n_tris, 3)
The subset of tris used by the label
"""
vertices_ = self.get_vertices_used(vertices)
selection = np.all(in1d(tris, vertices_).reshape(tris.shape),
axis=1)
label_tris = tris[selection]
if len(np.unique(label_tris)) < len(vertices_):
logger.info('Surprising label structure. Trying to repair '
'triangles.')
dropped_vertices = np.setdiff1d(vertices_, label_tris)
n_dropped = len(dropped_vertices)
assert n_dropped == (len(vertices_) - len(np.unique(label_tris)))
# put missing vertices as extra zero-length triangles
add_tris = (dropped_vertices +
np.zeros((len(dropped_vertices), 3), dtype=int).T)
label_tris = np.r_[label_tris, add_tris.T]
assert len(np.unique(label_tris)) == len(vertices_)
return label_tris
def center_of_mass(self, subject=None, restrict_vertices=False,
subjects_dir=None, surf='sphere'):
"""Compute the center of mass of the label
This function computes the spatial center of mass on the surface
as in [1]_.
Parameters
----------
subject : string | None
The subject the label is defined for.
restrict_vertices : bool | array of int | instance of SourceSpaces
If True, returned vertex will be one from the label. Otherwise,
it could be any vertex from surf. If an array of int, the
returned vertex will come from that array. If instance of
SourceSpaces (as of 0.13), the returned vertex will be from
the given source space. For most accuruate estimates, do not
restrict vertices.
subjects_dir : str, or None
Path to the SUBJECTS_DIR. If None, the path is obtained by using
the environment variable SUBJECTS_DIR.
surf : str
The surface to use for Euclidean distance center of mass
finding. The default here is "sphere", which finds the center
of mass on the spherical surface to help avoid potential issues
with cortical folding.
Returns
-------
vertex : int
Vertex of the spatial center of mass for the inferred hemisphere,
with each vertex weighted by its label value.
See Also
--------
SourceEstimate.center_of_mass
vertex_to_mni
Notes
-----
.. versionadded: 0.13
References
----------
.. [1] Larson and Lee, "The cortical dynamics underlying effective
switching of auditory spatial attention", NeuroImage 2012.
"""
if not isinstance(surf, string_types):
raise TypeError('surf must be a string, got %s' % (type(surf),))
subject = _check_subject(self.subject, subject)
if np.any(self.values < 0):
raise ValueError('Cannot compute COM with negative values')
vertex = _center_of_mass(self.vertices, self.values, self.hemi, surf,
subject, subjects_dir, restrict_vertices)
return vertex
class BiHemiLabel(object):
"""A freesurfer/MNE label with vertices in both hemispheres
Parameters
----------
lh : Label
Label for the left hemisphere.
rh : Label
Label for the right hemisphere.
name : None | str
name for the label
color : None | matplotlib color
Label color and alpha (e.g., ``(1., 0., 0., 1.)`` for red).
Note that due to file specification limitations, the color isn't saved
to or loaded from files written to disk.
Attributes
----------
lh : Label
Label for the left hemisphere.
rh : Label
Label for the right hemisphere.
name : None | str
A name for the label. It is OK to change that attribute manually.
subject : str | None
Subject the label is from.
"""
def __init__(self, lh, rh, name=None, color=None):
if lh.subject != rh.subject:
raise ValueError('lh.subject (%s) and rh.subject (%s) must '
'agree' % (lh.subject, rh.subject))
self.lh = lh
self.rh = rh
self.name = name
self.subject = lh.subject
self.color = color
self.hemi = 'both'
def __repr__(self):
temp = "<BiHemiLabel | %s, lh : %i vertices, rh : %i vertices>"
name = 'unknown, ' if self.subject is None else self.subject + ', '
name += repr(self.name) if self.name is not None else "unnamed"
return temp % (name, len(self.lh), len(self.rh))
def __len__(self):
return len(self.lh) + len(self.rh)
def __add__(self, other):
if isinstance(other, Label):
if other.hemi == 'lh':
lh = self.lh + other
rh = self.rh
else:
lh = self.lh
rh = self.rh + other
elif isinstance(other, BiHemiLabel):
lh = self.lh + other.lh
rh = self.rh + other.rh
else:
raise TypeError("Need: Label or BiHemiLabel. Got: %r" % other)
name = '%s + %s' % (self.name, other.name)
color = _blend_colors(self.color, other.color)
return BiHemiLabel(lh, rh, name, color)
def __sub__(self, other):
if isinstance(other, Label):
if other.hemi == 'lh':
lh = self.lh - other
rh = self.rh
else:
rh = self.rh - other
lh = self.lh
elif isinstance(other, BiHemiLabel):
lh = self.lh - other.lh
rh = self.rh - other.rh
else:
raise TypeError("Need: Label or BiHemiLabel. Got: %r" % other)
if len(lh.vertices) == 0:
return rh
elif len(rh.vertices) == 0:
return lh
else:
name = '%s - %s' % (self.name, other.name)
return BiHemiLabel(lh, rh, name, self.color)
def read_label(filename, subject=None, color=None):
"""Read FreeSurfer Label file
Parameters
----------
filename : string
Path to label file.
subject : str | None
Name of the subject the data are defined for.
It is good practice to set this attribute to avoid combining
incompatible labels and SourceEstimates (e.g., ones from other
subjects). Note that due to file specification limitations, the
subject name isn't saved to or loaded from files written to disk.
color : None | matplotlib color
Default label color and alpha (e.g., ``(1., 0., 0., 1.)`` for red).
Note that due to file specification limitations, the color isn't saved
to or loaded from files written to disk.
Returns
-------
label : Label
Instance of Label object with attributes:
- ``comment``: comment from the first line of the label file
- ``vertices``: vertex indices (0 based, column 1)
- ``pos``: locations in meters (columns 2 - 4 divided by 1000)
- ``values``: values at the vertices (column 5)
See Also
--------
read_labels_from_annot
"""
if subject is not None and not isinstance(subject, string_types):
raise TypeError('subject must be a string')
# find hemi
basename = op.basename(filename)
if basename.endswith('lh.label') or basename.startswith('lh.'):
hemi = 'lh'
elif basename.endswith('rh.label') or basename.startswith('rh.'):
hemi = 'rh'
else:
raise ValueError('Cannot find which hemisphere it is. File should end'
' with lh.label or rh.label')
# find name
if basename.startswith(('lh.', 'rh.')):
basename_ = basename[3:]
if basename.endswith('.label'):
basename_ = basename[:-6]
else:
basename_ = basename[:-9]
name = "%s-%s" % (basename_, hemi)
# read the file
with open(filename, 'r') as fid:
comment = fid.readline().replace('\n', '')[1:]
nv = int(fid.readline())
data = np.empty((5, nv))
for i, line in enumerate(fid):
data[:, i] = line.split()
# let's make sure everything is ordered correctly
vertices = np.array(data[0], dtype=np.int32)
pos = 1e-3 * data[1:4].T
values = data[4]
order = np.argsort(vertices)
vertices = vertices[order]
pos = pos[order]
values = values[order]
label = Label(vertices, pos, values, hemi, comment, name, filename,
subject, color)
return label
@verbose
def write_label(filename, label, verbose=None):
"""Write a FreeSurfer label
Parameters
----------
filename : string
Path to label file to produce.
label : Label
The label object to save.
verbose : bool, str, int, or None
If not None, override default verbose level (see mne.verbose).
Notes
-----
Note that due to file specification limitations, the Label's subject and
color attributes are not saved to disk.
See Also
--------
write_labels_to_annot
"""
hemi = label.hemi
path_head, name = op.split(filename)
if name.endswith('.label'):
name = name[:-6]
if not (name.startswith(hemi) or name.endswith(hemi)):
name += '-' + hemi
filename = op.join(path_head, name) + '.label'
logger.info('Saving label to : %s' % filename)
with open(filename, 'wb') as fid:
n_vertices = len(label.vertices)
data = np.zeros((n_vertices, 5), dtype=np.float)
data[:, 0] = label.vertices
data[:, 1:4] = 1e3 * label.pos
data[:, 4] = label.values
fid.write(b("#%s\n" % label.comment))
fid.write(b("%d\n" % n_vertices))
for d in data:
fid.write(b("%d %f %f %f %f\n" % tuple(d)))
return label
def _prep_label_split(label, subject=None, subjects_dir=None):
"""Helper to get label and subject information prior to label spliting"""
# If necessary, find the label
if isinstance(label, BiHemiLabel):
raise TypeError("Can only split labels restricted to one hemisphere.")
elif isinstance(label, string_types):
label = read_label(label)
# Find the subject
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
if label.subject is None and subject is None:
raise ValueError("The subject needs to be specified.")
elif subject is None:
subject = label.subject
elif label.subject is None:
pass
elif subject != label.subject:
raise ValueError("The label specifies a different subject (%r) from "
"the subject parameter (%r)."
% label.subject, subject)
return label, subject, subjects_dir
def _split_label_contig(label_to_split, subject=None, subjects_dir=None):
"""Split label into contiguous regions (i.e., connected components)
Parameters
----------
label_to_split : Label | str