Skip to content

Conversation

@sappelhoff
Copy link
Member

@sappelhoff sappelhoff commented Sep 29, 2022

I'd argue that the electrode positions as read from the Neuroscan CNT file header are in unknown space, see also the warning here: https://mne.tools/dev/auto_tutorials/io/20_reading_eeg_data.html#import-cnt

In the reader code, I see no indication of "sphere projection" and even if there was one, then we wouldn't know that the sphere corresponds to the "head" coord system.

follow up to:

fixes issue identified in mne-tools/mne-bids#1066 (comment)

Needed for mne-bids, would be nice to have in 1.2 release

CI issues seem unrelated

cc @larsoner

@sappelhoff sappelhoff requested a review from larsoner September 29, 2022 10:17
@sappelhoff sappelhoff added this to the 1.2 milestone Sep 29, 2022
@agramfort
Copy link
Member

any chance to add a test ? 🙏

@larsoner
Copy link
Member

@sappelhoff I see you have only modified the CNT reader here. In #11146 I messed with tests for at least curry, fieldtrip, and CNT. More importantly, though, I changed _test_raw_reader, which runs for every io type we have (or at least it should!). IIRC this is actually what caused me to change some of those other readers -- I asserted something that I thought should be the case in _test_raw_reader and it turned out not to be the case -- true TDD for once 😄 So I think to check the behavior here, you should add something to _test_raw_reader to ensure we're consistent about this across readers, then your CNT changes should follow from -- or at least be consistent with -- that change.

What should the consistency be? Ideally I guess that the dig coords for EEG should be in head coords if and only if there are fiducials also present in dig. Does that sound right? This would fail on main but pass on this PR for CNT, right?

Then the question is, how many other readers would this break, and should we also have them set to UNKNOWN in some cases...

Then another question -- probably for another PR -- is: are we then consistent between the coord frame we say in dig vs the one we say in info['chs']['coord_frame'].

Sorry for making this more complicated than it initially seemed, but hopefully this helps continue moving us toward being consistent with all this stuff across all reader!

@larsoner
Copy link
Member

@sappelhoff I think this is the last blocker for 1.2 release. Do you think you can get to it in the next few days? If not, I can push some commits

@sappelhoff
Copy link
Member Author

I was already afraid that it wouldn't be this simple 😉

My only expertise with this PR was that:

  1. this fixes mne-bids CI failures
  2. it's clear to me that elec positions read from a CNT header cannot possibly be in head coord system

Having that said, I agree that we should work on a more complete fix.

Ideally I guess that the dig coords for EEG should be in head coords if and only if there are fiducials also present in dig. Does that sound right?

Yes that sounds right to me! Except maybe for data formats that are guaranteed to ship electrode positions in "head" format (but nothing apart from BrainVision comes to mind). The coord system should be "unknown" otherwise.

Then another question -- probably for another PR -- is: are we then consistent between the coord frame we say in dig vs the one we say in info['chs']['coord_frame'].

I am not so clear about the distinction, and why there even needs to be one -- where should I read up on this?

@sappelhoff I think this is the last blocker for 1.2 release. Do you think you can get to it in the next few days? If not, I can push some commits

Unfortunately I can't -- I'll be on vacation starting next Sunday and need to wrap up some work things as well. But I can probably read comments and reply ... just not really dig into and write code (not before October 10th)

@larsoner
Copy link
Member

I am not so clear about the distinction, and why there even needs to be one -- where should I read up on this?

Not sure we have proper docs on it. But the TL;DR is that we store electrode information in two places: info['dig'] and in info['chs'][ii]['loc'][:3] (and ref in [3:6] when possible). For FIF these are always consistent, I think if you apply_montage they will be consistent, too. We've tried to keep them consistent in readers but I'm not sure if we have.

Moreover, historically the FIF format only allowed EEG points to have the FIFFV_COORD_HEAD coord frame for info['chs'][ii]['loc'] -- it wasn't even a value stored in the file, it was just assumed that if you had EEG electrodes they were in this frame. We recently (1-2 years ago?) added "extended channel block" support where you can say, for each channel, what its coord frame actually is. But we haven't used it much.

