Skip to content

Commit f2422db

Browse files
Fix backwards-incompatible changes to Brain.add_annotation (#14083)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 346d5a1 commit f2422db

3 files changed

Lines changed: 235 additions & 21 deletions

File tree

mne/label.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,6 +2124,87 @@ def _read_annot_cands(dir_name, raise_error=True):
21242124
return cands
21252125

21262126

2127+
def _read_annot(fname):
2128+
"""Read a Freesurfer annotation from a .annot file.
2129+
2130+
Note : Copied from PySurfer
2131+
2132+
Parameters
2133+
----------
2134+
fname : str
2135+
Path to annotation file
2136+
2137+
Returns
2138+
-------
2139+
annot : numpy array, shape=(n_verts)
2140+
Annotation id at each vertex
2141+
ctab : numpy array, shape=(n_entries, 5)
2142+
RGBA + label id colortable array
2143+
names : list of str
2144+
List of region names as stored in the annot file
2145+
2146+
"""
2147+
if not op.isfile(fname):
2148+
dir_name = op.split(fname)[0]
2149+
cands = _read_annot_cands(dir_name)
2150+
if len(cands) == 0:
2151+
raise OSError(
2152+
f"No such file {fname}, no candidate parcellations found in directory"
2153+
)
2154+
else:
2155+
raise OSError(
2156+
f"No such file {fname}, candidate parcellations in "
2157+
"that directory:\n" + "\n".join(cands)
2158+
)
2159+
with open(fname, "rb") as fid:
2160+
n_verts = np.fromfile(fid, ">i4", 1)[0]
2161+
data = np.fromfile(fid, ">i4", n_verts * 2).reshape(n_verts, 2)
2162+
annot = data[data[:, 0], 1]
2163+
ctab_exists = np.fromfile(fid, ">i4", 1)[0]
2164+
if not ctab_exists:
2165+
raise Exception("Color table not found in annotation file")
2166+
n_entries = np.fromfile(fid, ">i4", 1)[0]
2167+
if n_entries > 0:
2168+
length = np.fromfile(fid, ">i4", 1)[0]
2169+
np.fromfile(fid, ">c", length) # discard orig_tab
2170+
2171+
names = list()
2172+
ctab = np.zeros((n_entries, 5), np.int64)
2173+
for i in range(n_entries):
2174+
name_length = np.fromfile(fid, ">i4", 1)[0]
2175+
name = np.fromfile(fid, f"|S{name_length}", 1)[0]
2176+
names.append(name)
2177+
ctab[i, :4] = np.fromfile(fid, ">i4", 4)
2178+
ctab[i, 4] = (
2179+
ctab[i, 0]
2180+
+ ctab[i, 1] * (2**8)
2181+
+ ctab[i, 2] * (2**16)
2182+
+ ctab[i, 3] * (2**24)
2183+
)
2184+
else:
2185+
ctab_version = -n_entries
2186+
if ctab_version != 2:
2187+
raise Exception("Color table version not supported")
2188+
n_entries = np.fromfile(fid, ">i4", 1)[0]
2189+
ctab = np.zeros((n_entries, 5), np.int64)
2190+
length = np.fromfile(fid, ">i4", 1)[0]
2191+
np.fromfile(fid, f"|S{length}", 1) # Orig table path
2192+
entries_to_read = np.fromfile(fid, ">i4", 1)[0]
2193+
names = list()
2194+
for i in range(entries_to_read):
2195+
np.fromfile(fid, ">i4", 1) # Structure
2196+
name_length = np.fromfile(fid, ">i4", 1)[0]
2197+
name = np.fromfile(fid, f"|S{name_length}", 1)[0]
2198+
names.append(name)
2199+
ctab[i, :4] = np.fromfile(fid, ">i4", 4)
2200+
ctab[i, 4] = ctab[i, 0] + ctab[i, 1] * (2**8) + ctab[i, 2] * (2**16)
2201+
2202+
# convert to more common alpha value
2203+
ctab[:, 3] = 255 - ctab[:, 3]
2204+
2205+
return annot, ctab, names
2206+
2207+
21272208
def _get_annot_fname(annot_fname, subject, hemi, parc, subjects_dir):
21282209
"""Get the .annot filenames and hemispheres."""
21292210
if annot_fname is not None:

mne/viz/_brain/_brain.py

Lines changed: 148 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@
4646
from ...utils import (
4747
Bunch,
4848
_auto_weakref,
49+
_check_fname,
4950
_check_option,
5051
_ensure_int,
52+
_path_like,
5153
_ReuseCycle,
5254
_to_rgb,
5355
_validate_type,
@@ -3078,21 +3080,27 @@ def add_annotation(
30783080
color=None,
30793081
hover=True,
30803082
):
3081-
"""Add an annotation file.
3083+
"""Add an annotation (i.e. an atlas of many labels) to the brain figure.
30823084
30833085
Parameters
30843086
----------
3085-
annot : str
3086-
Either path to annotation file or annotation name.
3087+
annot : path-like | str | list of Label
3088+
Either path to annotation file, an annotation name, or a list of
3089+
:class:`mne.Label` objects.
3090+
3091+
DEPRECATED: The annotation can be specified as a ``(labels, ctab)`` tuple
3092+
per hemisphere, i.e. ``annot=(labels, ctab)`` for a single hemisphere or
3093+
``annot=((lh_labels, lh_ctab), (rh_labels, rh_ctab))`` for both hemispheres.
3094+
3095+
.. versionadded:: 1.13
3096+
The ability to supply a list of :class:`~mne.Label` objects.
30873097
borders : bool | int
30883098
Show only label borders. If int, specify the number of steps
30893099
(away from the true border) along the cortical mesh to include
30903100
as part of the border definition.
30913101
%(alpha)s Default is 1.
30923102
hemi : str | None
3093-
If None, it is assumed to belong to the hemisphere being
3094-
shown. If two hemispheres are being shown, data must exist
3095-
for both hemispheres.
3103+
Optionally restrict the annotation to the given hemisphere.
30963104
remove_existing : bool
30973105
If True (default), remove old annotations.
30983106
color : matplotlib-style color code
@@ -3105,19 +3113,49 @@ def add_annotation(
31053113
"""
31063114
from ...label import read_labels_from_annot
31073115

3116+
if (isinstance(annot, tuple) and isinstance(annot[0], np.ndarray)) or (
3117+
isinstance(annot, (tuple, list)) and isinstance(annot[0], tuple)
3118+
):
3119+
# Deprecated old style of passing a (labels, cmap) pair per hemisphere.
3120+
# Shortcut to old code that can be removed in version 1.14.
3121+
warn(
3122+
"Passing the annotation as a `(label, cmap)` tuple is deprecated and "
3123+
"will be removed in MNE-Python version 1.14.",
3124+
FutureWarning,
3125+
)
3126+
self._old_add_annotation(
3127+
annot,
3128+
borders=borders,
3129+
alpha=alpha,
3130+
hemi=hemi,
3131+
remove_existing=remove_existing,
3132+
color=color,
3133+
)
3134+
return
3135+
3136+
_validate_type(annot, ("path-like", str, list), "annot")
3137+
31083138
hemis = self._check_hemis(hemi)
31093139
kwargs = dict()
3110-
if os.path.isfile(annot):
3111-
kwargs["annot_fname"] = annot
3112-
else:
3113-
kwargs["parc"] = annot
31143140

3115-
for hemi in hemis:
3116-
labels = read_labels_from_annot(
3117-
self._subject, hemi=hemi, subjects_dir=self._subjects_dir, **kwargs
3118-
)
3141+
for hemi_idx, hemi in enumerate(hemis):
3142+
if _path_like(annot):
3143+
if os.path.isfile(annot):
3144+
kwargs["annot_fname"] = annot
3145+
else:
3146+
kwargs["parc"] = annot
3147+
labels = read_labels_from_annot(
3148+
self._subject,
3149+
hemi=hemi,
3150+
subjects_dir=self._subjects_dir,
3151+
**kwargs,
3152+
)
3153+
name = annot
3154+
else:
3155+
labels = [label for label in annot if label.hemi == hemi]
3156+
name = "annotation" # placeholder name for the annotation
31193157
n_labels = len(labels)
3120-
ids = np.zeros(self.geo[hemi].coords.shape[0], dtype=int)
3158+
ids = np.full(self.geo[hemi].coords.shape[0], -1, dtype=int)
31213159
cmap = np.zeros((len(labels) + 1, 4))
31223160
cmap[:, 3] = 1
31233161
cmap[0] = np.array(self._brain_color)
@@ -3131,7 +3169,7 @@ def add_annotation(
31313169
label.center_of_mass(subjects_dir=self._subjects_dir)
31323170
]
31333171
self._annots[hemi].append(
3134-
dict(name=annot, labels=labels, ids=ids, centroids=centroids)
3172+
dict(name=name, labels=labels, ids=ids, centroids=centroids)
31353173
)
31363174
del labels
31373175

@@ -3151,7 +3189,7 @@ def add_annotation(
31513189
colormap=ctable * 255,
31523190
rng=[0, n_labels],
31533191
opacity=alpha,
3154-
name=annot,
3192+
name=name,
31553193
)
31563194

31573195
if hover:
@@ -3175,6 +3213,99 @@ def on_annotation_hover(iren, event):
31753213
)
31763214
self._renderer._update()
31773215

3216+
# DEPRECATED: Can be removed in version 1.14. Also remove _read_annot from
3217+
# mne/labels.py.
3218+
def _old_add_annotation(
3219+
self, annot, borders=True, alpha=1, hemi=None, remove_existing=True, color=None
3220+
):
3221+
from ...label import _read_annot
3222+
3223+
hemis = self._check_hemis(hemi)
3224+
3225+
# Figure out where the data is coming from
3226+
if _path_like(annot):
3227+
if os.path.isfile(annot):
3228+
filepath = _check_fname(annot, overwrite="read")
3229+
file_hemi, annot = filepath.name.split(".", 1)
3230+
if len(hemis) > 1:
3231+
if file_hemi == "lh":
3232+
filepaths = [filepath, filepath.parent / ("rh." + annot)]
3233+
elif file_hemi == "rh":
3234+
filepaths = [filepath.parent / ("lh." + annot), filepath]
3235+
else:
3236+
raise RuntimeError(
3237+
"To add both hemispheres simultaneously, filename must "
3238+
'begin with "lh." or "rh."'
3239+
)
3240+
else:
3241+
filepaths = [filepath]
3242+
else:
3243+
filepaths = []
3244+
for hemi in hemis:
3245+
filepath = op.join(
3246+
self._subjects_dir,
3247+
self._subject,
3248+
"label",
3249+
".".join([hemi, annot, "annot"]),
3250+
)
3251+
if not os.path.exists(filepath):
3252+
raise ValueError(f"Annotation file {filepath} does not exist")
3253+
filepaths += [filepath]
3254+
annots = []
3255+
for hemi, filepath in zip(hemis, filepaths):
3256+
# Read in the data
3257+
labels, cmap, _ = _read_annot(filepath)
3258+
annots.append((labels, cmap))
3259+
else:
3260+
annots = [annot] if len(hemis) == 1 else annot
3261+
annot = "annotation"
3262+
3263+
for hemi, (labels, cmap) in zip(hemis, annots):
3264+
# Maybe zero-out the non-border vertices
3265+
self._to_borders(labels, hemi, borders)
3266+
3267+
# Handle null labels properly
3268+
cmap[:, 3] = 255
3269+
bgcolor = np.round(np.array(self._brain_color) * 255).astype(int)
3270+
bgcolor[-1] = 0
3271+
cmap[cmap[:, 4] < 0, 4] += 2**24 # wrap to positive
3272+
cmap[cmap[:, 4] <= 0, :4] = bgcolor
3273+
if np.any(labels == 0) and not np.any(cmap[:, -1] <= 0):
3274+
cmap = np.vstack((cmap, np.concatenate([bgcolor, [0]])))
3275+
3276+
# Set label ids sensibly
3277+
order = np.argsort(cmap[:, -1])
3278+
cmap = cmap[order]
3279+
ids = np.searchsorted(cmap[:, -1], labels)
3280+
cmap = cmap[:, :4]
3281+
3282+
# Set the alpha level
3283+
alpha_vec = cmap[:, 3]
3284+
alpha_vec[alpha_vec > 0] = alpha * 255
3285+
3286+
# Override the cmap when a single color is used
3287+
if color is not None:
3288+
rgb = np.round(np.multiply(_to_rgb(color), 255))
3289+
cmap[:, :3] = rgb.astype(cmap.dtype)
3290+
3291+
ctable = cmap.astype(np.float64)
3292+
for _ in self._iter_views(hemi):
3293+
mesh = self.layered_meshes[hemi]
3294+
mesh.add_overlay(
3295+
scalars=ids,
3296+
colormap=ctable,
3297+
rng=[np.min(ids), np.max(ids)],
3298+
opacity=alpha,
3299+
name=annot,
3300+
)
3301+
self._annots[hemi].append(annot)
3302+
if not self.time_viewer or self.traces_mode == "vertex":
3303+
self._renderer._set_colormap_range(
3304+
mesh._actor, cmap.astype(np.uint8), None
3305+
)
3306+
3307+
self._renderer._update()
3308+
31783309
def _create_caption(self):
31793310
from vtkmodules.vtkRenderingAnnotation import vtkCaptionActor2D
31803311

mne/viz/_brain/tests/test_brain.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
fname_trans = sample_dir / "sample_audvis_trunc-trans.fif"
5353
fname_stc = sample_dir / "sample_audvis_trunc-meg"
5454
fname_label = sample_dir / "labels" / "Vis-lh.label"
55+
fname_label2 = sample_dir / "labels" / "Vis-rh.label"
5556
fname_cov = sample_dir / "sample_audvis_trunc-cov.fif"
5657
fname_evoked = sample_dir / "sample_audvis_trunc-ave.fif"
5758
fname_fwd = sample_dir / "sample_audvis_trunc-meg-eeg-oct-4-fwd.fif"
@@ -590,10 +591,11 @@ def test_add_annotation(renderer_interactive_pyvistaqt, brain_gc):
590591
annots = [
591592
"aparc",
592593
subjects_dir / "fsaverage" / "label" / "lh.PALS_B12_Lobes.annot",
594+
[read_label(fname_label, "fsaverage"), read_label(fname_label2, "fsaverage")],
593595
]
594-
borders = [True, 2]
595-
alphas = [1, 0.5]
596-
colors = [None, "r"]
596+
borders = [True, 1, 2]
597+
alphas = [1, 0.5, 0]
598+
colors = [None, "r", "b"]
597599
size = (100, 100)
598600
brain = Brain(
599601
subject="fsaverage",
@@ -655,7 +657,7 @@ def GetPickPosition(self):
655657
subjects_dir=subjects_dir,
656658
)
657659
for a, b, p, color in zip(annots, borders, alphas, colors):
658-
brain.add_annotation(str(a), b, p, color=color)
660+
brain.add_annotation(a, b, p, color=color)
659661
brain.close()
660662

661663

0 commit comments

Comments
 (0)