What is 'outlines' in plot_topomap actually doing?

Disclaimer: I did not look at the code part. Running latest mne-main 1.2.dev0

For plot_topomap, looking at the description of the argument outlines here:

The outlines to be drawn. If ā€˜headā€™, the default head scheme will be drawn. If ā€˜skirtā€™ the head scheme will be drawn, but sensors are allowed to be plotted outside of the head circle.

So if I get it right, 'head' will crop the electrodes outside the head circle. Letā€™s give it a try, with sphere='eeglab' to push sensors further away from the head circle.

import numpy as np
from matplotlib import pyplot as plt
from mne import create_info
from mne.channels import make_standard_montage
from mne.viz import plot_topomap


montage = make_standard_montage("standard_1020")
ch_names = [
    ch for ch in montage.ch_names 
    if ch not in ("P7", "P8", "T3", "T5", "T4", "T6")
]
info = create_info(ch_names, 1, "eeg")
info.set_montage("standard_1020")
data = np.random.randn(len(ch_names))

f, ax = plt.subplots(1, 2)
ax[0].set_title("outlines='head'")
plot_topomap(data, info, outlines="head", sphere="eeglab", axes=ax[0])
ax[1].set_title("outlines='skirt'")
plot_topomap(data, info, outlines="skirt", sphere="eeglab", axes=ax[1])

I am not seeing the difference, are you?

Screenshot 2022-09-05 at 15.53.10

I never understood this either. Never seemed to work for meā€¦ I wonder if @drammock or @cbrnr have an idea?

My advice would be to find a tutorial that demos both options (if it exists) and look at older versions of our docs until you find a version where they look different. That at least provides a starting point!

also tagging @mmagnuski on this one, as I think he may have the best / most recent grasp of the topomap plotting code.

1 Like

Thanks for tagging me @drammock!

There was a time, when outlines actually did make a difference, but since the restructuring of the topomap plotting lead by @larsoner (but I am also to blame :slight_smile: ) outlines does not make a difference.

In old mne-topomap times the outlines='head' positioned all channels within the head circle and cropped the interpolation limits where the circle ended. On the other hand, outlines='skirt' allowed the channels and the interpolated map to extend beyond the head circle. This was because at that time how the channel positions were drawn with respect to the head outline was mostly dictated by aesthetic preferences (ā€œdo you prefer the channels to be packed inside the head outline?ā€). Currently they convey how sensors are placed with respect to the head, where head is represented by a sphere (in a vacuum :wink: ), whose position and radius is controlled by the sphere argument. So outlines should no longer make a difference.
So, if we want to stay consistent, I think we should remove the outlines argument. This is the easiest way out. However, I saw some posts on this forum that suggest that some people prefer to have all the channels plotted inside the head outline (even if it makes the channel-outline relationship arbitrary) so we could also repurpose outlines to match their needs (outlines='head' could change the sphere position so that all channels are within the head outline).
I much prefer the simple solution of removing outlines (plus maybe extending our tutorials/examples).

+1 to thus!! Thanks for the detailed explanation, @mmagnuski!

1 Like

Thanks for the explanation, so itā€™s an argument forgotten from a refactoring. Looking at the code, the only difference it makes is here: https://github.com/mne-tools/mne-python/blob/aef49669fe1bdf19221e03e85cf961671508e0bb/mne/viz/topomap.py#L447-L448

However, I saw some posts on this forum that suggest that some people prefer to have all the channels plotted inside the head outline (even if it makes the channel-outline relationship arbitrary) so we could also repurpose outlines to match their needs (outlines='head' could change the sphere position so that all channels are within the head outline)

Isnā€™t that what sphere='auto' is supposed to do?


+1 to deprecate it as well, either as part of or after Standardize topomap args by drammock Ā· Pull Request #11123 Ā· mne-tools/mne-python Ā· GitHub ? @drammock

Iā€™ll do it in #11123

1 Like

