Interpolation of bad channels in fNIRS data

  • MNE version: 0.24.0
  • MNE-NIRS version: 0.1.2
  • operating system: MacOS 12.01
  • Python version 3.9.7

Iā€™m trying to do preprocessing of fNIRS following the example provided in the MNE documentation. Unlike the documentation, I do have bad channels when applying SCI.

My question is what the MNE-nirs does to the channels labelled as ā€˜badā€™?

When plotting the raw_od before and after applying SCI, I can see that the bad channels have been ā€˜markedā€™ in the plot. But they are not removed and seemingly not interpolated in any way.

By lurking in the Github discussion of MNE-nirs, Iā€™ve found that the bad channels might be replaced with the mean signal of the nearest good channel. But Iā€™m not sure if this happens automatically, or if it is still a work in progress. (Link to github issue: ENH: Agenda for fNIRS processing Ā· Issue #7057 Ā· mne-tools/mne-python Ā· GitHub)

When I run the following code, it doesnā€™t work. So, everything points in the direction that interpolation of bad fNIRS channels is not fully implemented in th MNE-nirs yet. Or have I misunderstood something?

I wish to perform some sort of interpolation as I need to have the same number of channels per subject to run my analysis.

Thank you in advance!

Code is here:
raw_od.interpolate_bads(reset_bads = False, method = dict(fnirs = 'nearest'))

The error message is very long, but at the bottow the following is written:
RuntimeError: Digitization points not in head coordinates, contact mne-python developers

1 Like

Hello @sigridbom and welcome to the forum!

Iā€™m tagging @rob-luke, creator of MNE-NIRS :slight_smile:

2 Likes

Hi Sigrid and thanks @Richard,

My question is what the MNE-nirs does to the channels labelled as ā€˜badā€™?

When a channel is marked as bad it stays in the data, but the software will know it is a bad channel. How the bad channel is handled depends on the functions you then run. For example, if you plot the data using raw.plot() it will still show the bad channels, but they will be greyed out. Whereas, by default plot_compare_evokeds will not use bad channels by default. So for each downstream step you will want to check the documentation for how it handles bad channels.

Also this tutorial may be of use to you.
https://mne.tools/dev/auto_tutorials/preprocessing/15_handling_bad_channels.html

And the GLM analysis does not use bad channel information yet. There is an issue to do this at How to handle bad channels in GLM workflow Ā· Issue #279 Ā· mne-tools/mne-nirs Ā· GitHub so please chip in there if you have any additional thoughts.

When I run the following code, it doesnā€™t work. So, everything points in the direction that interpolation of bad fNIRS channels is not fully implemented in th MNE-nirs yet. Or have I misunderstood something?

interpolate_bads should work on fNIRS data. It is implemented. So lets try and figure out whatā€™s happening.

Thanks for sharing the code and error. From the error it looks like something is wrong with the montage. Can you answer the following questions to help us solve this issueā€¦:

  • What is the data you are using? What machine did it come from and how did you import it?
  • Can you share what raw_od.plot_sensors() produces
  • Can you provide a minimum working example code that demonstrates this bug?

Thanks,
Rob

2 Likes

Hi, thank you for the quick reply.

Thank you, Iā€™ll make sure to read those tutorials in detail. Iā€™d found them earlier, but didnā€™t think they necessarily applied to fNIRS as well.

Iā€™m using data from a research project. The data was collected using NIRSport2, which gives you data in .snirf format. Iā€™ll provide an example of manipulating one .snirf file, here:

# load file (called sub)
raw = mne.io.read_raw_snirf(sub)

#renaming triggers
raw.annotations.rename({'70': 'Visual',
                        '71': 'Visual',
                        '72': 'Visual',
                        '61': 'Auditory',
                        '62': 'Auditory', 
                        '63': 'Auditory'})

# choosing long channels
picks = mne.pick_types(raw.info, meg=False, fnirs=True)
dists = mne.preprocessing.nirs.source_detector_distances(
    raw.info, picks=picks)
raw.pick(picks[dists > 0.01])

# converting to optical density
raw_od = mne.preprocessing.nirs.optical_density(raw)

# applying SCI
sci = mne.preprocessing.nirs.scalp_coupling_index(raw_od)

# marking 'bad channels'
raw_od.info['bads'] = list(compress(raw_od.ch_names, sci < 0.2))

# trying to fix bad channels
raw_od.interpolate_bads(reset_bads = False, method = dict(fnirs = 'nearest'))

# error code

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
/var/folders/b0/t62rjb510wb79fxb5ch9n3kc0000gn/T/ipykernel_41185/3770898462.py in <module>
      1 # trying to fix bad channels
----> 2 raw_od.interpolate_bads(reset_bads = False, method = dict(fnirs = 'nearest'))
      3 
      4 #doesn't work

<decorator-gen-42> in interpolate_bads(self, reset_bads, mode, origin, method, exclude, verbose)

~/opt/anaconda3/lib/python3.9/site-packages/mne/channels/channels.py in interpolate_bads(self, reset_bads, mode, origin, method, exclude, verbose)
   1179             return self
   1180         logger.info('Interpolating bad channels')
-> 1181         origin = _check_origin(origin, self.info)
   1182         if method['eeg'] == 'spline':
   1183             _interpolate_bads_eeg(self, origin=origin, exclude=exclude)

~/opt/anaconda3/lib/python3.9/site-packages/mne/bem.py in _check_origin(origin, info, coord_frame, disp)
    993                              'not %s' % (origin,))
    994         if coord_frame == 'head':
