-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathlabel.py
2980 lines (2595 loc) · 98 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: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import copy as cp
import os
import os.path as op
import re
from collections import defaultdict
from colorsys import hsv_to_rgb, rgb_to_hsv
import numpy as np
from scipy import linalg, sparse
from .fixes import _safe_svd
from .morph_map import read_morph_map
from .parallel import parallel_func
from .source_estimate import (
SourceEstimate,
VolSourceEstimate,
_center_of_mass,
extract_label_time_course,
spatial_src_adjacency,
)
from .source_space._source_space import (
SourceSpaces,
_ensure_src,
add_source_space_distances,
)
from .stats.cluster_level import _find_clusters, _get_components
from .surface import (
_mesh_borders,
complete_surface_info,
fast_cross_3d,
mesh_dist,
mesh_edges,
read_surface,
)
from .utils import (
_check_fname,
_check_option,
_check_subject,
_validate_type,
check_random_state,
fill_doc,
get_subjects_dir,
logger,
verbose,
warn,
)
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.0
else:
h = max(h_1, h_2) + (1.0 - hue_diff) / 2.0
h %= 1.0
s = (s_1 + s_2) / 2.0
v = (v_1 + v_2) / 2.0
r, g, b = hsv_to_rgb(h, s, v)
a = (a_1 + a_2) / 2.0
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.0)
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(f"Can't produce more than {n_max} unique colors.")
from .viz.utils import _get_cmap
cm = _get_cmap(cmap)
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(
f"Could not get {n} unique colors from {cmap} "
"colormap. Try using a different colormap."
)
return colors
@fill_doc
class Label:
"""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, shape (N,)
Vertex indices (0 based).
pos : array, shape (N, 3) | None
Locations in meters. If None, then zeros are used.
values : array, shape (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_label)s
color : None | matplotlib color
Default label color and alpha (e.g., ``(1., 0., 0., 1.)`` for red).
%(verbose)s
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, 3)
Locations in meters.
subject : str | None
The label subject.
It is best practice to set this to the proper
value on initialization, but it can also be set manually.
values : array, shape (N,)
Values at the vertices.
vertices : array, shape (N,)
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, str):
raise ValueError(f"hemi must be a string, not {type(hemi)}")
vertices = np.asarray(vertices, int)
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.subject = _check_subject(None, subject, raise_error=False)
self.color = color
self.name = name
self.filename = filename
def __setstate__(self, state): # noqa: D105
self.vertices = state["vertices"]
self.pos = state["pos"]
self.values = state["values"]
self.hemi = state["hemi"]
self.comment = state["comment"]
self.subject = state.get("subject", None)
self.color = state.get("color", None)
self.name = state["name"]
self.filename = state["filename"]
def __getstate__(self): # noqa: D105
out = dict(
vertices=self.vertices,
pos=self.pos,
values=self.values,
hemi=self.hemi,
comment=self.comment,
subject=self.subject,
color=self.color,
name=self.name,
filename=self.filename,
)
return out
def __repr__(self): # noqa: D105
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 f"<Label | {name}, {self.hemi} : {n_vert} vertices>"
def __len__(self):
"""Return the number of vertices.
Returns
-------
n_vertices : int
The number of vertices.
"""
return len(self.vertices)
def __add__(self, other):
"""Add Labels."""
_validate_type(other, (Label, BiHemiLabel), "other")
if isinstance(other, BiHemiLabel):
return other + self
else: # isinstance(other, Label)
if self.subject != other.subject:
raise ValueError(
"Label subject parameters must match, got "
f'"{self.subject}" and "{other.subject}". Consider setting the '
"subject parameter on initialization, or "
"setting label.subject manually before "
"combining labels."
)
if self.hemi != other.hemi:
name = f"{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)
# 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 = (
f"Labels {repr(self.name)} and {repr(other.name)}: vertices "
"overlap but differ in position values"
)
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 = f"{self.comment} + {other.comment}"
name0 = self.name if self.name else "unnamed"
name1 = other.name if other.name else "unnamed"
name = f"{name0} + {name1}"
color = _blend_colors(self.color, other.color)
label = Label(
vertices, pos, values, self.hemi, comment, name, None, self.subject, color
)
return label
def __sub__(self, other):
"""Subtract Labels."""
_validate_type(other, (Label, BiHemiLabel), "other")
if isinstance(other, BiHemiLabel):
if self.hemi == "lh":
return self - other.lh
else:
return self - other.rh
else: # isinstance(other, Label):
if self.subject != other.subject:
raise ValueError(
"Label subject parameters must match, got "
f'"{self.subject}" and "{other.subject}". Consider setting the '
"subject parameter on initialization, or "
"setting label.subject manually before "
"combining labels."
)
if self.hemi == other.hemi:
keep = np.isin(self.vertices, other.vertices, True, invert=True)
else:
keep = np.arange(len(self.vertices))
name = f"{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,
)
def save(self, filename):
r"""Write to disk as FreeSurfer \*.label file.
Parameters
----------
filename : path-like
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 source space label.
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.
See Also
--------
Label.restrict
Label.smooth
"""
# find source space patch info
if len(self.vertices) == 0:
return self.copy()
hemi_src = _get_label_src(self, src)
if not np.all(np.isin(self.vertices, hemi_src["vertno"])):
msg = "Source space does not contain all of the label's vertices"
raise ValueError(msg)
if hemi_src["nearest"] is None:
warn(
"Source space is being modified in place because patch "
"information is needed. To avoid this in the future, run "
"mne.add_source_space_distances() on the source space "
"and save it to disk."
)
dist_limit = 0
add_source_space_distances(src, dist_limit=dist_limit)
nearest = hemi_src["nearest"]
# find new vertices
include = np.isin(nearest, self.vertices, False)
vertices = np.nonzero(include)[0]
# values
nearest_in_label = np.digitize(nearest[vertices], self.vertices, True)
values = self.values[nearest_in_label]
# pos
pos = hemi_src["rr"][vertices]
name = self.name if name is None else name
label = Label(
vertices,
pos,
values,
self.hemi,
self.comment,
name,
None,
self.subject,
self.color,
)
return label
def restrict(self, src, name=None):
"""Restrict a label to a source space.
Parameters
----------
src : instance of SourceSpaces
The source spaces to use to restrict the label.
name : None | str
Name for the new Label (default is self.name).
Returns
-------
label : instance of Label
The Label restricted to the set of source space vertices.
See Also
--------
Label.fill
Notes
-----
.. versionadded:: 0.20
"""
if len(self.vertices) == 0:
return self.copy()
hemi_src = _get_label_src(self, src)
mask = np.isin(self.vertices, hemi_src["vertno"])
name = self.name if name is None else name
label = Label(
self.vertices[mask],
self.pos[mask],
self.values[mask],
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=None,
verbose=None,
):
"""Smooth the label.
Useful for filling in labels made in a
decimated source space for display.
Parameters
----------
%(subject_label)s
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 shape (2,), 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)s
%(n_jobs)s
%(verbose)s
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, verbose=verbose
)
@verbose
def morph(
self,
subject_from=None,
subject_to=None,
smooth=5,
grade=None,
subjects_dir=None,
n_jobs=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 shape (2,), 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)s
%(n_jobs)s
%(verbose)s
Returns
-------
label : instance of Label
The morphed label.
See Also
--------
mne.morph_labels : Morph a set of labels.
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``.
"""
from .morph import compute_source_morph, grade_to_vertices
subject_from = _check_subject(self.subject, subject_from)
if not isinstance(subject_to, str):
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)."
)
idx = 0 if self.hemi == "lh" else 1
if isinstance(grade, np.ndarray):
grade_ = [np.array([], int)] * 2
grade_[idx] = grade
grade = grade_
del grade_
grade = grade_to_vertices(subject_to, grade, subjects_dir=subjects_dir)
spacing = [np.array([], int)] * 2
spacing[idx] = grade[idx]
vertices = [np.array([], int)] * 2
vertices[idx] = self.vertices
data = self.values[:, np.newaxis]
assert len(data) == sum(len(v) for v in vertices)
stc = SourceEstimate(data, vertices, tmin=1, tstep=1, subject=subject_from)
stc = compute_source_morph(
stc,
subject_from,
subject_to,
spacing=spacing,
smooth=smooth,
subjects_dir=subjects_dir,
warn=False,
).apply(stc)
inds = np.nonzero(stc.data)[0]
self.values = stc.data[inds, :].ravel()
self.pos = np.zeros((len(inds), 3))
self.vertices = stc.vertices[idx][inds]
self.subject = subject_to
return self
@fill_doc
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_label)s
%(subjects_dir)s
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, shape (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, str) 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 "
f"('contiguous'). Got {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[np.isin(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(np.isin(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
@fill_doc
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 :footcite:`LarsonLee2013`.
Parameters
----------
%(subject_label)s
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)s
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
----------
.. footbibliography::
"""
if not isinstance(surf, str):
raise TypeError(f"surf must be a string, got {type(surf)}")
subject = _check_subject(self.subject, subject)
if np.any(self.values < 0):
raise ValueError("Cannot compute COM with negative values")
if np.all(self.values == 0):
raise ValueError(
"Cannot compute COM with all values == 0. For "
"structural labels, consider setting to ones via "
"label.values[:] = 1."
)
vertex = _center_of_mass(
self.vertices,
self.values,
self.hemi,
surf,
subject,
subjects_dir,
restrict_vertices,
)
return vertex
@verbose
def distances_to_outside(
self, subject=None, subjects_dir=None, surface="white", *, verbose=None
):
"""Compute the distance from each vertex to outside the label.
Parameters
----------
%(subject_label)s
%(subjects_dir)s
%(surface)s
%(verbose)s
Returns
-------
dist : ndarray, shape (n_vertices,)
The distance from each vertex in ``self.vertices`` to exit the
label.
outside_vertices : ndarray, shape (n_vertices,)
For each vertex in the label, the nearest vertex outside the
label.
Notes
-----
Distances are computed along the cortical surface.
.. versionadded:: 0.24
"""
rr, tris = self._load_surface(subject, subjects_dir, surface)
adjacency = mesh_dist(tris, rr)
mask = np.zeros(len(rr))
mask[self.vertices] = 1
border_vert = _mesh_borders(tris, mask)
# vertices on the edge
outside_vert = np.setdiff1d(border_vert, self.vertices)
dist, _, outside = sparse.csgraph.dijkstra(
adjacency, indices=outside_vert, min_only=True, return_predecessors=True
)
dist = dist[self.vertices] * 1e-3 # mm to m
outside = outside[self.vertices]
return dist, outside
@verbose
def compute_area(
self, subject=None, subjects_dir=None, surface="white", *, verbose=None
):
"""Compute the surface area of a label.
Parameters
----------
%(subject_label)s
%(subjects_dir)s
%(surface)s
%(verbose)s
Returns
-------
area : float
The area (in m²) of the label.
Notes
-----
..versionadded:: 0.24
"""
_, _, surf = self._load_surface(
subject, subjects_dir, surface, return_dict=True
)
complete_surface_info(
surf, do_neighbor_vert=False, do_neighbor_tri=False, copy=False
)
in_ = np.isin(surf["tris"], self.vertices).reshape(surf["tris"].shape)
tidx = np.where(in_.all(-1))[0]
if len(tidx) == 0:
warn("No complete triangles found, perhaps label is not filled?")
return surf["tri_area"][tidx].sum() * 1e-6 # mm² -> m²
def _load_surface(self, subject, subjects_dir, surface, **kwargs):
subject = _check_subject(self.subject, subject)
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
fname = subjects_dir / subject / "surf" / f"{self.hemi}.{surface}"
fname = _check_fname(fname, overwrite="read", must_exist=True, name="Surface")
return read_surface(fname, **kwargs)
def _get_label_src(label, src):
src = _ensure_src(src)
if src.kind != "surface":
raise RuntimeError(
f"Cannot operate on SourceSpaces that are not surface type, got {src.kind}"
)
if label.hemi == "lh":
hemi_src = src[0]
else:
hemi_src = src[1]
return hemi_src
class BiHemiLabel:
"""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 | 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