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..81bd391f7 --- /dev/null +++ b/src/pymovements/gaze/io.py @@ -0,0 +1,160 @@ +# 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 # pylint: disable=cyclic-import +from pymovements.gaze.gaze_dataframe import GazeDataFrame # pylint: disable=cyclic-import + + +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 `tests/gaze/io/files/monocular_example.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. + + >>> from pymovements.gaze.io import from_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 + 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..cd5e35abd --- /dev/null +++ b/tests/gaze/io/csv_test.py @@ -0,0 +1,52 @@ +# 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..ddfd55ab4 --- /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 diff --git a/tests/gaze/io/files/monocular_example.csv b/tests/gaze/io/files/monocular_example.csv new file mode 100644 index 000000000..5493f10e4 --- /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