From d23b81f9f4f18e84d1fbbda5c0171ed6ee88cae0 Mon Sep 17 00:00:00 2001 From: prassepaul Date: Fri, 15 Sep 2023 09:13:07 +0200 Subject: [PATCH 01/10] added from_csv (#504) --- src/pymovements/gaze/__init__.py | 2 + src/pymovements/gaze/io.py | 157 ++++++++++++++++++++++ tests/gaze/io/csv_test.py | 48 +++++++ tests/gaze/io/files/binocular_example.csv | 11 ++ tests/gaze/io/files/monocular_example.csv | 11 ++ 5 files changed, 229 insertions(+) create mode 100644 src/pymovements/gaze/io.py create mode 100644 tests/gaze/io/csv_test.py create mode 100644 tests/gaze/io/files/binocular_example.csv create mode 100644 tests/gaze/io/files/monocular_example.csv diff --git a/src/pymovements/gaze/__init__.py b/src/pymovements/gaze/__init__.py index b8bcec64d..0e2ceed5a 100644 --- a/src/pymovements/gaze/__init__.py +++ b/src/pymovements/gaze/__init__.py @@ -62,6 +62,7 @@ from pymovements.gaze.gaze_dataframe import GazeDataFrame from pymovements.gaze.integration import from_numpy from pymovements.gaze.integration import from_pandas +from pymovements.gaze.io import from_csv from pymovements.gaze.screen import Screen @@ -73,4 +74,5 @@ 'Screen', 'transforms_numpy', 'transforms', + 'from_csv', ] diff --git a/src/pymovements/gaze/io.py b/src/pymovements/gaze/io.py new file mode 100644 index 000000000..ad59dbd6b --- /dev/null +++ b/src/pymovements/gaze/io.py @@ -0,0 +1,157 @@ +# 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. +"""Functionality to load GazeDataFrame from a csv file.""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import polars as pl + +from pymovements.gaze import Experiment +from pymovements.gaze.gaze_dataframe import GazeDataFrame + + +def from_csv( + file: str | Path, + experiment: Experiment | None = None, + *, + trial_columns: list[str] | None = None, + time_column: str | None = None, + pixel_columns: list[str] | None = None, + position_columns: list[str] | None = None, + velocity_columns: list[str] | None = None, + acceleration_columns: list[str] | None = None, + **read_csv_kwargs: Any, +) -> GazeDataFrame: + """Initialize a :py:class:`pymovements.gaze.gaze_dataframe.GazeDataFrame`. + + Parameters + ---------- + file: + Path of gaze file. + experiment : Experiment + The experiment definition. + trial_columns: + The name of the trial columns in the input data frame. If the list is empty or None, + the input data frame is assumed to contain only one trial. If the list is not empty, + the input data frame is assumed to contain multiple trials and the transformation + methods will be applied to each trial separately. + time_column: + The name of the timestamp column in the input data frame. + pixel_columns: + The name of the pixel position columns in the input data frame. These columns will be + nested into the column ``pixel``. If the list is empty or None, the nested ``pixel`` + column will not be created. + position_columns: + The name of the dva position columns in the input data frame. These columns will be + nested into the column ``position``. If the list is empty or None, the nested + ``position`` column will not be created. + velocity_columns: + The name of the velocity columns in the input data frame. These columns will be nested + into the column ``velocity``. If the list is empty or None, the nested ``velocity`` + column will not be created. + acceleration_columns: + The name of the acceleration columns in the input data frame. These columns will be + nested into the column ``acceleration``. If the list is empty or None, the nested + ``acceleration`` column will not be created. + **read_csv_kwargs: + Additional keyword arguments to be passed to polars to read in the csv. + + Notes + ----- + About using the arguments ``pixel_columns``, ``position_columns``, ``velocity_columns``, + and ``acceleration_columns``: + + By passing a list of columns as any of these arguments, these columns will be merged into a + single column with the corresponding name , e.g. using `pixel_columns` will merge the + respective columns into the column `pixel`. + + The supported number of component columns with the expected order are: + + * zero columns: No nested component column will be created. + * two columns: monocular data; expected order: x-component, y-component + * four columns: binocular data; expected order: x-component left eye, y-component left eye, + x-component right eye, y-component right eye, + * six columns: binocular data with additional cyclopian data; expected order: x-component + left eye, y-component left eye, x-component right eye, y-component right eye, + x-component cyclopian eye, y-component cyclopian eye, + + + Examples + -------- + First let's assume a CSV file stored `sample.csv` with the following content: + shape: (10, 3) + ┌──────┬────────────┬────────────┐ + │ time ┆ x_left_pix ┆ y_left_pix │ + │ --- ┆ --- ┆ --- │ + │ i64 ┆ i64 ┆ i64 │ + ╞══════╪════════════╪════════════╡ + │ 0 ┆ 0 ┆ 0 │ + │ 0 ┆ 0 ┆ 0 │ + │ 0 ┆ 0 ┆ 0 │ + │ 0 ┆ 0 ┆ 0 │ + │ … ┆ … ┆ … │ + │ 0 ┆ 0 ┆ 0 │ + │ 0 ┆ 0 ┆ 0 │ + │ 0 ┆ 0 ┆ 0 │ + │ 0 ┆ 0 ┆ 0 │ + └──────┴────────────┴────────────┘ + + We can now load the data into a ``GazeDataFrame`` by specyfing the experimental setting + and the names of the pixel position columns. + + >>> gaze = from_csv(file='sample.csv', + time_column = 'time', + pixel_columns = ['x_left_pix','y_left_pix'],) + >>> gaze.frame + shape: (10, 2) + ┌──────┬───────────┐ + │ time ┆ pixel │ + │ --- ┆ --- │ + │ i64 ┆ list[i64] │ + ╞══════╪═══════════╡ + │ 0 ┆ [0, 0] │ + │ 0 ┆ [0, 0] │ + │ 0 ┆ [0, 0] │ + │ 0 ┆ [0, 0] │ + │ … ┆ … │ + │ 0 ┆ [0, 0] │ + │ 0 ┆ [0, 0] │ + │ 0 ┆ [0, 0] │ + │ 0 ┆ [0, 0] │ + └──────┴───────────┘ + + """ + # read data + gaze_data = pl.read_csv(file, **read_csv_kwargs) + + # create gaze data frame + gaze_df = GazeDataFrame( + gaze_data, + experiment=experiment, + trial_columns=trial_columns, + time_column=time_column, + pixel_columns=pixel_columns, + position_columns=position_columns, + velocity_columns=velocity_columns, + acceleration_columns=acceleration_columns, + ) + return gaze_df diff --git a/tests/gaze/io/csv_test.py b/tests/gaze/io/csv_test.py new file mode 100644 index 000000000..c64264ebe --- /dev/null +++ b/tests/gaze/io/csv_test.py @@ -0,0 +1,48 @@ +# 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 read from csv.""" +import pytest + +import pymovements as pm + + +@pytest.mark.parametrize( + ('kwargs', 'shape'), + [ + pytest.param( + {'file': 'tests/gaze/io/files/monocular_example.csv', + 'time_column': 'time', 'pixel_columns': ['x_left_pix', 'y_left_pix']}, + (10, 2), + id='csv_mono_shape', + ), + pytest.param( + {'file': 'tests/gaze/io/files/binocular_example.csv', + 'time_column': 'time', + 'pixel_columns': ['x_left_pix', 'y_left_pix', 'x_right_pix', 'y_right_pix'], + 'position_columns': ['x_left_pos', 'y_left_pos', 'x_right_pos', 'y_right_pos']}, + (10, 3), + id='csv_bino_shape', + ), + ] +) +def test_shapes(kwargs, shape): + gaze_dataframe = pm.gaze.from_csv(**kwargs) + + assert gaze_dataframe.frame.shape == shape diff --git a/tests/gaze/io/files/binocular_example.csv b/tests/gaze/io/files/binocular_example.csv new file mode 100644 index 000000000..e6672e317 --- /dev/null +++ b/tests/gaze/io/files/binocular_example.csv @@ -0,0 +1,11 @@ +time,x_left_pix,y_left_pix,x_right_pix,y_right_pix,x_left_pos,y_left_pos,x_right_pos,y_right_pos +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 \ No newline at end of file diff --git a/tests/gaze/io/files/monocular_example.csv b/tests/gaze/io/files/monocular_example.csv new file mode 100644 index 000000000..87891121b --- /dev/null +++ b/tests/gaze/io/files/monocular_example.csv @@ -0,0 +1,11 @@ +time,x_left_pix,y_left_pix +0,0,0 +0,0,0 +0,0,0 +0,0,0 +0,0,0 +0,0,0 +0,0,0 +0,0,0 +0,0,0 +0,0,0 \ No newline at end of file From bf9f7d860ac88f3e99ae0dabcf2f3dc41dab1f46 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 07:26:55 +0000 Subject: [PATCH 02/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/gaze/io/csv_test.py | 14 +++++++++----- tests/gaze/io/files/binocular_example.csv | 2 +- tests/gaze/io/files/monocular_example.csv | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/gaze/io/csv_test.py b/tests/gaze/io/csv_test.py index c64264ebe..cd5e35abd 100644 --- a/tests/gaze/io/csv_test.py +++ b/tests/gaze/io/csv_test.py @@ -27,20 +27,24 @@ ('kwargs', 'shape'), [ pytest.param( - {'file': 'tests/gaze/io/files/monocular_example.csv', - 'time_column': 'time', 'pixel_columns': ['x_left_pix', 'y_left_pix']}, + { + 'file': 'tests/gaze/io/files/monocular_example.csv', + 'time_column': 'time', 'pixel_columns': ['x_left_pix', 'y_left_pix'], + }, (10, 2), id='csv_mono_shape', ), pytest.param( - {'file': 'tests/gaze/io/files/binocular_example.csv', + { + 'file': 'tests/gaze/io/files/binocular_example.csv', 'time_column': 'time', 'pixel_columns': ['x_left_pix', 'y_left_pix', 'x_right_pix', 'y_right_pix'], - 'position_columns': ['x_left_pos', 'y_left_pos', 'x_right_pos', 'y_right_pos']}, + 'position_columns': ['x_left_pos', 'y_left_pos', 'x_right_pos', 'y_right_pos'], + }, (10, 3), id='csv_bino_shape', ), - ] + ], ) def test_shapes(kwargs, shape): gaze_dataframe = pm.gaze.from_csv(**kwargs) diff --git a/tests/gaze/io/files/binocular_example.csv b/tests/gaze/io/files/binocular_example.csv index e6672e317..ddfd55ab4 100644 --- a/tests/gaze/io/files/binocular_example.csv +++ b/tests/gaze/io/files/binocular_example.csv @@ -8,4 +8,4 @@ time,x_left_pix,y_left_pix,x_right_pix,y_right_pix,x_left_pos,y_left_pos,x_right 0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 -0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 \ No newline at end of file +0,0,0,0,0,-23.104783, -13.489493,-23.104783, -13.489493 diff --git a/tests/gaze/io/files/monocular_example.csv b/tests/gaze/io/files/monocular_example.csv index 87891121b..5493f10e4 100644 --- a/tests/gaze/io/files/monocular_example.csv +++ b/tests/gaze/io/files/monocular_example.csv @@ -8,4 +8,4 @@ time,x_left_pix,y_left_pix 0,0,0 0,0,0 0,0,0 -0,0,0 \ No newline at end of file +0,0,0 From 4397cb9480ca05408172dcc25517acdec12786ec Mon Sep 17 00:00:00 2001 From: prassepaul Date: Fri, 15 Sep 2023 09:45:51 +0200 Subject: [PATCH 03/10] fix docstring error --- src/pymovements/gaze/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymovements/gaze/io.py b/src/pymovements/gaze/io.py index ad59dbd6b..dfbc41b58 100644 --- a/src/pymovements/gaze/io.py +++ b/src/pymovements/gaze/io.py @@ -119,8 +119,8 @@ def from_csv( and the names of the pixel position columns. >>> gaze = from_csv(file='sample.csv', - time_column = 'time', - pixel_columns = ['x_left_pix','y_left_pix'],) + ... time_column = 'time', + ... pixel_columns = ['x_left_pix','y_left_pix'],) >>> gaze.frame shape: (10, 2) ┌──────┬───────────┐ From aa994f008cd464e713d510981690b9bfdbfdbd28 Mon Sep 17 00:00:00 2001 From: prassepaul Date: Fri, 15 Sep 2023 10:04:29 +0200 Subject: [PATCH 04/10] docstring error --- src/pymovements/gaze/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymovements/gaze/io.py b/src/pymovements/gaze/io.py index dfbc41b58..5a06f9006 100644 --- a/src/pymovements/gaze/io.py +++ b/src/pymovements/gaze/io.py @@ -97,7 +97,7 @@ def from_csv( Examples -------- - First let's assume a CSV file stored `sample.csv` with the following content: + First let's assume a CSV file stored `tests/gaze/io/files/monocular_example.csv` with the following content: shape: (10, 3) ┌──────┬────────────┬────────────┐ │ time ┆ x_left_pix ┆ y_left_pix │ @@ -118,7 +118,7 @@ def from_csv( We can now load the data into a ``GazeDataFrame`` by specyfing the experimental setting and the names of the pixel position columns. - >>> gaze = from_csv(file='sample.csv', + >>> gaze = from_csv(file='tests/gaze/io/files/monocular_example.csv', ... time_column = 'time', ... pixel_columns = ['x_left_pix','y_left_pix'],) >>> gaze.frame From 16e3946f3d286c7bb148c4561e465111148bb21e Mon Sep 17 00:00:00 2001 From: prassepaul Date: Fri, 15 Sep 2023 10:12:19 +0200 Subject: [PATCH 05/10] flake8 issue --- src/pymovements/gaze/io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pymovements/gaze/io.py b/src/pymovements/gaze/io.py index 5a06f9006..50d0a6c69 100644 --- a/src/pymovements/gaze/io.py +++ b/src/pymovements/gaze/io.py @@ -97,7 +97,8 @@ def from_csv( Examples -------- - First let's assume a CSV file stored `tests/gaze/io/files/monocular_example.csv` with the following content: + First let's assume a CSV file stored `tests/gaze/io/files/monocular_example.csv` + with the following content: shape: (10, 3) ┌──────┬────────────┬────────────┐ │ time ┆ x_left_pix ┆ y_left_pix │ @@ -118,7 +119,8 @@ def from_csv( We can now load the data into a ``GazeDataFrame`` by specyfing the experimental setting and the names of the pixel position columns. - >>> gaze = from_csv(file='tests/gaze/io/files/monocular_example.csv', + >>> gaze = from_csv( + ... file='tests/gaze/io/files/monocular_example.csv', ... time_column = 'time', ... pixel_columns = ['x_left_pix','y_left_pix'],) >>> gaze.frame From 493efdb5deae0725f65586b3b9cfa8e82ff79d2f Mon Sep 17 00:00:00 2001 From: prassepaul Date: Fri, 15 Sep 2023 11:02:05 +0200 Subject: [PATCH 06/10] cyclic import --- src/pymovements/gaze/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymovements/gaze/io.py b/src/pymovements/gaze/io.py index 50d0a6c69..0edd6da04 100644 --- a/src/pymovements/gaze/io.py +++ b/src/pymovements/gaze/io.py @@ -25,8 +25,8 @@ import polars as pl -from pymovements.gaze import Experiment -from pymovements.gaze.gaze_dataframe import GazeDataFrame +from pymovements.gaze import Experiment # pylint: disable=cyclic-import +from pymovements.gaze.gaze_dataframe import GazeDataFrame # pylint: disable=cyclic-import def from_csv( From 926e27cc8a203b220094e7f0e50bbc41b55027c6 Mon Sep 17 00:00:00 2001 From: prassepaul Date: Tue, 19 Sep 2023 09:52:53 +0200 Subject: [PATCH 07/10] added import to docstring --- src/pymovements/gaze/io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pymovements/gaze/io.py b/src/pymovements/gaze/io.py index 0edd6da04..81bd391f7 100644 --- a/src/pymovements/gaze/io.py +++ b/src/pymovements/gaze/io.py @@ -119,6 +119,7 @@ def from_csv( We can now load the data into a ``GazeDataFrame`` by specyfing the experimental setting and the names of the pixel position columns. + >>> from pymovements.gaze.io import from_csv >>> gaze = from_csv( ... file='tests/gaze/io/files/monocular_example.csv', ... time_column = 'time', From 9b074ae75612e1fb1e6412fdfe1c3643cf7c31a4 Mon Sep 17 00:00:00 2001 From: prassepaul Date: Thu, 21 Sep 2023 14:35:29 +0200 Subject: [PATCH 08/10] added dataset gaze_on_faces --- docs/source/bibliography.bib | 11 ++ src/pymovements/datasets/__init__.py | 3 + src/pymovements/datasets/gaze_on_faces.py | 148 ++++++++++++++++++++++ tests/datasets/gaze_on_faces_test.py | 79 ++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 src/pymovements/datasets/gaze_on_faces.py create mode 100644 tests/datasets/gaze_on_faces_test.py diff --git a/docs/source/bibliography.bib b/docs/source/bibliography.bib index 0992286e2..c6fec225c 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} +} \ No newline at end of file diff --git a/src/pymovements/datasets/__init__.py b/src/pymovements/datasets/__init__.py index b1eb00ee2..b0f18e6ed 100644 --- a/src/pymovements/datasets/__init__.py +++ b/src/pymovements/datasets/__init__.py @@ -28,6 +28,7 @@ pymovements.datasets.GazeBase pymovements.datasets.GazeBaseVR pymovements.datasets.JuDo1000 + pymovements.datasets.GazeOnFaces .. rubric:: Example Datasets @@ -42,6 +43,7 @@ from pymovements.datasets.gazebase import GazeBase from pymovements.datasets.gazebasevr import GazeBaseVR from pymovements.datasets.judo1000 import JuDo1000 +from pymovements.datasets.gaze_on_faces import GazeOnFaces from pymovements.datasets.toy_dataset import ToyDataset from pymovements.datasets.toy_dataset_eyelink import ToyDatasetEyeLink @@ -50,6 +52,7 @@ 'GazeBase', 'GazeBaseVR', 'JuDo1000', + 'GazeOnFaces', '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..69e6a70e4 --- /dev/null +++ b/src/pymovements/datasets/gaze_on_faces.py @@ -0,0 +1,148 @@ +# 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/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'] From 348b113a1925b1eaf28dd617f694dea9b214bd17 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 12:40:00 +0000 Subject: [PATCH 09/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/bibliography.bib | 2 +- src/pymovements/datasets/__init__.py | 2 +- src/pymovements/datasets/gaze_on_faces.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/bibliography.bib b/docs/source/bibliography.bib index c6fec225c..3a7ab8bbd 100644 --- a/docs/source/bibliography.bib +++ b/docs/source/bibliography.bib @@ -75,4 +75,4 @@ @article{GazeOnFaces pages={16--16}, year={2016}, publisher={The Association for Research in Vision and Ophthalmology} -} \ No newline at end of file +} diff --git a/src/pymovements/datasets/__init__.py b/src/pymovements/datasets/__init__.py index b0f18e6ed..c87a85221 100644 --- a/src/pymovements/datasets/__init__.py +++ b/src/pymovements/datasets/__init__.py @@ -40,10 +40,10 @@ 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 -from pymovements.datasets.gaze_on_faces import GazeOnFaces from pymovements.datasets.toy_dataset import ToyDataset from pymovements.datasets.toy_dataset_eyelink import ToyDatasetEyeLink diff --git a/src/pymovements/datasets/gaze_on_faces.py b/src/pymovements/datasets/gaze_on_faces.py index 69e6a70e4..0a5795dfe 100644 --- a/src/pymovements/datasets/gaze_on_faces.py +++ b/src/pymovements/datasets/gaze_on_faces.py @@ -23,6 +23,7 @@ from dataclasses import dataclass from dataclasses import field from typing import Any + import polars as pl from pymovements.dataset.dataset_definition import DatasetDefinition From fef59ecd8fbbf8b19f6e6260218251d755f52baf Mon Sep 17 00:00:00 2001 From: prassepaul Date: Fri, 22 Sep 2023 09:30:15 +0200 Subject: [PATCH 10/10] requested changes --- src/pymovements/datasets/__init__.py | 4 ++-- tests/datasets/datasets_test.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pymovements/datasets/__init__.py b/src/pymovements/datasets/__init__.py index c87a85221..75597101c 100644 --- a/src/pymovements/datasets/__init__.py +++ b/src/pymovements/datasets/__init__.py @@ -27,8 +27,8 @@ pymovements.datasets.GazeBase pymovements.datasets.GazeBaseVR - pymovements.datasets.JuDo1000 pymovements.datasets.GazeOnFaces + pymovements.datasets.JuDo1000 .. rubric:: Example Datasets @@ -51,8 +51,8 @@ __all__ = [ 'GazeBase', 'GazeBaseVR', - 'JuDo1000', 'GazeOnFaces', + 'JuDo1000', 'ToyDataset', 'ToyDatasetEyeLink', ] 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'), ], )