mne-python
mne-python copied to clipboard
Pupil size unit
Description of the problem
MNE version: 1.9.0 Operating system: Windows 11
When I was using the following code to read eye-movement data from .asc file. I found that the values of pupil size (i.e., pupil_right/pupil_left) were extremely large. They were typically 1000-5000 in v1.7.0, while they were 10^6 times larger in v1.9.0 (the raw data are the same). The issue still existed when I removed mne.preprocessing.eyetracking.convert_units.
cals = read_eyelink_calibration(self.edata_path)
first_cal = cals[0]
first_cal["screen_resolution"] = (2560, 1440)
first_cal["screen_size"] = (0.6, 0.35)
first_cal["screen_distance"] = 0.62
raw_edata = read_raw_eyelink(self.edata_path, create_annotations=["blinks", "saccades", "fixations", "messages"])
mne.preprocessing.eyetracking.convert_units(raw_edata, calibration=first_cal, to="radians")
raw_pick = raw_edata.pick(picks="eyetrack")
data = raw_pick.to_data_frame()
In v1.7.0, it is arbitrary units (AU), which makes sense. What is the pupil size unit in v1.9.0? I noticed that the unit is micrometers in the figure given in the document. However, the pupil size (pupil_right) is about 2x10^9 micrometers (i.e., 2km), which is impossible. Did I miss a parameter in any function? Or it is actually a bug.
Plus, I notice that it says
MNE-Python expects eyegaze positions to be in radians of visual angle, and pupil size to be in meters.
in the document. It could be better if the pupil size is millimeters or micrometers (AU is OK as well). Meters may be too large to describe pupil size.
Steps to reproduce
cals = read_eyelink_calibration(self.edata_path)
first_cal = cals[0]
first_cal["screen_resolution"] = (2560, 1440)
first_cal["screen_size"] = (0.6, 0.35)
first_cal["screen_distance"] = 0.62
raw_edata = read_raw_eyelink(self.edata_path, create_annotations=["blinks", "saccades", "fixations", "messages"])
mne.preprocessing.eyetracking.convert_units(raw_edata, calibration=first_cal, to="radians")
raw_pick = raw_edata.pick(picks="eyetrack")
data = raw_pick.to_data_frame()
Link to data
I'm sorry that I can't upload an .asc file, but I believe that you could evaluate with any .asc file.
Expected results
The values of pupil_right/pupil_left in data are typically 1000-5000. It is true in v1.7.0.
Actual results
The values in v1.9.0 are exactly 10^6 times larger than in v1.7.0.
Additional information
v1.7.0: Platform Windows-11-10.0.22631-SP0 Python 3.12.4 (tags/v3.12.4:8e8a4ba, Jun 6 2024, 19:30:16) [MSC v.1940 64 bit (AMD64)] Executable C:\Program Files\Python312\python.exe CPU Intel64 Family 6 Model 167 Stepping 1, GenuineIntel (16 cores) Memory Unavailable (requires "psutil" package) Core ├☒ mne 1.7.0 (outdated, release 1.9.0 is available!) ├☑ numpy 2.2.5 (unknown linalg bindings (threadpoolctl module not found: No module named 'threadpoolctl')) ├☑ scipy 1.15.3 └☑ matplotlib 3.10.3 (backend=module://backend_interagg) Numerical (optional) ├☑ pandas 2.2.3 └☐ unavailable sklearn, numba, nibabel, nilearn, dipy, openmeeg, cupy, h5io, h5py Visualization (optional) └☐ unavailable pyvista, pyvistaqt, vtk, qtpy, ipympl, pyqtgraph, mne-qt-browser, ipywidgets, trame_client, trame_server, trame_vtk, trame_vuetify Ecosystem (optional) └☐ unavailable mne-bids, mne-nirs, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline, neo, eeglabio, edfio, mffpy, pybv
v1.9.0: Platform Windows-11-10.0.22631-SP0 Python 3.12.4 (tags/v3.12.4:8e8a4ba, Jun 6 2024, 19:30:16) [MSC v.1940 64 bit (AMD64)] Executable C:\Program Files\Python312\python.exe CPU 11th Gen Intel(R) Core(TM) i9-11900K @ 3.50GHz (16 cores) Memory 63.7 GiB Core
- mne 1.9.0 (latest release)
- numpy 2.2.5 (unknown linalg bindings (threadpoolctl module not found: No module named 'threadpoolctl'))
- scipy 1.15.3
- matplotlib 3.10.3 (backend=module://backend_interagg) Numerical (optional)
- pandas 2.2.3
- unavailable sklearn, numba, nibabel, nilearn, dipy, openmeeg, cupy, h5io, h5py Visualization (optional)
- unavailable pyvista, pyvistaqt, vtk, qtpy, ipympl, pyqtgraph, mne-qt-browser, ipywidgets, trame_client, trame_server, trame_vtk, trame_vuetify Ecosystem (optional)
- unavailable mne-bids, mne-nirs, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline, neo, eeglabio, edfio, mffpy, pybv
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴
@scott-huberty ?
Sorry for the radio silence. The change you describe might have something to do with PR's that stemmed from this discussion.
But more directly, It seems like to_data_frame() is handling pupil channels incorrectly?
import numpy as np
import mne
sfreq = 100
n = sfreq + 0 # 10 seconds
ch_names = ["my_pupil"]
ch_types = "pupil"
info = mne.create_info(ch_names, sfreq, ch_types)
mydata = np.atleast_2d(np.random.randint(2, 8, n)) # data are in mm
raw = mne.io.RawArray(mydata, info, verbose=False)
# But let's explicitly convert data to Meters (SI)
ch_type_mapping_eye = {
"my_pupil": ("pupil", "mm", "right"),
}
raw = mne.preprocessing.eyetracking.set_channel_types_eyetrack(raw, ch_type_mapping_eye)
raw.get_data()[0, :10]
array([0.002, 0.004, 0.002, 0.003, 0.003, 0.004, 0.003, 0.006, 0.004,
0.005])
raw.to_data_frame().head(10) # the output values are 10^6 times larger
time my_pupil
0 0.00 2000.0
1 0.01 4000.0
2 0.02 2000.0
3 0.03 3000.0
4 0.04 3000.0
5 0.05 4000.0
6 0.06 3000.0
7 0.07 6000.0
8 0.08 4000.0
9 0.09 5000.0
It seems like to_data_frame is treating pupil channels as if they were EEG channels and is trying to convert from V -> µV? Honestly I would expect it to not do any conversion at all, or convert from meters -to mm (1e3)
A temporary hacky work around is to do:
raw.to_data_frame(scalings={"pupil": 1})
@drammock will you have time to look at this next week?
internally, default (plotting/axis-label) unit for pupil is µm
https://github.com/mne-tools/mne-python/blob/6ad882335644fb80f8744e508387c52f7231553a/mne/defaults.py#L96
and accordingly the default scaling is 1e6:
https://github.com/mne-tools/mne-python/blob/6ad882335644fb80f8744e508387c52f7231553a/mne/defaults.py#L125
therefore _handle_default("scalings", None) (which is what to_data_frame() method uses internally when no scalings are passed) returns a dict with entry "pupil": 1000000.0.
The default scaling for pupil channels was changed from 1 to 1e6 in #12846 by @scott-huberty. I don't really know off-the-cuff what the right move is here; AFAICT the to_data_frame method is working as expected.
Thanks @drammock I will look into it this Wednesday 7/9 and aim to have a PR (if necessary) same day.
@scott-huberty I'm going to start the release process, but we can always backport a fix if needed