diff --git a/docs/source/bibliography.bib b/docs/source/bibliography.bib index 0992286e2..3a7ab8bbd 100644 --- a/docs/source/bibliography.bib +++ b/docs/source/bibliography.bib @@ -65,3 +65,14 @@ @article{GazeBaseVR journal = {Scientific Data}, doi = {10.1038/s41597-023-02075-5}, } + +@article{GazeOnFaces, + title={Face exploration dynamics differentiate men and women}, + author={Coutrot, Antoine and Binetti, Nicola and Harrison, Charlotte and Mareschal, Isabelle and Johnston, Alan}, + journal={Journal of vision}, + volume={16}, + number={14}, + pages={16--16}, + year={2016}, + publisher={The Association for Research in Vision and Ophthalmology} +} diff --git a/src/pymovements/datasets/__init__.py b/src/pymovements/datasets/__init__.py index b1eb00ee2..75597101c 100644 --- a/src/pymovements/datasets/__init__.py +++ b/src/pymovements/datasets/__init__.py @@ -27,6 +27,7 @@ pymovements.datasets.GazeBase pymovements.datasets.GazeBaseVR + pymovements.datasets.GazeOnFaces pymovements.datasets.JuDo1000 @@ -39,6 +40,7 @@ pymovements.datasets.ToyDataset pymovements.datasets.ToyDatasetEyeLink """ +from pymovements.datasets.gaze_on_faces import GazeOnFaces from pymovements.datasets.gazebase import GazeBase from pymovements.datasets.gazebasevr import GazeBaseVR from pymovements.datasets.judo1000 import JuDo1000 @@ -49,6 +51,7 @@ __all__ = [ 'GazeBase', 'GazeBaseVR', + 'GazeOnFaces', 'JuDo1000', 'ToyDataset', 'ToyDatasetEyeLink', diff --git a/src/pymovements/datasets/gaze_on_faces.py b/src/pymovements/datasets/gaze_on_faces.py new file mode 100644 index 000000000..0a5795dfe --- /dev/null +++ b/src/pymovements/datasets/gaze_on_faces.py @@ -0,0 +1,149 @@ +# Copyright (c) 2022-2023 The pymovements Project Authors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""This module provides an interface to the GazeOnFaces dataset.""" +from __future__ import annotations + +from dataclasses import dataclass +from dataclasses import field +from typing import Any + +import polars as pl + +from pymovements.dataset.dataset_definition import DatasetDefinition +from pymovements.dataset.dataset_library import register_dataset +from pymovements.gaze.experiment import Experiment + + +@dataclass +@register_dataset +class GazeOnFaces(DatasetDefinition): + """GazeBaseVR dataset :cite:p:`GazeOnFaces`. + + This dataset includes monocular eye tracking data from single participants in a single + session. Eye movements are recorded at a sampling frequency of 60 Hz + using an EyeLink 1000 video-based eye tracker and are provided as pixel coordinates. + + Participants were sat 57 cm away from the screen (19inch LCD monitor, + screen res=1280×1024, 60 Hz). Recordings of the eye movements of one eye in monocular + pupil/corneal reflection tracking mode. + + Check the respective paper for details :cite:p:`GazeOnFaces`. + + Attributes + ---------- + name : str + The name of the dataset. + + mirrors : tuple[str, ...] + A tuple of mirrors of the dataset. Each entry must be of type `str` and end with a '/'. + + resources : tuple[dict[str, str], ...] + A tuple of dataset resources. Each list entry must be a dictionary with the following keys: + - `resource`: The url suffix of the resource. This will be concatenated with the mirror. + - `filename`: The filename under which the file is saved as. + - `md5`: The MD5 checksum of the respective file. + + experiment : Experiment + The experiment definition. + + filename_format : str + Regular expression which will be matched before trying to load the file. Namedgroups will + appear in the `fileinfo` dataframe. + + filename_format_dtypes : dict[str, type], optional + If named groups are present in the `filename_format`, this makes it possible to cast + specific named groups to a particular datatype. + + column_map : dict[str, str] + The keys are the columns to read, the values are the names to which they should be renamed. + + custom_read_kwargs : dict[str, Any], optional + If specified, these keyword arguments will be passed to the file reading function. + + Examples + -------- + Initialize your :py:class:`~pymovements.PublicDataset` object with the + :py:class:`~pymovements.GazeOnFaces` definition: + + >>> import pymovements as pm + >>> + >>> dataset = pm.Dataset("GazeOnFaces", path='data/GazeOnFaces') + + Download the dataset resources resources: + + >>> dataset.download()# doctest: +SKIP + + Load the data into memory: + + >>> dataset.load()# doctest: +SKIP + """ + + # pylint: disable=similarities + # The PublicDatasetDefinition child classes potentially share code chunks for definitions. + + name: str = 'GazeOnFaces' + + mirrors: tuple[str, ...] = ( + 'https://uncloud.univ-nantes.fr/index.php/s/', + ) + + resources: tuple[dict[str, str], ...] = ( + { + 'resource': '8KW6dEdyBJqxpmo/download?path=%2F&files=gaze_csv.zip', + 'filename': 'gaze_csv.zip', + 'md5': 'fe219f07c9253cd9aaee6bd50233c034', + }, + ) + + experiment: Experiment = Experiment( + screen_width_px=1280, + screen_height_px=1024, + screen_width_cm=38, + screen_height_cm=30, + distance_cm=57, + origin='center', + sampling_rate=60, + ) + + filename_format: str = r'gaze_sub{sub_id:d}_trial{trial_id:d}.csv' + + filename_format_dtypes: dict[str, type] = field( + default_factory=lambda: { + 'sub_id': int, + 'trial_id': int, + }, + ) + + trial_columns: list[str] = field(default_factory=lambda: ['sub_id', 'trial_id']) + + time_column: Any = None + + pixel_columns: list[str] = field(default_factory=lambda: ['x', 'y']) + + column_map: dict[str, str] = field(default_factory=lambda: {}) + + custom_read_kwargs: dict[str, Any] = field( + default_factory=lambda: { + 'separator': ',', + 'has_header': False, + 'new_columns': ['x', 'y'], + 'dtypes': [pl.Float32, pl.Float32], + }, + ) diff --git a/tests/datasets/datasets_test.py b/tests/datasets/datasets_test.py index 6df6db7d0..074002c10 100644 --- a/tests/datasets/datasets_test.py +++ b/tests/datasets/datasets_test.py @@ -31,6 +31,7 @@ pytest.param(pm.datasets.ToyDataset, 'ToyDataset', id='ToyDataset'), pytest.param(pm.datasets.GazeBase, 'GazeBase', id='GazeBase'), pytest.param(pm.datasets.GazeBaseVR, 'GazeBaseVR', id='GazeBaseVR'), + pytest.param(pm.datasets.GazeOnFaces, 'GazeOnFaces', id='GazeOnFaces'), pytest.param(pm.datasets.JuDo1000, 'JuDo1000', id='JuDo1000'), ], ) @@ -46,6 +47,7 @@ def test_public_dataset_registered(definition_class, dataset_name): pytest.param(pm.datasets.ToyDataset, id='ToyDataset'), pytest.param(pm.datasets.GazeBase, id='GazeBase'), pytest.param(pm.datasets.GazeBaseVR, id='GazeBaseVR'), + pytest.param(pm.datasets.GazeOnFaces, id='GazeOnFaces'), pytest.param(pm.datasets.JuDo1000, id='JuDo1000'), ], ) diff --git a/tests/datasets/gaze_on_faces_test.py b/tests/datasets/gaze_on_faces_test.py new file mode 100644 index 000000000..86e28e444 --- /dev/null +++ b/tests/datasets/gaze_on_faces_test.py @@ -0,0 +1,79 @@ +# Copyright (c) 2023 The pymovements Project Authors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Test all functionality in pymovements.dataset.gaze_on_faces.""" +from pathlib import Path + +import pytest + +import pymovements as pm + + +@pytest.mark.parametrize( + 'init_path, expected_paths', + [ + pytest.param( + '/data/set/path', + { + 'root': Path('/data/set/path/'), + 'dataset': Path('/data/set/path/'), + 'download': Path('/data/set/path/downloads'), + }, + ), + pytest.param( + pm.DatasetPaths(root='/data/set/path'), + { + 'root': Path('/data/set/path/'), + 'dataset': Path('/data/set/path/GazeOnFaces'), + 'download': Path('/data/set/path/GazeOnFaces/downloads'), + }, + ), + pytest.param( + pm.DatasetPaths(root='/data/set/path', dataset='.'), + { + 'root': Path('/data/set/path/'), + 'dataset': Path('/data/set/path/'), + 'download': Path('/data/set/path/downloads'), + }, + ), + pytest.param( + pm.DatasetPaths(root='/data/set/path', dataset='dataset'), + { + 'root': Path('/data/set/path/'), + 'dataset': Path('/data/set/path/dataset'), + 'download': Path('/data/set/path/dataset/downloads'), + }, + ), + pytest.param( + pm.DatasetPaths(root='/data/set/path', downloads='custom_downloads'), + { + 'root': Path('/data/set/path/'), + 'dataset': Path('/data/set/path/GazeOnFaces'), + 'download': Path('/data/set/path/GazeOnFaces/custom_downloads'), + }, + ), + ], +) +def test_paths(init_path, expected_paths): + dataset = pm.Dataset(pm.datasets.GazeOnFaces, path=init_path) + + assert dataset.paths.root == expected_paths['root'] + assert dataset.path == expected_paths['dataset'] + assert dataset.paths.dataset == expected_paths['dataset'] + assert dataset.paths.downloads == expected_paths['download']