--> 995             R, origin = fit_sphere_to_headshape(info, verbose=False,
    996                                                 units='m')[:2]
    997             logger.info('    Automatic origin fit: head of radius %0.1f mm'

<decorator-gen-60> in fit_sphere_to_headshape(info, dig_kinds, units, verbose)

~/opt/anaconda3/lib/python3.9/site-packages/mne/bem.py in fit_sphere_to_headshape(info, dig_kinds, units, verbose)
    847     if not isinstance(units, str) or units not in ('m', 'mm'):
    848         raise ValueError('units must be a "m" or "mm"')
--> 849     radius, origin_head, origin_device = _fit_sphere_to_headshape(
    850         info, dig_kinds)
    851     if units == 'mm':

<decorator-gen-62> in _fit_sphere_to_headshape(info, dig_kinds, verbose)

~/opt/anaconda3/lib/python3.9/site-packages/mne/bem.py in _fit_sphere_to_headshape(info, dig_kinds, verbose)
    931 def _fit_sphere_to_headshape(info, dig_kinds, verbose=None):
    932     """Fit a sphere to the given head shape."""
--> 933     hsp = get_fitting_dig(info, dig_kinds)
    934     radius, origin_head = _fit_sphere(np.array(hsp), disp=False)
    935     # compute origin in device coordinates

<decorator-gen-61> in get_fitting_dig(info, dig_kinds, exclude_frontal, verbose)

~/opt/anaconda3/lib/python3.9/site-packages/mne/bem.py in get_fitting_dig(info, dig_kinds, exclude_frontal, verbose)
    891             # try "extra" first
    892             try:
--> 893                 return get_fitting_dig(info, 'extra')
    894             except ValueError:
    895                 pass

<decorator-gen-61> in get_fitting_dig(info, dig_kinds, exclude_frontal, verbose)

~/opt/anaconda3/lib/python3.9/site-packages/mne/bem.py in get_fitting_dig(info, dig_kinds, exclude_frontal, verbose)
    908     hsp = [p['r'] for p in info['dig'] if p['kind'] in dig_kinds]
    909     if any(p['coord_frame'] != FIFF.FIFFV_COORD_HEAD for p in info['dig']):
--> 910         raise RuntimeError('Digitization points not in head coordinates, '
    911                            'contact mne-python developers')
    912 

RuntimeError: Digitization points not in head coordinates, contact mne-python developers

#plot
raw_od.plot_sensors()

plot_sensors

Best,
Sigrid

1 Like

First, great summary @sigridbom! And you code looks good (you arenā€™t using any weird functions and it should support bad channels), so I think this could either be a bug or small quirk. We will solve this. However, I am knocking off for the day, and probably wonā€™t be able to dig in to this till Monday.

But in the meantime, the data optode frame may need to be specified. As you have a nirx device you may wish to set the coordinate frame of the optodes using raw = mne.io.read_raw_snirf(sub, optode_frame="mri") see Importing data from fNIRS devices ā€” MNE 1.0.dev0 documentation

Does that fix things? If not, I can dig further early next week. I think I have a few NIRSport2 files lying around I can test.

1 Like

The code works when I add optode_frame = "mri" when importing the file! When plotting, it also looks like interpolation takes place. Thank you so much.

However, when looking at the provided link, I found the following text:
ā€œMNE-Python only supports NIRx files recorded with NIRStar version 15.0 and above. MNE-Python supports reading data from NIRScout and NIRSport 1 devices.ā€

We didnā€™t use NIRStar but Aurora to record the data, which is the accompanying software to NIRSport 2 devices. I hope this is not a problem, though the text does imply that it isā€¦

1 Like

We didnā€™t use NIRStar but Aurora to record the data, which is the accompanying software to NIRSport 2 devices. I hope this is not a problem, though the text does imply that it isā€¦

Thanks @sigridbom for noting this. MNE is compatible with NIRSport2, we just forgot to update the docs. Thanks for pointing this out, I will update it.

2 Likes