* upstream/main:
  Don't insert superfluous newlines in subprocess log messages (mne-tools#11219)
  purge _get_args helper func (mne-tools#11215)
  Standardize topomap args (mne-tools#11123)
  MAINT: Ensure no datasets are downloaded in tests (mne-tools#11213)
@larsoner
Copy link
Member

larsoner commented Oct 5, 2022

@agramfort @sappelhoff this keeps getting slightly more and more hacky, I had to work around this:

______________________ test_csd_degenerate[testing_data] _______________________
mne/preprocessing/tests/test_csd.py:156: in test_csd_degenerate
    raw = compute_current_source_density(raw)
mne/preprocessing/_csd.py:132: in compute_current_source_density
    radius, origin_head, origin_device = fit_sphere_to_headshape(inst.info)
mne/bem.py:1083: in _fit_sphere_to_headshape
    hsp = get_fitting_dig(info, dig_kinds)
mne/bem.py:1060: in get_fitting_dig
    raise RuntimeError('Digitization points not in head coordinates, '
E   RuntimeError: Digitization points not in head coordinates, contact mne-python developers

This is because in this PR, the info['dig'] are left in UNKNOWN coords (while the info['chs'] are in HEAD) :(

This all only really matters in the case where we are setting a montage (or equivalently, reading ch pos in a reader) in the UNKNOWN frame that does not have fiducials. I think I managed to convince myself that a better solution in this case is to insert synthetic fiducials when "converting" to the head coordinate frame. Compared to the current approach:

  • This requires creating/estimating some fiducials. At first this might seem like a disadvantage. But:
    1. This is fairly easy: estimate the average radius r of all EEG dig points, and put points at (-r, 0, 0), (0, r, 0), and (0, 0, r). (This ensures that the unknown->head is identity, so they won't move when the montage is converted to the head frame)
    2. This makes explicit what we're implicitly doing conceptually anyway in 1.1 by assigning info['dig'] and info['chs'] the HEAD frame. The extent to which this solution is wrong, it always has been wrong, it will just more obvious/clear/explicit rather than implicit now. (LPA/RPA/Nas generally don't perfectly lie on a sphere on real heads, but I think this is negligible for our purposes.)
    3. Increases usability of the data with coreg, MNE-BIDS, etc.
  • This ensures that info['dig'][ii]['coord_frame'] and info['chs'][ii]['coord_frame'] stay consistent, i.e., both in HEAD. In the current PR, chs will have HEAD and dig will have UNKNOWN.
  • This maintains backward compat with 1.1 better, where info['dig'] were always in HEAD coords for EEG readers, even when we aren't sure they use the head coordinate frame. (This PR's current method is to leave info['dig'] in UNKNOWN.)

In the long run, a solution where we do this synthetic insertion, then allow/improve the montage code to allow people to adjust the coordinate frame and/or these fiducial points seems better than introducing more inconsistencies.

@larsoner larsoner changed the title CNT header coords are in unknown space WIP: CNT header coords are in unknown space Oct 5, 2022
@drammock
Copy link
Member

drammock commented Oct 5, 2022

(LPA/RPA/Nas generally don't perfectly lie on a sphere on real heads, but I think this is negligible for our purposes.)

to this point, we could fit them to a spheroid (revolved ellipse) instead of a sphere. But allowing people to adjust the fiducials later may be enough / better.

@larsoner
Copy link
Member

larsoner commented Oct 5, 2022

Yeah we could -- but I think the simpler the better here

@sappelhoff
Copy link
Member Author

I agree with @larsoner's proposal, the backward compatibility and the part below convinced me specifically:

The extent to which this solution is wrong, it always has been wrong, it will just more obvious/clear/explicit rather than implicit now.

If this fixes our current issues and makes problems more obvious to fix in the future, while not changing behavior, I am fine with it! Thanks!

@larsoner larsoner changed the title WIP: CNT header coords are in unknown space MAINT: Add estimated fiducials when missing / assumed head coords Oct 5, 2022
@larsoner larsoner changed the title MAINT: Add estimated fiducials when missing / assumed head coords BUG: Add estimated fiducials when missing / assumed head coords Oct 5, 2022
@larsoner
Copy link
Member

larsoner commented Oct 5, 2022

Okay, proposal implemented. Ready for review/merge from my end

Comment on lines +489 to +494
# These should now be estimated from the data
# TODO: This is in meters... so clearly wrong. (The estimation is
# not the problem, the dig are all off like this.)
assert_allclose(pos['nasion'], [0, 9.97, 0], atol=1e-4)
assert_allclose(pos['lpa'], -pos['nasion'][[1, 0, 0]])
assert_allclose(pos['rpa'], pos['nasion'][[1, 0, 0]])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will open a new issue for this once this PR is merged

Copy link
Member

@agramfort agramfort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth running circle full to make sure the examples look good? I am thinking about our tutorials using EEG template MRI.

🙏 @sappelhoff @larsoner

return
# These should now be estimated from the data
# TODO: This is in meters... so clearly wrong. (The estimation is
# not the problem, the dig are all off like this.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a file in testing should be fixed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea if the test file is broken or our reading of it. The associated test appears to be written by @agramfort so maybe they'll be able to figure it out most quickly :)

larsoner and others added 2 commits October 5, 2022 15:43
Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>
@larsoner larsoner enabled auto-merge (squash) October 5, 2022 21:14
@larsoner larsoner merged commit ac7cab3 into mne-tools:main Oct 5, 2022
@sappelhoff
Copy link
Member Author

Thanks a lot @larsoner

larsoner added a commit to larsoner/mne-python that referenced this pull request Oct 11, 2022
* upstream/main: (64 commits)
  MAINT: Better check (mne-tools#11229)
  MAINT: Fix link and update instantiation note (mne-tools#11228)
  BUG: Add estimated fiducials when missing / assumed head coords (mne-tools#11212)
  Fix tfr db (mne-tools#11223)
  MAINT: Update link (mne-tools#11222)
  add CPGRL doc section (mne-tools#11216)
  Don't insert superfluous newlines in subprocess log messages (mne-tools#11219)
  purge _get_args helper func (mne-tools#11215)
  Standardize topomap args (mne-tools#11123)
  MAINT: Ensure no datasets are downloaded in tests (mne-tools#11213)
  MAINT: Fix Cirrus caching (mne-tools#11211)
  Fix mesh display in tutorial (mne-tools#11200)
  MAINT: Add arm64 CI using CirrusCI (mne-tools#11209)
  Fix spatial colors (mne-tools#11201)
  MAINT: Fix CircleCI error (mne-tools#11205) [circle deploy]
  Add regression-based approach to removing EOG artifacts (mne-tools#11046)
  [DOC, MRG] Minor documentation improvements and remove glossary entry for array-like (mne-tools#11207)
  Fix `include_tmax` not considered in `mne.io.Raw.crop` to check `tmax` in bounds (mne-tools#11204)
  MAINT: Fix notebook backend (mne-tools#11206)
  MRG: Fix displayed Raw duration in Jupyter notebook (mne-tools#11203)
  ...
@sappelhoff sappelhoff deleted the fix/cnt/coords/unknown branch December 7, 2023 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants