mne-python icon indicating copy to clipboard operation
mne-python copied to clipboard

Pupil size unit

Open auracionjyl opened this issue 7 months ago • 2 comments

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

auracionjyl avatar May 12 '25 13:05 auracionjyl

Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴

welcome[bot] avatar May 12 '25 13:05 welcome[bot]

@scott-huberty ?

larsoner avatar May 12 '25 18:05 larsoner

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})

scott-huberty avatar Jun 23 '25 19:06 scott-huberty

@drammock will you have time to look at this next week?

larsoner avatar Jul 03 '25 13:07 larsoner

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.

drammock avatar Jul 04 '25 17:07 drammock

Thanks @drammock I will look into it this Wednesday 7/9 and aim to have a PR (if necessary) same day.

scott-huberty avatar Jul 07 '25 16:07 scott-huberty

@scott-huberty I'm going to start the release process, but we can always backport a fix if needed

larsoner avatar Jul 10 '25 14:07 larsoner