Hello Dear Mne Developers,
I am new to MNE python and this EEG field. I am using mne.ICA for removing EOG artifacts. I have 3 EOG channels and I am following the example published on the web. The problem is after fitting the ICA I use the find_bads_eog for selecting bad components but this one only takes into account the first EOG channel and removes only one component. But on display, I can clearly see a lot of artifacts are coming from other 2 channels. Is there a way to force ICA to take into account all the available EOG channels?
Thanks in advance :)
Internally find_bads_eog with ch_name=None will already use any channels marked as EOG. But we should also allow ch_name to be a list instead of a singleton so that equivalent functionality can be achieved manually instead of based on ch_type (for consistency at the very least).
Thanks for your quick reply @larsoner but I want to ask even after doing ICA the signal is still quite messy. What can be done after ICA ?
That is a conceptual question better suited to the mne_analyze listserv
Internally find_bads_eog with ch_name=None will already use any channels marked as EOG.
Unless I am missing something, this function does use only one EOG channel (arbitrarily chosen as the first in the list of EOG channels returned by _get_eog_channel_index) as mentioned by @Ali-pak. From the code of this function :
eog_inds = _get_eog_channel_index(ch_name, inst)
if len(eog_inds) > 2:
eog_inds = eog_inds[:1]
logger.info('Using EOG channel %s' % inst.ch_names[eog_inds[0]])
eog_chs = [inst.ch_names[k] for k in eog_inds]
Ahh sorry I was thinking of the EOG functions for SSP:
https://github.com/mne-tools/mne-python/blob/master/mne/preprocessing/eog.py#L55-L71
Even looking at that internally we eventually just pick one of the two channels to make events from.
It's maybe already a bug that ICA has if len(eog_inds) > 2:, because it means if you have exactly two you use two, if you have more than two, you use one -- this is weird. Someone will need to dig into the code a bit to see what the right thing is to do here, but it could be a bugfix just to use as many channels as are available by default (?).
It doesn't look like it will be too difficult to make it work, given that the internal find-bad functions can already take a list of channels. @christian-oreilly do you want to look into it?
@larsoner Sure, I can tackle this one since I would code an alternative version of this function anyway because I am not satisfied with the results I see in my data. The first idea that comes to mind is to run _find_bads_ch separately on each EOG channel and then combine the scores. I see two obvious choices for combination: max and mean. I think it would make sense to use max (as soon as the score is high on one EOG channel, you want to reject), although a case can be made for mean (more robust to outliers). I will check what I get on my data. Regardless of what metric is used by default, I would suggest giving the possibility to the user to change this function, maybe by using a supplementary optional arguments agg_func to find_bad_eog which would take for example "max" as default value but could also accept "mean" or a user-defined function (e.g., np.median). Sounds good? Any suggestions?
agg_func to find_bad_eog which would take for example "max" as default value but could also accept "mean" or a user-defined function (e.g., np.median). Sounds good? Any suggestions?
Sounds reasonable but first I would look to see what's already done in e.g. find_bads_ref. Presumably whatever aggegation / channel-based choices are made there can be made similarly in your function. It might just be a max/or-like operation already, in which case let's just stick with that unless in your testing you need mean (assume YAGNI until proven otherwise).
@larsoner From what I see from its sister functions (find_bads_ref, find_bads_ecg), they just collect a list of channels and pass it to _find_bads_ch. The function _find_bads_ch returns a list of the outlier components detected across channels (i.e., detected separately on each channel, then concatenated, and then removing duplicates).
From the code, I cannot see why we should not just remove
if len(eog_inds) > 2:
eog_inds = eog_inds[:1]
logger.info('Using EOG channel %s' % inst.ch_names[eog_inds[0]])
Removing this, the scores returned becomes a N x M matrix (instead of M-size array) so it would requires updating some functions (e.g., ica.plot_scores) calling find_bads_eog.
Sounds reasonable to me. @dengemann this is code from 6 years ago apparently in case you have input. But @christian-oreilly for now I'd say we should remove it and consider it a bug fix.
Ok. I'll submit a PR with just these line removed. Works well for me. ica.plot_scores also works out-of-the-box (it just generate more rows in the subplot figure)
Most helpful comment
Internally
find_bads_eogwithch_name=Nonewill already use any channels marked as EOG. But we should also allowch_nameto be a list instead of a singleton so that equivalent functionality can be achieved manually instead of based onch_type(for consistency at the very least).