As it turns out, head and skirt are not the only options, you can also pass custom dicts or matplotlib patches (or callables that create patches). So in #11123 what Iā€™m doing is just deprecating the value skirt. So now the options are

  • 'head' (what weā€™re all familiar with)
  • None (nothing)
  • a custom dict (for heads of different shapes?)
  • a patch or patch-returning callable (for really complicated masking?)

Since head is now the only allowed string value I wonder if it should become 'auto' (i.e., just use the standard, familiar, built-in head outline). WDYT?

also tagging @larsoner and @agramfort for opinions here.

1 Like

I wouldnā€™t bother changing 'head' ā†’ 'auto' but deprecating skirt makes sense to me

2 Likes

works for me

thanks
Alex

I agree this sounds good

The docstring does mention the dict and the patch or patch-returning callable, but do we really support them?

  • dict: We donā€™t mention which keys have to be present, so except if someone digs in _make_head_outlines itā€™s hard to figure out what this dict is suppose to look like. e.g. the key clip_radius is required by _setup_interp.

  • Patch or Callable: looking again at _make_head_outlines, itā€™s structured as:

if outlines in ('head', 'skirt', None):
    [...]
elif isinstance(outlines, dict):
    [...]
else:
    raise ValueError('Invalid value for `outlines`.')
1 Like

we need to keep supporting dict (at least in the short term) because many of our other functions internally call the (public) plot_topomap function and pass in a dict for the outlines argument. As for the patch: I think the docstring is unclear, the patch should be passed as part of the dict (at least if Iā€™m reading the code correctly?) But I think both the docstring and that part of the code could use some love. #11123 is complex enough as it is, so Iā€™ll probably do any patch/dict cleanup / simplification in a subsequent PR. @mscheltienne if you have time and inclination, feel free to look and propose a better docstring / code path for dict and patch handling.

Ok sure. In the long run, I think it would be better to replace the internal calls to plot_topomap with _plot_topomap, and thus be able to remove the outlines argument entirely from the public API.
Of course no need to do all that in a single PR :wink:

For Patch, yes it looks like it has to be provided in the dict, if I read _get_patch correctly. There is no way anyone figures that out from the documentation. I canā€™t imagine a user providing that dictionary.

1 Like

Maybe the dict should actually be a proper dataclass or at least a TypedDict so itā€™s obvious which fields must be present.

This is in general an approach Iā€™d like to see adopted across MNEā€˜s code base.

I had the same thought, but yeah, not gonna tackle that in #11123 :slight_smile:

2 Likes

'auto' fits the sphere to the digitization points so you can have channels outside the head outline - because you will likely have some channels below the sphere origin after it has been fit.

1 Like

Hi All

I was wondering, if folks may think itā€™s possible to use outline to mask the topogoaphy such that you get the topography plotted on a smaller area of the scalp. That is, suppose you donā€™t want to interpolate on some channels. I tried to feed it with the following:

info = mne.create_info(list(peaks_time_df.index), sfreq=200, ch_types='eeg')
delay_topo = mne.EvokedArray(peaks_time_df, info)  # this is for average
grass_montage = mne.channels.make_standard_montage('standard_1020')
delay_topo.set_montage(grass_montage)
# %%
layout = mne.channels.layout.find_layout(delay_topo.info)
pos = layout.pos
# %%
relevant_outline = dict(mask_pos=pos[0:4])

# %%
fig,ax=plt.subplots(1,1)
cn,im= mne.viz.plot_topomap(delay_topo.data[:, 0], delay_topo.info,names=delay_topo.ch_names, vlim=[-0.04,0.15],
                            cmap='YlOrRd',res=300,show=True,size=5,axes=ax,image_interp='linear',outlines=relevant_outline)
# add color bar:
fig.colorbar(cn, ax=ax, label='delay (ms)')
fig.show()

but of course I ran into the missing radius, not sure how that should be fed to the function. Any ideas?
if anyone has other ideas how this can be done, that would be awesome, openAI was clueless.

thanks

Hi @sharomer,
Iā€™m not sure if I understand what you are trying to achieve. If you want to omit some channels from the topomap plotting - you can pick the data and info or set these channels as bad.