Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reading 3D coordinates from snirf produces different result than read_custom_montage() #545

Open
kdarti opened this issue Mar 28, 2024 · 7 comments

Comments

@kdarti
Copy link

kdarti commented Mar 28, 2024

Describe the bug

I'm not completely sure that this is a bug, and I'm not sure it's even expected that you'd get the same results when automatically reading coordinates from a snirf file as when reading the same coordinates using read_custom_montage().

Steps to reproduce

20240327-snirf3d-mne.zip

import mne
mne.io.read_raw_snirf(r"2x12_nz-to-nasion.snirf").get_montage().plot()
mne.channels.read_custom_montage(r"digitisation_2x12.elc").plot()
mne.io.read_raw_snirf(r"2x12_nz-to-nasion.snirf").get_montage().get_positions()
mne.channels.read_custom_montage(r"digitisation_2x12.elc").get_positions()

Expected results

I'd expect the resulting coordinates to be the same, given the coordinates in the .snirf and the .elc files are identical.

The difference seems to lie here:

https://github.com/mne-tools/mne-python/blob/eee8e6fe580034f4a3a4fb13bdca3bfc99240708/mne/channels/_standard_montage_utils.py#L274

If I comment out that line I get identical results.

Actual results

image
image

I should say that the digitisation is bad and not realistic, but the result that I expect is what I get from read_custom_montage()

mne.io.read_raw_snirf(r"2x12_nz-to-nasion.snirf").get_montage().get_positions()
Loading ...\2x12_nz-to-nasion.snirf
Out[221]: 
{'ch_pos': OrderedDict([('S1', array([0.05263, 0.04361, 0.10604])),
              ('S2', array([-0.0598 ,  0.03692,  0.10477])),
              ('S3', array([-0.00454,  0.09951,  0.07741])),
              ('S4', array([0.05279, 0.11416, 0.00981])),
              ('S5', array([-0.06111,  0.11034,  0.00243])),
              ('S6', array([ 0.06122, -0.1047 ,  0.0347 ])),
              ('S7', array([-0.0399 , -0.10976,  0.04771])),
              ('S8', array([ 0.01618, -0.07903,  0.09699])),
              ('S9', array([ 0.07706, -0.03319,  0.09421])),
              ('S10', array([-0.04648, -0.03294,  0.11256])),
              ('D1', array([-0.00076,  0.04186,  0.119  ])),
              ('D2', array([0.05333, 0.08289, 0.07874])),
              ('D3', array([-0.06278,  0.08358,  0.07064])),
              ('D4', array([-0.0044 ,  0.12561,  0.01085])),
              ('D5', array([ 0.01436, -0.11273,  0.05478])),
              ('D6', array([ 0.07158, -0.08069,  0.06542])),
              ('D7', array([-0.03738, -0.07695,  0.09272])),
              ('D8', array([ 0.01718, -0.03431,  0.12018]))]),
 'coord_frame': 'unknown',
 'nasion': array([-0.001,  0.084, -0.043]),
 'lpa': array([-0.0825, -0.018 , -0.048 ]),
 'rpa': array([ 0.081, -0.019, -0.048]),
 'hsp': None,
 'hpi': None}

mne.channels.read_custom_montage(r"digitisation_2x12.elc").get_positions()
Out[222]: 
{'ch_pos': OrderedDict([('D1', array([-0.00057231,  0.03152226,  0.08961177])),
              ('D2', array([0.04015963, 0.06241949, 0.05929438])),
              ('D3', array([-0.04727586,  0.06293909,  0.05319475])),
              ('D4', array([-0.00331338,  0.09458937,  0.00817049])),
              ('D5', array([ 0.01081366, -0.08489021,  0.04125154])),
              ('D6', array([ 0.05390261, -0.06076281,  0.04926388])),
              ('D7', array([-0.02814864, -0.05794644,  0.06982188])),
              ('D8', array([ 0.01293723, -0.02583681,  0.09050036])),
              ('S1', array([0.0396325 , 0.03284008, 0.07985237])),
              ('S2', array([-0.0450318 ,  0.02780224,  0.07889601])),
              ('S3', array([-0.0034188 ,  0.07493502,  0.05829283])),
              ('S4', array([0.03975299, 0.08596706, 0.00738732])),
              ('S5', array([-0.04601828,  0.08309044,  0.00182989])),
              ('S6', array([ 0.04610111, -0.0788433 ,  0.02613049])),
              ('S7', array([-0.0300463 , -0.08265368,  0.03592754])),
              ('S8', array([ 0.01218419, -0.05951276,  0.07303736])),
              ('S9', array([ 0.05802927, -0.0249934 ,  0.07094391])),
              ('S10', array([-0.0350013 , -0.02480514,  0.08476219]))]),
 'coord_frame': 'unknown',
 'nasion': array([-0.00075304,  0.06325537, -0.03238072]),
 'lpa': array([-0.06212581, -0.01355472, -0.03614592]),
 'rpa': array([ 0.06099625, -0.01430776, -0.03614592]),
 'hsp': None,
 'hpi': None}

Additional information

Platform Windows-10-10.0.19045-SP0
Python 3.12.2 | packaged by conda-forge | (main, Feb 16 2024, 20:42:31) [MSC v.1937 64 bit (AMD64)]
Executable C:\Users\kdahlslatt\Anaconda3\envs\mne\python.exe
CPU Intel64 Family 6 Model 158 Stepping 10, GenuineIntel (12 cores)
Memory 15.7 GB

Core
├☑ mne 1.6.1 (latest release)
├☑ numpy 1.26.4 (OpenBLAS 0.3.26 with 12 threads)
├☑ scipy 1.12.0
├☑ matplotlib 3.8.3 (backend=Qt5Agg)
├☑ pooch 1.8.1
└☑ jinja2 3.1.3

Numerical (optional)
├☑ sklearn 1.4.1.post1
├☑ numba 0.59.1
├☑ nibabel 5.2.1
├☑ nilearn 0.10.3
├☑ dipy 1.9.0
├☑ openmeeg 2.5.7
├☑ pandas 2.2.1
└☐ unavailable cupy

Visualization (optional)
├☑ pyvista 0.43.4 (OpenGL 4.5.0 - Build 30.0.101.1404 via Intel(R) UHD Graphics 630)
├☑ pyvistaqt 0.11.0
├☑ vtk 9.2.6
├☑ qtpy 2.4.1 (PyQt5=5.15.8)
├☑ pyqtgraph 0.13.4
├☑ mne-qt-browser 0.6.2
├☑ ipywidgets 8.1.2
├☑ trame_client 2.16.5
├☑ trame_server 2.17.2
├☑ trame_vtk 2.8.5
├☑ trame_vuetify 2.4.3
└☐ unavailable ipympl

Ecosystem (optional)
├☑ mne-nirs 0.6.0
└☐ unavailable mne-bids, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline

@larsoner
Copy link
Member

If it's from that line then I think it's expected, and if you pass head_size=None it shouldn't modify it. Can you check?

@rob-luke
Copy link
Member

Thanks for sharing @kdarti , and great detailed issue.
For debugging purposes, how did you acquire these files? What device was the data collected on and how was the snirf file and elc file generated? (did the manafacturer device create the files?)
I will download and examine the files myself ASAP

@kdarti
Copy link
Author

kdarti commented Apr 3, 2024

Thanks for sharing @kdarti , and great detailed issue. For debugging purposes, how did you acquire these files? What device was the data collected on and how was the snirf file and elc file generated? (did the manafacturer device create the files?) I will download and examine the files myself ASAP

Hi Rob, it's Kristoffer from Artinis, we just implemented export of digitised 3d positions to snirf files in our main software, so that's the origin of the snirf file. I made the .elc manually, based on the coordinates in the snirf.

If it's from that line then I think it's expected, and if you pass head_size=None it shouldn't modify it. Can you check?

Yep, with that added I do get the same behavior with the .elc using read_custom_montage() as with the coordinates from the snirf file.

However, then the positions are not really what I expect. Below is a screenshot from our software (where the positions were digitised and from where the .snirf file was exported), showing the digitised positions in our 3d vis, along with a 3d vis of the positions using read_custom_montage() + .elc (not using head_size=None). Meaning, I do get the expected positions with read_customer_montage() + .elc, but not with .snirf file.

image

@larsoner
Copy link
Member

larsoner commented Apr 3, 2024

So if things are okay with read_custom_montage(..., head_size=None) but not with read_raw_snirf, then adding a head_size=None default kwarg to read_raw_snirf should fix it in theory I think right?

@kdarti
Copy link
Author

kdarti commented Apr 3, 2024

So if things are okay with read_custom_montage(..., head_size=None) but not with read_raw_snirf, then adding a head_size=None default kwarg to read_raw_snirf should fix it in theory I think right?

Maybe I was a bit unclear, if I use read_custom_montage(..., head_size=None) then the positions are identical to what I get from read_raw_snirf, but these positions are not what I'd expect given the positions in the software from which the positions were exported. read_custom_montage()` is what provides the positions I'd expect.

@larsoner
Copy link
Member

larsoner commented Apr 3, 2024

Okay -- if read_custom_montage(fname) is fine, then that's the same as read_custom_montage(fname, head_size=0.095) (since head_size=0.095 is the default). So then adding a head_size=None to read_raw_snirf that you could set to head_size=0.095 should fix things?

@kdarti
Copy link
Author

kdarti commented May 29, 2024

Okay -- if read_custom_montage(fname) is fine, then that's the same as read_custom_montage(fname, head_size=0.095) (since head_size=0.095 is the default). So then adding a head_size=None to read_raw_snirf that you could set to head_size=0.095 should fix things?

Sorry, forgot to reply to this. Having a head_size parameter for read_raw_snirf that functions the same way as the same parameter for read_custom_montage() would indeed help.

Tbh, I'm not even sure what I should expect when reading the coordinates from a .snirf file, all I know is that I got a mismatch when reading identical coordinates using two different functions.

I think @rob-luke would have to say what is actually the intended result when reading the coordinates from a .snirf file.

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

No branches or pull requests

3 participants