mne-nirs
mne-nirs copied to clipboard
Reading 3D coordinates from snirf produces different result than read_custom_montage()
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
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
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
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?
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
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.
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?
So if things are okay with
read_custom_montage(..., head_size=None)
but not withread_raw_snirf
, then adding ahead_size=None
default kwarg toread_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.
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?
Okay -- if
read_custom_montage(fname)
is fine, then that's the same asread_custom_montage(fname, head_size=0.095)
(sincehead_size=0.095
is the default). So then adding ahead_size=None
toread_raw_snirf
that you could set tohead_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.