@@ -1710,36 +1710,13 @@ def _verts_within_dist(graph, sources, max_dist):
17101710 dist : array
17111711 Distances from source vertex.
17121712 """
1713- dist_map = {}
1714- verts_added_last = []
1715- for source in sources :
1716- dist_map [source ] = 0
1717- verts_added_last .append (source )
1718-
1719- # add neighbors until no more neighbors within max_dist can be found
1720- while len (verts_added_last ) > 0 :
1721- verts_added = []
1722- for i in verts_added_last :
1723- v_dist = dist_map [i ]
1724- row = graph [[i ], :]
1725- neighbor_vert = row .indices
1726- neighbor_dist = row .data
1727- for j , d in zip (neighbor_vert , neighbor_dist ):
1728- n_dist = v_dist + d
1729- if j in dist_map :
1730- if n_dist < dist_map [j ]:
1731- dist_map [j ] = n_dist
1732- else :
1733- if n_dist <= max_dist :
1734- dist_map [j ] = n_dist
1735- # we found a new vertex within max_dist
1736- verts_added .append (j )
1737- verts_added_last = verts_added
1738-
1739- verts = np .sort (np .array (list (dist_map .keys ()), int ))
1740- dist = np .array ([dist_map [v ] for v in verts ], int )
1713+ from scipy .sparse .csgraph import dijkstra
17411714
1742- return verts , dist
1715+ # ``min_only`` gives the distance to the closest of ``sources`` for every vertex,
1716+ # and ``limit`` leaves the ones beyond max_dist at infinity
1717+ dist = dijkstra (graph , indices = sources , min_only = True , limit = max_dist )
1718+ verts = np .flatnonzero (np .isfinite (dist ))
1719+ return verts , dist [verts ].astype (int )
17431720
17441721
17451722def _grow_labels (seeds , extents , hemis , names , dist , vert , subject ):
@@ -1931,6 +1908,8 @@ def _grow_nonoverlapping_labels(
19311908 subject , seeds_ , extents_ , hemis , vertices_ , graphs , names_
19321909):
19331910 """Grow labels while ensuring that they don't overlap."""
1911+ from scipy .sparse .csgraph import dijkstra
1912+
19341913 labels = []
19351914 for hemi in set (hemis ):
19361915 hemi_index = hemis == hemi
@@ -1941,56 +1920,34 @@ def _grow_nonoverlapping_labels(
19411920 n_vertices = len (vertices_ [hemi ])
19421921 n_labels = len (seeds )
19431922
1944- # prepare parcellation
1945- parc = np .empty (n_vertices , dtype = "int32" )
1946- parc [:] = - 1
1947-
1948- # initialize active sources
1949- sources = {} # vert -> (label, dist_from_seed)
1950- edge = [] # queue of vertices to process
1923+ # which label each seed vertex belongs to
1924+ seed_label = np .full (n_vertices , - 1 , int )
19511925 for label , seed in enumerate (seeds ):
1952- if np .any (parc [seed ] >= 0 ):
1926+ if np .any (seed_label [seed ] >= 0 ):
19531927 raise ValueError ("Overlapping seeds" )
1954- parc [seed ] = label
1955- for s in np .atleast_1d (seed ):
1956- sources [s ] = (label , 0.0 )
1957- edge .append (s )
1958-
1959- # grow from sources
1960- while edge :
1961- vert_from = edge .pop (0 )
1962- label , old_dist = sources [vert_from ]
1963-
1964- # add neighbors within allowable distance
1965- row = graph [[vert_from ], :]
1966- for vert_to , dist in zip (row .indices , row .data ):
1967- # Prevent adding a point that has already been used
1968- # (prevents infinite loop)
1969- if (vert_to == seeds [label ]).any ():
1970- continue
1971- new_dist = old_dist + dist
1972-
1973- # abort if outside of extent
1974- if new_dist > extents [label ]:
1975- continue
1976-
1977- vert_to_label = parc [vert_to ]
1978- if vert_to_label >= 0 :
1979- _ , vert_to_dist = sources [vert_to ]
1980- # abort if the vertex is occupied by a closer seed
1981- if new_dist > vert_to_dist :
1982- continue
1983- elif vert_to in edge :
1984- edge .remove (vert_to )
1985-
1986- # assign label value
1987- parc [vert_to ] = label
1988- sources [vert_to ] = (label , new_dist )
1989- edge .append (vert_to )
1928+ seed_label [seed ] = label
1929+
1930+ # A multi-source Dijkstra with min_only gives, for every vertex, its distance
1931+ # to the closest seed vertex and which seed vertex that was, i.e. exactly the
1932+ # non-overlapping assignment we want: each label grows outward until it runs
1933+ # into a vertex that some other label reaches sooner.
1934+ dist , _ , sources = dijkstra (
1935+ graph ,
1936+ indices = np .flatnonzero (seed_label >= 0 ),
1937+ min_only = True ,
1938+ return_predecessors = True ,
1939+ )
1940+ parc = np .full (n_vertices , - 1 , int )
1941+ reached = np .isfinite (dist )
1942+ parc [reached ] = seed_label [sources [reached ]]
1943+ # then drop vertices that are further away than their own label's extent
1944+ parc [reached ] = np .where (
1945+ dist [reached ] <= extents [parc [reached ]], parc [reached ], - 1
1946+ )
19901947
19911948 # convert parc to labels
19921949 for i in range (n_labels ):
1993- vertices = np .nonzero (parc == i )[ 0 ]
1950+ vertices = np .flatnonzero (parc == i )
19941951 name = str (names [i ])
19951952 label_ = Label (vertices , hemi = hemi , name = name , subject = subject )
19961953 labels .append (label_ )
@@ -2118,16 +2075,21 @@ def _cortex_parcellation(subject, n_parcel, hemis, vertices_, graphs, rng):
21182075 rest -= 1
21192076
21202077 # merging small labels
2121- # label adjacency matrix
2078+ # label adjacency matrix: every vertex belongs to exactly one label at this
2079+ # point, so mapping both ends of each graph edge through parc marks all pairs
2080+ # of adjacent labels at once. Functionally equivalent to, but much faster than:
2081+ #
2082+ # for i in range(n_labels):
2083+ # vertices = np.nonzero(parc == i)[0]
2084+ # label_sizes[i] = len(vertices)
2085+ # neighbor_labels = np.unique(parc[graph[vertices, :].indices])
2086+ # label_conn[i, neighbor_labels] = 1
2087+ #
21222088 n_labels = label_idx + 1
2123- label_sizes = np .empty ( n_labels , dtype = int )
2089+ label_sizes = np .bincount ( parc , minlength = n_labels )
21242090 label_conn = np .zeros ([n_labels , n_labels ], dtype = "bool" )
2125- for i in range (n_labels ):
2126- vertices = np .nonzero (parc == i )[0 ]
2127- label_sizes [i ] = len (vertices )
2128- neighbor_vertices = graph [vertices , :].indices
2129- neighbor_labels = np .unique (np .array (parc [neighbor_vertices ]))
2130- label_conn [i , neighbor_labels ] = 1
2091+ edges = graph .tocoo ()
2092+ label_conn [parc [edges .row ], parc [edges .col ]] = True
21312093 np .fill_diagonal (label_conn , 0 )
21322094
21332095 # merging
@@ -2616,30 +2578,53 @@ def _check_values_labels(values, n_labels):
26162578 )
26172579
26182580
2619- def _labels_to_stc_surf (labels , values , tmin , tstep , subject ):
2581+ def _label_membership (label_indices , n_vertices ):
2582+ """Get a sparse (n_labels, n_vertices) matrix of which vertices are in each label.
2583+
2584+ ``label_indices[li]`` holds the indices, into some length-``n_vertices`` array of
2585+ vertices, of the vertices belonging to label ``li``. Multiplying by this matrix (or
2586+ its transpose) is how the label-wise operations in this module avoid looping over
2587+ labels; see ``_labels_to_stc_surf`` and ``_label_adjacency`` for examples.
2588+ """
26202589 from scipy import sparse
26212590
2591+ n_labels = len (label_indices )
2592+ rows = np .repeat (np .arange (n_labels ), [len (ind ) for ind in label_indices ])
2593+ cols = np .concatenate ([np .empty (0 , int )] + list (label_indices ))
2594+ return sparse .csr_array (
2595+ (np .ones (len (cols )), (rows , cols )), shape = (n_labels , n_vertices )
2596+ )
2597+
2598+
2599+ def _labels_to_stc_surf (labels , values , tmin , tstep , subject ):
26222600 subject = _check_labels_subject (labels , subject , "subject" )
26232601 _check_values_labels (values , len (labels ))
2624- vertices = dict (lh = [], rh = [])
2625- data = dict (lh = [], rh = [])
2626- for li , label in enumerate (labels ):
2627- data [label .hemi ].append (
2628- np .repeat (values [li ][np .newaxis ], len (label .vertices ), axis = 0 )
2602+ vertices = list ()
2603+ data = list ()
2604+ for hemi in ("lh" , "rh" ):
2605+ idx = np .array (
2606+ [li for li , label in enumerate (labels ) if label .hemi == hemi ], int
2607+ )
2608+ label_vertices = [labels [li ].vertices for li in idx ]
2609+ these_vertices = np .unique (np .concatenate ([np .empty (0 , int )] + label_vertices ))
2610+ membership = _label_membership (
2611+ [np .searchsorted (these_vertices , verts ) for verts in label_vertices ],
2612+ len (these_vertices ),
26292613 )
2630- vertices [label .hemi ].append (label .vertices )
2631- hemis = ("lh" , "rh" )
2632- for hemi in hemis :
2633- vertices [hemi ] = np .concatenate (vertices [hemi ], axis = 0 )
2634- data [hemi ] = np .concatenate (data [hemi ], axis = 0 ).astype (float )
2635- cols = np .arange (len (vertices [hemi ]))
2636- vertices [hemi ], rows = np .unique (vertices [hemi ], return_inverse = True )
2637- mat = sparse .coo_array ((np .ones (len (rows )), (rows , cols ))).tocsr ()
2638- mat *= 1.0 / mat .sum (axis = - 1 )
2639- data [hemi ] = mat @ data [hemi ]
2640- vertices = [vertices [hemi ] for hemi in hemis ]
2641- data = np .concatenate ([data [hemi ] for hemi in hemis ], axis = 0 )
2642- return data , vertices , subject
2614+ # ``membership.T @ values[idx]`` sums, for each vertex, the values of every
2615+ # label containing it, and ``n_used`` is how many labels that was, so the two
2616+ # together give the mean. Functionally equivalent to, but much faster than:
2617+ #
2618+ # for vi, vertex in enumerate(these_vertices):
2619+ # in_label = [
2620+ # ii for ii, verts in enumerate(label_vertices) if vertex in verts
2621+ # ]
2622+ # this_data[vi] = values[idx[in_label]].mean(axis=0)
2623+ #
2624+ n_used = membership .sum (axis = 0 ) # number of labels containing each vertex
2625+ vertices .append (these_vertices )
2626+ data .append ((membership .T @ values [idx ]) / n_used [:, np .newaxis ])
2627+ return np .concatenate (data ), vertices , subject
26432628
26442629
26452630_DEFAULT_TABLE_NAME = "MNE-Python Colortable"
@@ -3048,6 +3033,37 @@ def select_sources(
30483033 return new_label
30493034
30503035
3036+ def _label_adjacency (label_src_ind , src_adjacency ):
3037+ """Turn per-label source space indices plus source adjacency into label adjacency.
3038+
3039+ Two labels are adjacent if any vertex of one is adjacent to any vertex of the
3040+ other. ``label_src_ind[li]`` holds the indices into the source space of the
3041+ vertices belonging to label ``li``.
3042+ """
3043+ from scipy import sparse
3044+
3045+ n_labels = len (label_src_ind )
3046+ membership = _label_membership (label_src_ind , src_adjacency .shape [0 ])
3047+ # ``counts[i, j]`` is the number of adjacent (vertex in label i, vertex in label j)
3048+ # vertex pairs, which is nonzero exactly when the two labels are adjacent.
3049+ # Functionally equivalent to, but much faster than:
3050+ #
3051+ # src_adjacency = src_adjacency.tocsr()
3052+ # counts = np.zeros((n_labels, n_labels))
3053+ # for i in range(n_labels):
3054+ # for j in range(n_labels):
3055+ # counts[i, j] = src_adjacency[label_src_ind[i]][
3056+ # :, label_src_ind[j]
3057+ # ].sum()
3058+ #
3059+ # which needs O(n_labels ** 2) sparse slices, each costing O(nnz(src_adjacency)).
3060+ counts = membership @ src_adjacency .tocsr () @ membership .T
3061+ row , col = counts .nonzero () # ignores any explicitly stored zeros
3062+ return sparse .coo_matrix (
3063+ (np .ones (len (row )), (row , col )), shape = (n_labels , n_labels )
3064+ )
3065+
3066+
30513067def label_adjacency (labels , src ):
30523068 """Compute adjacency between labels.
30533069
@@ -3075,9 +3091,7 @@ def label_adjacency(labels, src):
30753091 -----
30763092 .. versionadded:: 1.13
30773093 """
3078- from scipy .sparse import coo_matrix
3079-
3080- src_adjacency = spatial_src_adjacency (src ).tocsr ()
3094+ src_adjacency = spatial_src_adjacency (src )
30813095 label_src_ind = list ()
30823096 for label in labels :
30833097 src_hemi = src [0 ] if label .hemi == "lh" else src [1 ]
@@ -3086,22 +3100,10 @@ def label_adjacency(labels, src):
30863100 if label .hemi == "rh" :
30873101 src_ind += src [0 ]["nuse" ]
30883102 label_src_ind .append (src_ind )
3089-
3090- adjacent_label_inds = list () # list of pairs of label indices
3091- for ind1 , label1 in enumerate (labels ):
3092- for ind2 , label2 in enumerate (labels ):
3093- # If the labels are on different hemispheres, they are not adjacent.
3094- if label1 .hemi != label2 .hemi :
3095- continue
3096-
3097- # Get adjacent vertices if any.
3098- adj_verts = src_adjacency [label_src_ind [ind1 ], :][:, label_src_ind [ind2 ]]
3099- if adj_verts .data .any ():
3100- adjacent_label_inds .append ((ind1 , ind2 ))
3101- return coo_matrix (
3102- (np .ones (len (adjacent_label_inds )), tuple (zip (* adjacent_label_inds ))),
3103- shape = (len (labels ), len (labels )),
3104- )
3103+ # Labels in different hemispheres are never adjacent because the source space
3104+ # adjacency has no inter-hemispheric edges, so no explicit hemisphere check is
3105+ # needed here.
3106+ return _label_adjacency (label_src_ind , src_adjacency )
31053107
31063108
31073109@fill_doc
@@ -3136,8 +3138,6 @@ def volume_label_adjacency(src, subject, subjects_dir, *, aseg="auto", labels=No
31363138 -----
31373139 .. versionadded:: 1.13
31383140 """
3139- from scipy import sparse
3140-
31413141 subjects_dir = Path (get_subjects_dir (subjects_dir , raise_error = True ))
31423142 if aseg == "auto" : # use aparc+aseg if auto
31433143 aseg = _check_fname (
@@ -3154,22 +3154,8 @@ def volume_label_adjacency(src, subject, subjects_dir, *, aseg="auto", labels=No
31543154 labels = get_volume_labels_from_aseg (aseg )
31553155
31563156 vol_labels = _volume_labels (src , (aseg , labels ), mri_resolution = False )
3157- src_adjacency = spatial_src_adjacency (src ).tocsr ()
3158-
3159- label_verts = list ()
3160- for label in vol_labels :
3161- label_verts .append (np .searchsorted (src [0 ]["vertno" ], label .vertices ))
3162-
3163- adjacent_label_inds = list () # list of pairs of label indices
3164- for ind1 , verts1 in enumerate (label_verts ):
3165- for ind2 , verts2 in enumerate (label_verts ):
3166- # Get adjacent vertices if any.
3167- adj_verts = src_adjacency [verts1 , :][:, verts2 ]
3168- if adj_verts .data .any ():
3169- adjacent_label_inds .append ((ind1 , ind2 ))
3170-
3171- adj = sparse .coo_matrix (
3172- (np .ones (len (adjacent_label_inds )), tuple (zip (* adjacent_label_inds ))),
3173- shape = (len (labels ), len (labels )),
3174- )
3175- return adj , labels
3157+ src_adjacency = spatial_src_adjacency (src )
3158+ label_src_ind = [
3159+ np .searchsorted (src [0 ]["vertno" ], label .vertices ) for label in vol_labels
3160+ ]
3161+ return _label_adjacency (label_src_ind , src_adjacency ), labels
0 commit comments