gazeMapper 1.4.2__tar.gz → 1.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {gazemapper-1.4.2/src/gazeMapper.egg-info → gazemapper-1.4.4}/PKG-INFO +67 -15
  2. {gazemapper-1.4.2 → gazemapper-1.4.4}/README.md +63 -11
  3. {gazemapper-1.4.2 → gazemapper-1.4.4}/requirements.txt +2 -3
  4. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/_impl/callbacks.py +27 -12
  5. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/_impl/gui.py +23 -22
  6. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/_impl/session_lister.py +3 -32
  7. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/_impl/settings_editor.py +6 -7
  8. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/config.py +24 -22
  9. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/episode.py +3 -3
  10. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/marker.py +5 -2
  11. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/__init__.py +14 -25
  12. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/auto_code_sync_points.py +2 -2
  13. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/auto_code_trials.py +2 -2
  14. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/code_episodes.py +3 -2
  15. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/detect_markers.py +2 -3
  16. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/export_trials.py +12 -4
  17. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/gaze_to_plane.py +2 -3
  18. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/make_gaze_overlay_video.py +3 -4
  19. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/make_mapped_gaze_video.py +8 -8
  20. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/run_validation.py +2 -3
  21. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/sync_et_to_cam.py +5 -7
  22. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/sync_to_ref.py +2 -2
  23. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/session.py +15 -15
  24. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/synchronization.py +3 -3
  25. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/version.py +1 -1
  26. {gazemapper-1.4.2 → gazemapper-1.4.4/src/gazeMapper.egg-info}/PKG-INFO +67 -15
  27. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper.egg-info/SOURCES.txt +0 -2
  28. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper.egg-info/requires.txt +1 -2
  29. gazemapper-1.4.2/src/gazeMapper/GUI/_impl/colors.py +0 -11
  30. gazemapper-1.4.2/src/gazeMapper/GUI/_impl/process_pool.py +0 -281
  31. {gazemapper-1.4.2 → gazemapper-1.4.4}/LICENSE +0 -0
  32. {gazemapper-1.4.2 → gazemapper-1.4.4}/MANIFEST.in +0 -0
  33. {gazemapper-1.4.2 → gazemapper-1.4.4}/pyproject.toml +0 -0
  34. {gazemapper-1.4.2 → gazemapper-1.4.4}/setup.cfg +0 -0
  35. {gazemapper-1.4.2 → gazemapper-1.4.4}/setup.py +0 -0
  36. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/__init__.py +0 -0
  37. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/_impl/__init__.py +0 -0
  38. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/_impl/image_helper.py +0 -0
  39. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/GUI/_impl/utils.py +0 -0
  40. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/__init__.py +0 -0
  41. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/naming.py +0 -0
  42. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/plane.py +0 -0
  43. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/process/_utils.py +0 -0
  44. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/project_watcher.py +0 -0
  45. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/type_utils.py +0 -0
  46. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/typed_dict_defaults.py +0 -0
  47. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/utils/__init__.py +0 -0
  48. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper/utils/color_blob_localizer.py +0 -0
  49. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper.egg-info/dependency_links.txt +0 -0
  50. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper.egg-info/entry_points.txt +0 -0
  51. {gazemapper-1.4.2 → gazemapper-1.4.4}/src/gazeMapper.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: gazeMapper
3
- Version: 1.4.2
3
+ Version: 1.4.4
4
4
  Summary: Automated world-based analysis of wearable eye tracker data.
5
5
  Home-page: https://github.com/dcnieho/gazeMapper
6
6
  Author: Diederick Niehorster
@@ -34,11 +34,10 @@ Classifier: Operating System :: OS Independent
34
34
  Requires-Python: >=3.11
35
35
  Description-Content-Type: text/markdown
36
36
  License-File: LICENSE
37
- Requires-Dist: glassesTools[GUI]>=1.16.0
37
+ Requires-Dist: glassesTools[GUI]>=1.17.1
38
38
  Requires-Dist: typeguard
39
39
  Requires-Dist: uvloop; sys_platform != "win32"
40
40
  Requires-Dist: watchfiles
41
- Requires-Dist: pebble
42
41
  Dynamic: author
43
42
  Dynamic: author-email
44
43
  Dynamic: classifier
@@ -46,6 +45,7 @@ Dynamic: description
46
45
  Dynamic: description-content-type
47
46
  Dynamic: home-page
48
47
  Dynamic: license
48
+ Dynamic: license-file
49
49
  Dynamic: project-url
50
50
  Dynamic: requires-dist
51
51
  Dynamic: requires-python
@@ -55,7 +55,7 @@ Dynamic: summary
55
55
  [![PyPI Latest Release](https://img.shields.io/pypi/v/gazeMapper.svg)](https://pypi.org/project/gazeMapper/)
56
56
  [![image](https://img.shields.io/pypi/pyversions/gazeMapper.svg)](https://pypi.org/project/gazeMapper/)
57
57
 
58
- # gazeMapper v1.4.2
58
+ # gazeMapper v1.4.4
59
59
  Tool for automated world-based analysis of wearable eye tracker data.
60
60
  gazeMapper is an open-source tool for automated mapping and processing of eye-tracking data to enable automated world-based analysis.
61
61
  gazeMapper can:
@@ -64,15 +64,67 @@ gazeMapper can:
64
64
  3) Determine data quality measures, e.g., accuracy and precision using [glassesValidator](https://github.com/dcnieho/glassesValidator) ([Niehorster et al., 2023](https://doi.org/10.3758/s13428-023-02105-5)).
65
65
 
66
66
  If you use this tool or any of the code in this repository, please cite:<br>
67
- Niehorster, D.C., Hessels, R.S., Nyström, M., Benjamins, J.S. and Hooge, I.T.C. (submitted). gazeMapper: A tool for automated world-based analysis of gaze data from one or multiple wearable eye trackers. Manuscript submitted for publication, 2024 ([BibTeX](#bibtex))<br>
67
+ Niehorster, D.C., Hessels, R.S., Nyström, M., Benjamins, J.S. and Hooge, I.T.C. (submitted). gazeMapper: A tool for automated world-based analysis of gaze data from one or multiple wearable eye trackers. Manuscript submitted for publication, 2025 ([BibTeX](#bibtex))<br>
68
68
  If you use the functionality for automatic determining the data quality (accuracy and precision) of wearable eye tracker recordings, please additionally cite:<br>
69
69
  [Niehorster, D.C., Hessels, R.S., Benjamins, J.S., Nyström, M. and Hooge, I.T.C. (2023). GlassesValidator:
70
70
  A data quality tool for eye tracking glasses. Behavior Research Methods. doi: 10.3758/s13428-023-02105-5](https://doi.org/10.3758/s13428-023-02105-5) ([BibTeX](#bibtex))
71
71
 
72
72
  ## Example
73
73
  ![application example](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/world_data.png?raw=true)
74
- Example where gazeMapper has been used to map head-centered gaze from two head-worn eye tracker recordings to synchronized world-centered
75
- gaze data of the pair, drawn on an overview video recording with an additional external camera. From Hessels, R.S., Iwabuchi, T., Niehorster, D.C., Funawatari, R., Benjamins, J.S., Kawakami, S.; Nyström, M., Suda, M., Hooge, I.T.C., Sumiya, M., Heijnen, J., Teunisse, M. & Senju, A. (submitted). Gaze behavior in face-to-face interaction: A cross-cultural investigation between Japan and the Netherlands. Manuscript submitted for publication, 2024
74
+ Example where gazeMapper has been used to map head-centered eye tracking data from two head-worn eye tracker recordings (top panels, camera positions are indicated using stars, and gaze positions using colored circles. Blue indicates eye tracker 1 [ET1] and orange eye tracker 2) to synchronized world-centered gaze data of the pair, drawn on an overview video frame (bottom panel) recorded with an additional external camera. The shown frames are produced by gazeMapper's advanced gaze video functionality (though larger stars and circles to indicate the camera and gaze points were manually overlaid for clarity). From [Hessels, R.S.,Teunisse, M., Niehorster, D.C., Nyström, M., Benjamins, J.S., Senju, A. & Hooge, I.T.S. (2023). Task related gaze behaviour in face to face dyadic collaboration: Toward an interactive theory. _Visual Cognition 31_(4), pp. 291-313. doi: 10.1080/13506285.2023.2250507](https://doi.org/10.1080/13506285.2023.2250507).
75
+
76
+ # Table of contents
77
+ - [How to acquire](#how-to-acquire)
78
+ - [Complete instructions for MacOS](#complete-instructions-for-macos)
79
+ - [Complete instructions for Linux](#complete-instructions-for-linux)
80
+ - [Usage](#usage)
81
+ - [Workflow and example data](#workflow-and-example-data)
82
+ - [Example 1: Single participant and single plane](#example-1-single-participant-and-single-plane)
83
+ - [Example 2: Two participants and two planes](#example-2-two-participants-and-two-planes)
84
+ - [Example 3: One participant, multiple planes and an overview camera](#example-3-one-participant-multiple-planes-and-an-overview-camera)
85
+ - [gazeMapper projects](#gazemapper-projects)
86
+ - [Output](#output)
87
+ - [Coordinate system of data](#coordinate-system-of-data)
88
+ - [Eye trackers](#eye-trackers)
89
+ - [Required preprocessing outside gazeMapper](#required-preprocessing-outside-gazemapper)
90
+ - [gazeMapper sessions](#gazemapper-sessions)
91
+ - [gazeMapper planes](#gazemapper-planes)
92
+ - [Validation (glassesValidator planes)](#validation-glassesvalidator-planes)
93
+ - [Individual Markers](#individual-markers)
94
+ - [Actions](#actions)
95
+ - [Coding analysis, synchronization and validation episodes](#coding-analysis-synchronization-and-validation-episodes)
96
+ - [Automatic coding of analysis and synchronization episodes](#automatic-coding-of-analysis-and-synchronization-episodes)
97
+ - [Automatic coding of synchronization timepoints](#automatic-coding-of-synchronization-timepoints)
98
+ - [Automatic coding of analysis episodes](#automatic-coding-of-analysis-episodes)
99
+ - [Synchronization](#synchronization)
100
+ - [Synchronizing eye tracker data and scene camera](#synchronizing-eye-tracker-data-and-scene-camera)
101
+ - [Synchronizing multiple eye tracker or external camera recordings](#synchronizing-multiple-eye-tracker-or-external-camera-recordings)
102
+ - [Configuration](#configuration)
103
+ - [`gazeMapper.config.AutoCodeSyncPoints`](#gazemapperconfigautocodesyncpoints)
104
+ - [`gazeMapper.config.AutoCodeTrialEpisodes`](#gazemapperconfigautocodetrialepisodes)
105
+ - [`gazeMapper.config.CamMovementForEtSyncFunction`](#gazemapperconfigcammovementforetsyncfunction)
106
+ - [`gazeMapper.config.I2MCSettings`](#gazemapperconfigi2mcsettings)
107
+ - [`gazeMapper.config.RgbColor`](#gazemapperconfigrgbcolor)
108
+ - [Overriding a project's settings for a specific session or recording](#overriding-a-projects-settings-for-a-specific-session-or-recording)
109
+ - [API](#api)
110
+ - [`gazeMapper.config`](#gazemapperconfig)
111
+ - [`gazeMapper.config.Study`](#gazemapperconfigstudy)
112
+ - [`gazeMapper.config.OverrideLevel`](#gazemapperconfigoverridelevel)
113
+ - [`gazeMapper.config.StudyOverride`](#gazemapperconfigstudyoverride)
114
+ - [`gazeMapper.episode`](#gazemapperepisode)
115
+ - [`gazeMapper.marker`](#gazemappermarker)
116
+ - [`gazeMapper.plane`](#gazemapperplane)
117
+ - [`gazeMapper.plane.Type`](#gazemapperplanetype)
118
+ - [`gazeMapper.plane.Definition` and subclasses](#gazemapperplanedefinition-and-subclasses)
119
+ - [`gazeMapper.process`](#gazemapperprocess)
120
+ - [`gazeMapper.session`](#gazemappersession)
121
+ - [`gazeMapper.session.RecordingType`](#gazemappersessionrecordingtype)
122
+ - [`gazeMapper.session.RecordingDefinition`](#gazemappersessionrecordingdefinition)
123
+ - [`gazeMapper.session.Recording`](#gazemappersessionrecording)
124
+ - [`gazeMapper.session.SessionDefinition`](#gazemappersessionsessiondefinition)
125
+ - [`gazeMapper.session.Session`](#gazemappersessionsession)
126
+ - [Free functions](#free-functions)
127
+ - [Citation](#citation)
76
128
 
77
129
  # How to acquire
78
130
  GazeMapper is available from `https://github.com/dcnieho/gazeMapper`, and supports Python 3.11 on Windows, MacOS and Linux (newer versions of Python should work fine but are not tested).
@@ -222,7 +274,7 @@ Example 1 is a minimum example, showing a short recording where a participant lo
222
274
 
223
275
  ![Project config](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/project_config_1.png?raw=true)
224
276
  4. Now that the project is set up, we are ready to import and process recordings.
225
- 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/1_simple/data) and indicate its an `SMI ETG` recording. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper. Note also that an SMI ETG recording has to be [prepared](#required-preprocessing-outside-gazemapper) before it can be imported by gazeMapper. This preparation has already been performed for the included example data.
277
+ 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/1_simple/data) (`1_simple/data`) and indicate its an `SMI ETG` recording. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper. Note also that an SMI ETG recording has to be [prepared](#required-preprocessing-outside-gazemapper) before it can be imported by gazeMapper. This preparation has already been performed for the included example data.
226
278
  1. On the window that pops up, click `+ new session`, and name the session `1` (or any name you like). Expand session `1`. You should now see the following:
227
279
 
228
280
  ![Add recording 1](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_1_1.png?raw=true)
@@ -317,12 +369,12 @@ Nonetheless, below we provide a full description of how recording preparation, d
317
369
 
318
370
  ![Project config](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/project_config_2.png?raw=true)
319
371
  4. Now that the project is set up, we are ready to import and process recordings.
320
- 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) and indicate you're looking for `Tobii Glasses 2` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
372
+ 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) (`2_two_participants_two_planes/data`) and indicate you're looking for `Tobii Glasses 2` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
321
373
  1. On the window that pops up, click `+ new session`, and name the session `1` (or any name you like). Expand session `1`. You should now see the following:
322
374
 
323
375
  ![Add recording 1](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_2_1.png?raw=true)
324
376
  1. Assign the recording to the session by dragging it from the right, and dropping it on the left for the `lead` recording where it says `drop recording here`. Click `Continue`. The recording will now be imported.
325
- 1. Again click `import eye tracker recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) and this time indicate you're looking for `Pupil Invisible` recordings. Assign the found Pupil Invisible recording to the `follow` recording.
377
+ 1. Again click `import eye tracker recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) (`2_two_participants_two_planes/data`) and this time indicate you're looking for `Pupil Invisible` recordings. Assign the found Pupil Invisible recording to the `follow` recording.
326
378
 
327
379
  ![Add recording 2](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_2_2.png?raw=true)
328
380
 
@@ -416,12 +468,12 @@ Example 3 is a more advanced example, showing a recording of a participant looki
416
468
 
417
469
  ![Project config](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/project_config_3.png?raw=true)
418
470
  4. Now that the project is set up, we are ready to import and process recordings.
419
- 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) and indicate you're looking for `Pupil Invisible` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
471
+ 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) (`3_external_camera_moving_plane/data`) and indicate you're looking for `Pupil Invisible` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
420
472
  1. On the window that pops up, click `+ new session`, and name the session `1` (or any name you like). Expand session `1`. You should now see the following:
421
473
 
422
474
  ![Add recording 1](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_3_1.png?raw=true)
423
475
  1. Assign the recording to the session by dragging it from the right, and dropping it on the left for the `eye tracker` recording where it says `drop recording here`. Click `Continue`. The recording will now be imported.
424
- 1. Click `import camera recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) and click continue. Assign the found camera recording (`cam1_R003_reencode.mp4`) to the `overview` recording.
476
+ 1. Click `import camera recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) (`3_external_camera_moving_plane/data`) and click continue. Assign the found camera recording (`cam1_R003_reencode.mp4`) to the `overview` recording.
425
477
 
426
478
  ![Add recording 2](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_3_2.png?raw=true)
427
479
 
@@ -603,7 +655,7 @@ A generic 2D fiducial marker plane is defined by a file with four columns that d
603
655
  | `y` |The vertical location of the marker's center on the plane (mm).|
604
656
  | `rotation_angle` |The rotation of the marker, if any (degree).|
605
657
 
606
- A file with this information should be stored under any name (e.g., `markerPositions.csv`) in the plane's configuration folder inside the project's configuration folder. See the example data for an example of such a file (TODO).
658
+ A file with this information should be stored under any name (e.g., `markerPositions.csv`) in the plane's configuration folder inside the project's configuration folder. See the [example data](https://github.com/dcnieho/gazeMapper/blob/master/example_data/1_simple/configuration/screen/markerPositions_screen.csv) for an example of such a file.
607
659
 
608
660
  To be able turn the information of the above file into a plane, further settings are needed:
609
661
  | Setting | Description |
@@ -622,7 +674,7 @@ These settings are typically stored in a file `plane_def.json` in the plane's co
622
674
  ### Validation (glassesValidator planes)
623
675
  gazeMapper has built-in support for computing data quality from the gaze data of a participant looking at a validation poster using glassesValidator. To use this functionality, a plane of type GlassesValidator (`gazeMapper.plane.Type.GlassesValidator`) needs to be defined in the project's setup. By default, the default glassesValidator plane is used for a GlassesValidator plane. When the default checkbox is unchecked in the GUI (the `is_default` setting in `plane_def.json` is False), a custom configuration can be used. When unchecking this checkbox in the GUI, files containing the plane setup are deployed to the plane configuration folder, so that the user can edit or replace them. API users are requested to call `glassesValidator.config.deploy_validation_config()` to deploy the glassesValidator configuration files to the plane's configuration folder. The customization options for a glassesValidator plane are [documented here](https://github.com/dcnieho/glassesValidator/blob/master/README.md#customizing-the-poster).
624
676
 
625
- ### Individual Markers
677
+ ## Individual Markers
626
678
  Besides planes, gazeMapper can also be configured to detect and report on the appearance of individual markers. This is configured in the `Individual markers editor` pane in the GUI or by means of `gazeMapper.marker.Marker` objects.
627
679
 
628
680
  ## Actions
@@ -2,7 +2,7 @@
2
2
  [![PyPI Latest Release](https://img.shields.io/pypi/v/gazeMapper.svg)](https://pypi.org/project/gazeMapper/)
3
3
  [![image](https://img.shields.io/pypi/pyversions/gazeMapper.svg)](https://pypi.org/project/gazeMapper/)
4
4
 
5
- # gazeMapper v1.4.2
5
+ # gazeMapper v1.4.4
6
6
  Tool for automated world-based analysis of wearable eye tracker data.
7
7
  gazeMapper is an open-source tool for automated mapping and processing of eye-tracking data to enable automated world-based analysis.
8
8
  gazeMapper can:
@@ -11,15 +11,67 @@ gazeMapper can:
11
11
  3) Determine data quality measures, e.g., accuracy and precision using [glassesValidator](https://github.com/dcnieho/glassesValidator) ([Niehorster et al., 2023](https://doi.org/10.3758/s13428-023-02105-5)).
12
12
 
13
13
  If you use this tool or any of the code in this repository, please cite:<br>
14
- Niehorster, D.C., Hessels, R.S., Nyström, M., Benjamins, J.S. and Hooge, I.T.C. (submitted). gazeMapper: A tool for automated world-based analysis of gaze data from one or multiple wearable eye trackers. Manuscript submitted for publication, 2024 ([BibTeX](#bibtex))<br>
14
+ Niehorster, D.C., Hessels, R.S., Nyström, M., Benjamins, J.S. and Hooge, I.T.C. (submitted). gazeMapper: A tool for automated world-based analysis of gaze data from one or multiple wearable eye trackers. Manuscript submitted for publication, 2025 ([BibTeX](#bibtex))<br>
15
15
  If you use the functionality for automatic determining the data quality (accuracy and precision) of wearable eye tracker recordings, please additionally cite:<br>
16
16
  [Niehorster, D.C., Hessels, R.S., Benjamins, J.S., Nyström, M. and Hooge, I.T.C. (2023). GlassesValidator:
17
17
  A data quality tool for eye tracking glasses. Behavior Research Methods. doi: 10.3758/s13428-023-02105-5](https://doi.org/10.3758/s13428-023-02105-5) ([BibTeX](#bibtex))
18
18
 
19
19
  ## Example
20
20
  ![application example](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/world_data.png?raw=true)
21
- Example where gazeMapper has been used to map head-centered gaze from two head-worn eye tracker recordings to synchronized world-centered
22
- gaze data of the pair, drawn on an overview video recording with an additional external camera. From Hessels, R.S., Iwabuchi, T., Niehorster, D.C., Funawatari, R., Benjamins, J.S., Kawakami, S.; Nyström, M., Suda, M., Hooge, I.T.C., Sumiya, M., Heijnen, J., Teunisse, M. & Senju, A. (submitted). Gaze behavior in face-to-face interaction: A cross-cultural investigation between Japan and the Netherlands. Manuscript submitted for publication, 2024
21
+ Example where gazeMapper has been used to map head-centered eye tracking data from two head-worn eye tracker recordings (top panels, camera positions are indicated using stars, and gaze positions using colored circles. Blue indicates eye tracker 1 [ET1] and orange eye tracker 2) to synchronized world-centered gaze data of the pair, drawn on an overview video frame (bottom panel) recorded with an additional external camera. The shown frames are produced by gazeMapper's advanced gaze video functionality (though larger stars and circles to indicate the camera and gaze points were manually overlaid for clarity). From [Hessels, R.S.,Teunisse, M., Niehorster, D.C., Nyström, M., Benjamins, J.S., Senju, A. & Hooge, I.T.S. (2023). Task related gaze behaviour in face to face dyadic collaboration: Toward an interactive theory. _Visual Cognition 31_(4), pp. 291-313. doi: 10.1080/13506285.2023.2250507](https://doi.org/10.1080/13506285.2023.2250507).
22
+
23
+ # Table of contents
24
+ - [How to acquire](#how-to-acquire)
25
+ - [Complete instructions for MacOS](#complete-instructions-for-macos)
26
+ - [Complete instructions for Linux](#complete-instructions-for-linux)
27
+ - [Usage](#usage)
28
+ - [Workflow and example data](#workflow-and-example-data)
29
+ - [Example 1: Single participant and single plane](#example-1-single-participant-and-single-plane)
30
+ - [Example 2: Two participants and two planes](#example-2-two-participants-and-two-planes)
31
+ - [Example 3: One participant, multiple planes and an overview camera](#example-3-one-participant-multiple-planes-and-an-overview-camera)
32
+ - [gazeMapper projects](#gazemapper-projects)
33
+ - [Output](#output)
34
+ - [Coordinate system of data](#coordinate-system-of-data)
35
+ - [Eye trackers](#eye-trackers)
36
+ - [Required preprocessing outside gazeMapper](#required-preprocessing-outside-gazemapper)
37
+ - [gazeMapper sessions](#gazemapper-sessions)
38
+ - [gazeMapper planes](#gazemapper-planes)
39
+ - [Validation (glassesValidator planes)](#validation-glassesvalidator-planes)
40
+ - [Individual Markers](#individual-markers)
41
+ - [Actions](#actions)
42
+ - [Coding analysis, synchronization and validation episodes](#coding-analysis-synchronization-and-validation-episodes)
43
+ - [Automatic coding of analysis and synchronization episodes](#automatic-coding-of-analysis-and-synchronization-episodes)
44
+ - [Automatic coding of synchronization timepoints](#automatic-coding-of-synchronization-timepoints)
45
+ - [Automatic coding of analysis episodes](#automatic-coding-of-analysis-episodes)
46
+ - [Synchronization](#synchronization)
47
+ - [Synchronizing eye tracker data and scene camera](#synchronizing-eye-tracker-data-and-scene-camera)
48
+ - [Synchronizing multiple eye tracker or external camera recordings](#synchronizing-multiple-eye-tracker-or-external-camera-recordings)
49
+ - [Configuration](#configuration)
50
+ - [`gazeMapper.config.AutoCodeSyncPoints`](#gazemapperconfigautocodesyncpoints)
51
+ - [`gazeMapper.config.AutoCodeTrialEpisodes`](#gazemapperconfigautocodetrialepisodes)
52
+ - [`gazeMapper.config.CamMovementForEtSyncFunction`](#gazemapperconfigcammovementforetsyncfunction)
53
+ - [`gazeMapper.config.I2MCSettings`](#gazemapperconfigi2mcsettings)
54
+ - [`gazeMapper.config.RgbColor`](#gazemapperconfigrgbcolor)
55
+ - [Overriding a project's settings for a specific session or recording](#overriding-a-projects-settings-for-a-specific-session-or-recording)
56
+ - [API](#api)
57
+ - [`gazeMapper.config`](#gazemapperconfig)
58
+ - [`gazeMapper.config.Study`](#gazemapperconfigstudy)
59
+ - [`gazeMapper.config.OverrideLevel`](#gazemapperconfigoverridelevel)
60
+ - [`gazeMapper.config.StudyOverride`](#gazemapperconfigstudyoverride)
61
+ - [`gazeMapper.episode`](#gazemapperepisode)
62
+ - [`gazeMapper.marker`](#gazemappermarker)
63
+ - [`gazeMapper.plane`](#gazemapperplane)
64
+ - [`gazeMapper.plane.Type`](#gazemapperplanetype)
65
+ - [`gazeMapper.plane.Definition` and subclasses](#gazemapperplanedefinition-and-subclasses)
66
+ - [`gazeMapper.process`](#gazemapperprocess)
67
+ - [`gazeMapper.session`](#gazemappersession)
68
+ - [`gazeMapper.session.RecordingType`](#gazemappersessionrecordingtype)
69
+ - [`gazeMapper.session.RecordingDefinition`](#gazemappersessionrecordingdefinition)
70
+ - [`gazeMapper.session.Recording`](#gazemappersessionrecording)
71
+ - [`gazeMapper.session.SessionDefinition`](#gazemappersessionsessiondefinition)
72
+ - [`gazeMapper.session.Session`](#gazemappersessionsession)
73
+ - [Free functions](#free-functions)
74
+ - [Citation](#citation)
23
75
 
24
76
  # How to acquire
25
77
  GazeMapper is available from `https://github.com/dcnieho/gazeMapper`, and supports Python 3.11 on Windows, MacOS and Linux (newer versions of Python should work fine but are not tested).
@@ -169,7 +221,7 @@ Example 1 is a minimum example, showing a short recording where a participant lo
169
221
 
170
222
  ![Project config](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/project_config_1.png?raw=true)
171
223
  4. Now that the project is set up, we are ready to import and process recordings.
172
- 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/1_simple/data) and indicate its an `SMI ETG` recording. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper. Note also that an SMI ETG recording has to be [prepared](#required-preprocessing-outside-gazemapper) before it can be imported by gazeMapper. This preparation has already been performed for the included example data.
224
+ 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/1_simple/data) (`1_simple/data`) and indicate its an `SMI ETG` recording. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper. Note also that an SMI ETG recording has to be [prepared](#required-preprocessing-outside-gazemapper) before it can be imported by gazeMapper. This preparation has already been performed for the included example data.
173
225
  1. On the window that pops up, click `+ new session`, and name the session `1` (or any name you like). Expand session `1`. You should now see the following:
174
226
 
175
227
  ![Add recording 1](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_1_1.png?raw=true)
@@ -264,12 +316,12 @@ Nonetheless, below we provide a full description of how recording preparation, d
264
316
 
265
317
  ![Project config](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/project_config_2.png?raw=true)
266
318
  4. Now that the project is set up, we are ready to import and process recordings.
267
- 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) and indicate you're looking for `Tobii Glasses 2` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
319
+ 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) (`2_two_participants_two_planes/data`) and indicate you're looking for `Tobii Glasses 2` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
268
320
  1. On the window that pops up, click `+ new session`, and name the session `1` (or any name you like). Expand session `1`. You should now see the following:
269
321
 
270
322
  ![Add recording 1](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_2_1.png?raw=true)
271
323
  1. Assign the recording to the session by dragging it from the right, and dropping it on the left for the `lead` recording where it says `drop recording here`. Click `Continue`. The recording will now be imported.
272
- 1. Again click `import eye tracker recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) and this time indicate you're looking for `Pupil Invisible` recordings. Assign the found Pupil Invisible recording to the `follow` recording.
324
+ 1. Again click `import eye tracker recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/2_two_participants_two_planes/data) (`2_two_participants_two_planes/data`) and this time indicate you're looking for `Pupil Invisible` recordings. Assign the found Pupil Invisible recording to the `follow` recording.
273
325
 
274
326
  ![Add recording 2](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_2_2.png?raw=true)
275
327
 
@@ -363,12 +415,12 @@ Example 3 is a more advanced example, showing a recording of a participant looki
363
415
 
364
416
  ![Project config](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/project_config_3.png?raw=true)
365
417
  4. Now that the project is set up, we are ready to import and process recordings.
366
- 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) and indicate you're looking for `Pupil Invisible` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
418
+ 1. On the `Session` pane, click `import eye tracker recordings`. There, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) (`3_external_camera_moving_plane/data`) and indicate you're looking for `Pupil Invisible` recordings. Note that you could also trigger import by drag-dropping a data folder onto gazeMapper.
367
419
  1. On the window that pops up, click `+ new session`, and name the session `1` (or any name you like). Expand session `1`. You should now see the following:
368
420
 
369
421
  ![Add recording 1](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_3_1.png?raw=true)
370
422
  1. Assign the recording to the session by dragging it from the right, and dropping it on the left for the `eye tracker` recording where it says `drop recording here`. Click `Continue`. The recording will now be imported.
371
- 1. Click `import camera recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) and click continue. Assign the found camera recording (`cam1_R003_reencode.mp4`) to the `overview` recording.
423
+ 1. Click `import camera recordings`, select the [folder containing the example data](https://github.com/dcnieho/gazeMapper/tree/master/example_data/3_external_camera_moving_plane/data) (`3_external_camera_moving_plane/data`) and click continue. Assign the found camera recording (`cam1_R003_reencode.mp4`) to the `overview` recording.
372
424
 
373
425
  ![Add recording 2](https://raw.githubusercontent.com/dcnieho/gazeMapper/master/.github/images/add_recording_3_2.png?raw=true)
374
426
 
@@ -550,7 +602,7 @@ A generic 2D fiducial marker plane is defined by a file with four columns that d
550
602
  | `y` |The vertical location of the marker's center on the plane (mm).|
551
603
  | `rotation_angle` |The rotation of the marker, if any (degree).|
552
604
 
553
- A file with this information should be stored under any name (e.g., `markerPositions.csv`) in the plane's configuration folder inside the project's configuration folder. See the example data for an example of such a file (TODO).
605
+ A file with this information should be stored under any name (e.g., `markerPositions.csv`) in the plane's configuration folder inside the project's configuration folder. See the [example data](https://github.com/dcnieho/gazeMapper/blob/master/example_data/1_simple/configuration/screen/markerPositions_screen.csv) for an example of such a file.
554
606
 
555
607
  To be able turn the information of the above file into a plane, further settings are needed:
556
608
  | Setting | Description |
@@ -569,7 +621,7 @@ These settings are typically stored in a file `plane_def.json` in the plane's co
569
621
  ### Validation (glassesValidator planes)
570
622
  gazeMapper has built-in support for computing data quality from the gaze data of a participant looking at a validation poster using glassesValidator. To use this functionality, a plane of type GlassesValidator (`gazeMapper.plane.Type.GlassesValidator`) needs to be defined in the project's setup. By default, the default glassesValidator plane is used for a GlassesValidator plane. When the default checkbox is unchecked in the GUI (the `is_default` setting in `plane_def.json` is False), a custom configuration can be used. When unchecking this checkbox in the GUI, files containing the plane setup are deployed to the plane configuration folder, so that the user can edit or replace them. API users are requested to call `glassesValidator.config.deploy_validation_config()` to deploy the glassesValidator configuration files to the plane's configuration folder. The customization options for a glassesValidator plane are [documented here](https://github.com/dcnieho/glassesValidator/blob/master/README.md#customizing-the-poster).
571
623
 
572
- ### Individual Markers
624
+ ## Individual Markers
573
625
  Besides planes, gazeMapper can also be configured to detect and report on the appearance of individual markers. This is configured in the `Individual markers editor` pane in the GUI or by means of `gazeMapper.marker.Marker` objects.
574
626
 
575
627
  ## Actions
@@ -1,5 +1,4 @@
1
- glassesTools[GUI]>=1.16.0 # pulls in various other dependencies such as imgui_bundle, matplotlib, numpy, opencv, pandas, polars
1
+ glassesTools[GUI]>=1.17.1 # pulls in various other dependencies such as imgui_bundle, matplotlib, numpy, opencv, pandas, polars
2
2
  typeguard
3
3
  uvloop ; sys_platform != "win32"
4
- watchfiles
5
- pebble
4
+ watchfiles
@@ -8,16 +8,23 @@ import pathvalidate
8
8
  import threading
9
9
  from imgui_bundle import imgui, imspinner, hello_imgui, icons_fontawesome_6 as ifa6
10
10
 
11
- from glassesTools import annotation, aruco, async_thread, camera_recording, eyetracker, gui as gt_gui, naming as gt_naming, platform, recording, video_utils
11
+ from glassesTools import annotation, aruco, async_thread, camera_recording, eyetracker, gui as gt_gui, naming as gt_naming, platform, process_pool, recording, video_utils
12
12
  from glassesTools.validation import config as val_config, DataQualityType, export, get_DataQualityType_explanation
13
+ from glassesTools.gui import colors
13
14
 
14
- from . import colors, utils
15
+ from . import utils
15
16
  from ... import config, marker, naming, plane, process, session
16
17
 
18
+ _picker_last_folder = None
17
19
  def get_folder_picker(g, reason: str, *args, **kwargs):
18
20
  from . import gui
19
21
  g = typing.cast(gui.GUI,g) # indicate type to typechecker
20
22
  def select_callback(selected: list[pathlib.Path]):
23
+ global _picker_last_folder
24
+ if len(selected)==1 and selected[0].is_dir():
25
+ _picker_last_folder = selected[0]
26
+ else:
27
+ _picker_last_folder = selected[0].parent
21
28
  match reason:
22
29
  case 'loading' | 'creating':
23
30
  try_load_project(g, selected, action=reason)
@@ -69,7 +76,7 @@ def get_folder_picker(g, reason: str, *args, **kwargs):
69
76
  picker_type = gt_gui.file_picker.DirPicker
70
77
  case _:
71
78
  raise ValueError(f'reason "{reason}" not understood')
72
- picker = picker_type(title=header, allow_multiple=allow_multiple, callback=select_callback)
79
+ picker = picker_type(title=header, allow_multiple=allow_multiple, callback=select_callback, start_dir=_picker_last_folder)
73
80
  picker.set_show_only_dirs(False)
74
81
  return picker
75
82
 
@@ -335,27 +342,31 @@ def show_export_config(g, path: str|pathlib.Path, sessions: list[str]):
335
342
 
336
343
  # for the sessions, see what we have to export
337
344
  to_export: dict[str,bool] = {}
338
- rec_dirs: list[pathlib.Path] = []
345
+ rec_dirs_val: list[pathlib.Path] = []
346
+ rec_dirs_et_sync: list[pathlib.Path] = []
339
347
  for s_name in sessions:
340
348
  s = g.sessions.get(s_name,None)
341
349
  if s is None:
342
350
  continue
343
351
  if 'plane gaze' not in to_export:
344
- if annotation.Event.Trial in g.study_config.planes_per_episode and any((s.recordings[r].state[process.Action.GAZE_TO_PLANE]==process.State.Completed for r in s.recordings)):
352
+ if annotation.Event.Trial in g.study_config.planes_per_episode and any((s.recordings[r].state[process.Action.GAZE_TO_PLANE]==process_pool.State.Completed for r in s.recordings)):
345
353
  to_export['plane gaze'] = True
346
- if recs:=[s.recordings[r].info.working_directory for r in s.recordings if s.recordings[r].state[process.Action.VALIDATE]==process.State.Completed]:
354
+ if recs:=[s.recordings[r].info.working_directory for r in s.recordings if s.recordings[r].state[process.Action.VALIDATE]==process_pool.State.Completed]:
347
355
  to_export['validation'] = True
348
- rec_dirs.extend(recs)
356
+ rec_dirs_val.extend(recs)
357
+ if recs:=[s.recordings[r].info.working_directory for r in s.recordings if s.recordings[r].state[process.Action.SYNC_ET_TO_CAM]==process_pool.State.Completed]:
358
+ to_export['eye tracker synchronization'] = True
359
+ rec_dirs_et_sync.extend(recs)
349
360
  if 'gaze overlay video' not in to_export:
350
- if any((s.recordings[r].state[process.Action.MAKE_GAZE_OVERLAY_VIDEO]==process.State.Completed for r in s.recordings)):
361
+ if any((s.recordings[r].state[process.Action.MAKE_GAZE_OVERLAY_VIDEO]==process_pool.State.Completed for r in s.recordings)):
351
362
  to_export['gaze overlay video'] = True
352
363
  if 'mapped gaze video' not in to_export:
353
- if s.state[process.Action.MAKE_MAPPED_GAZE_VIDEO]==process.State.Completed:
364
+ if s.state[process.Action.MAKE_MAPPED_GAZE_VIDEO]==process_pool.State.Completed:
354
365
  to_export['mapped gaze video'] = True
355
366
 
356
367
  dq_df, dq_set = None, None
357
- if rec_dirs:
358
- dq_df, default_dq_type, dq_targets = export.collect_data_quality(rec_dirs, {p:f'{naming.validation_prefix}{p}_data_quality.tsv' for p in g.study_config.planes_per_episode[annotation.Event.Validate]}, col_for_parent='session')
368
+ if rec_dirs_val:
369
+ dq_df, default_dq_type, dq_targets = export.collect_data_quality(rec_dirs_val, {p:f'{naming.validation_prefix}{p}_data_quality.tsv' for p in g.study_config.planes_per_episode[annotation.Event.Validate]}, col_for_parent='session')
359
370
  if dq_df is None:
360
371
  to_export.pop('validation', None)
361
372
  else:
@@ -465,7 +476,7 @@ def show_export_config(g, path: str|pathlib.Path, sessions: list[str]):
465
476
  def launch_export():
466
477
  exp = []
467
478
  if 'plane gaze' in to_export and to_export['plane gaze']:
468
- exp.append('planeGaze')
479
+ exp.append('plane_gaze')
469
480
  if 'gaze overlay video' in to_export and to_export['gaze overlay video']:
470
481
  exp.append('gaze_overlay_video')
471
482
  if 'mapped gaze video' in to_export and to_export['mapped gaze video']:
@@ -479,6 +490,10 @@ def show_export_config(g, path: str|pathlib.Path, sessions: list[str]):
479
490
  targets = [t for t in dq_set['targets'] if dq_set['targets'][t]]
480
491
  export.summarize_and_store_data_quality(dq_df, path/'data_quality.tsv', dq_types, targets, dq_set['targets_avg'], dq_set['include_data_loss'])
481
492
 
493
+ # export et sync for all recordings in all sessions
494
+ if 'eye tracker synchronization' in to_export and to_export['eye tracker synchronization']:
495
+ export.export_et_sync(rec_dirs_et_sync, naming.VOR_sync_file, path/'et_sync.tsv')
496
+
482
497
  buttons = {
483
498
  ifa6.ICON_FA_CHECK+f" Continue": (launch_export, lambda: not any((to_export[e] for e in to_export))),
484
499
  ifa6.ICON_FA_CIRCLE_XMARK+f" Cancel": None
@@ -19,11 +19,12 @@ import OpenGL
19
19
  import OpenGL.GL as gl
20
20
 
21
21
  import glassesTools
22
- from glassesTools import annotation, gui as gt_gui, naming as gt_naming, plane as gt_plane, platform as gt_platform
22
+ from glassesTools import annotation, gui as gt_gui, naming as gt_naming, plane as gt_plane, platform as gt_platform, process_pool
23
+ from glassesTools.gui import colors
23
24
 
24
25
  from ... import config, marker, plane, process, project_watcher, session, type_utils, version
25
26
  from .. import async_thread
26
- from . import callbacks, colors, image_helper, process_pool, session_lister, settings_editor, utils
27
+ from . import callbacks, image_helper, session_lister, settings_editor, utils
27
28
 
28
29
 
29
30
  class GUI:
@@ -418,7 +419,7 @@ class GUI:
418
419
  # set pending and running states since these are not stored in the states file
419
420
  for job_id in self.job_scheduler.jobs:
420
421
  job_state = self.job_scheduler.jobs[job_id].get_state()
421
- if job_state in [process.State.Pending, process.State.Running]:
422
+ if job_state in [process_pool.State.Pending, process_pool.State.Running]:
422
423
  self._update_job_states_impl(self.job_scheduler.jobs[job_id].user_data, job_state)
423
424
 
424
425
  # if there are no jobs left, clean up process pool
@@ -432,7 +433,7 @@ class GUI:
432
433
  return False
433
434
  return True
434
435
 
435
- def _update_job_states_impl(self, job: utils.JobInfo, job_state: process.State):
436
+ def _update_job_states_impl(self, job: utils.JobInfo, job_state: process_pool.State):
436
437
  sess = self.sessions.get(job.session,None)
437
438
  if sess is None:
438
439
  return
@@ -446,10 +447,10 @@ class GUI:
446
447
  else:
447
448
  sess.state[job.action] = job_state
448
449
 
449
- def _action_done_callback(self, future: process_pool.ProcessFuture, job_id: int, job: utils.JobInfo, state: process.State):
450
+ def _action_done_callback(self, future: process_pool.ProcessFuture, job_id: int, job: utils.JobInfo, state: process_pool.State):
450
451
  # if process failed, notify error
451
452
  session_level = job.recording is None
452
- if state==process.State.Failed:
453
+ if state==process_pool.State.Failed:
453
454
  exc = future.exception() # should not throw exception since CancelledError is already encoded in state and future is done
454
455
  tb = gt_gui.utils.get_traceback(type(exc), exc, exc.__traceback__)
455
456
  lbl = f'session "{job.session}"'
@@ -460,7 +461,7 @@ class GUI:
460
461
  self.job_scheduler.jobs[job_id].error = tb
461
462
 
462
463
  # clean up, if needed, when a task failed or was canceled
463
- if job.action==process.Action.IMPORT and state in [process.State.Canceled, process.State.Failed]:
464
+ if job.action==process.Action.IMPORT and state in [process_pool.State.Canceled, process_pool.State.Failed]:
464
465
  # remove working directory if this was an import task
465
466
  async_thread.run(callbacks.remove_recording_working_dir(self.project_dir, job.session, job.recording))
466
467
  # also update GUI state, might be needed if there was no working directory yet for instance
@@ -580,7 +581,7 @@ class GUI:
580
581
  actions = process.get_actions_for_config(cfg, exclude_session_level=True)
581
582
  def _draw_status(action: process.Action, item: session.Recording):
582
583
  if process.is_action_possible_for_recording(item.definition.name, item.definition.type, action, cfg):
583
- session_lister.draw_process_state(item.state[action])
584
+ gt_gui.utils.draw_process_state(item.state[action])
584
585
  else:
585
586
  imgui.text('-')
586
587
  if action==process.Action.AUTO_CODE_TRIALS and cfg.sync_ref_recording and item.definition.name!=cfg.sync_ref_recording:
@@ -815,7 +816,7 @@ class GUI:
815
816
  jobs = self.job_scheduler.jobs.copy()
816
817
  job_ids = sorted(jobs.keys())
817
818
  job_states = {job_id:jobs[job_id].get_state() for job_id in job_ids}
818
- if any((job_states[i] in [process.State.Pending, process.State.Running] for i in job_states)):
819
+ if any((job_states[i] in [process_pool.State.Pending, process_pool.State.Running] for i in job_states)):
819
820
  if imgui.button(ifa6.ICON_FA_HAND+' Cancel all'):
820
821
  self.job_scheduler.cancel_all_jobs()
821
822
 
@@ -873,14 +874,14 @@ class GUI:
873
874
  imgui.text(f'{job_id}')
874
875
  case 1:
875
876
  # Status
876
- session_lister.draw_process_state((job_state:=jobs[job_id].get_state()))
877
+ gt_gui.utils.draw_process_state((job_state:=jobs[job_id].get_state()))
877
878
  if jobs[job_id].error:
878
879
  gt_gui.utils.draw_hover_text(jobs[job_id].error, text='')
879
880
  imgui.same_line()
880
881
  if imgui.small_button(ifa6.ICON_FA_COPY+f'##{job_id}_copy_error'):
881
882
  imgui.set_clipboard_text(jobs[job_id].error)
882
883
  gt_gui.utils.draw_hover_text('Copy error to clipboard', text='')
883
- if job_state in [process.State.Pending, process.State.Running]:
884
+ if job_state in [process_pool.State.Pending, process_pool.State.Running]:
884
885
  imgui.same_line()
885
886
  if imgui.small_button(ifa6.ICON_FA_HAND+f' Cancel##{job_id}'):
886
887
  self.job_scheduler.cancel_job(job_id)
@@ -1207,7 +1208,7 @@ class GUI:
1207
1208
  def _get_pending_running_job_list(self) -> dict[utils.JobInfo, int]:
1208
1209
  active_jobs: dict[utils.JobInfo, int] = {}
1209
1210
  for job_id in self.job_scheduler.jobs:
1210
- if self.job_scheduler.jobs[job_id].get_state() in [process.State.Pending, process.State.Running]:
1211
+ if self.job_scheduler.jobs[job_id].get_state() in [process_pool.State.Pending, process_pool.State.Running]:
1211
1212
  active_jobs[self.job_scheduler.jobs[job_id].user_data] = job_id
1212
1213
  return active_jobs
1213
1214
 
@@ -1216,14 +1217,14 @@ class GUI:
1216
1217
  imgui.text_colored(colors.error, '-')
1217
1218
  else:
1218
1219
  if process.is_session_level_action(action):
1219
- session_lister.draw_process_state(item.state[action])
1220
+ gt_gui.utils.draw_process_state(item.state[action])
1220
1221
  else:
1221
1222
  cfg = self.session_config_overrides[item.name].apply(self.study_config, strict_check=False) if item.name in self.session_config_overrides else self.study_config
1222
1223
  states = {r:item.recordings[r].state[action] for r in item.recordings if process.is_action_possible_for_recording(r, item.definition.get_recording_def(r).type, action, cfg)}
1223
- not_completed = [r for r in states if states[r]!=process.State.Completed]
1224
- if any(st:=[s for r in states if (s:=states[r]) in [process.State.Pending, process.State.Running]]):
1224
+ not_completed = [r for r in states if states[r]!=process_pool.State.Completed]
1225
+ if any(st:=[s for r in states if (s:=states[r]) in [process_pool.State.Pending, process_pool.State.Running]]):
1225
1226
  # progress marker
1226
- session_lister.draw_process_state(process.State.Running if process.State.Running in st else process.State.Pending, have_hover_popup=False)
1227
+ gt_gui.utils.draw_process_state(process_pool.State.Running if process_pool.State.Running in st else process_pool.State.Pending, have_hover_popup=False)
1227
1228
  else:
1228
1229
  n_rec = len(states)
1229
1230
  clr = colors.error if not_completed else colors.ok
@@ -1276,10 +1277,10 @@ class GUI:
1276
1277
  else:
1277
1278
  hover_text = process.get_impossible_reason_text(a, actions)
1278
1279
  status = [self.sessions[s].recordings[r].state[a] for s in actions if a in actions[s] for r in actions[s][a][0]]
1279
- status = max(status) if status else process.State.Not_Run
1280
+ status = max(status) if status else process_pool.State.Not_Run
1280
1281
  if a.has_options and possible:
1281
1282
  hover_text += '\nShift-click to bring up a popup with configuration options for this run.'
1282
- icon = ifa6.ICON_FA_PLAY if status<process.State.Completed else ifa6.ICON_FA_ARROW_ROTATE_RIGHT
1283
+ icon = ifa6.ICON_FA_PLAY if status<process_pool.State.Completed else ifa6.ICON_FA_ARROW_ROTATE_RIGHT
1283
1284
  if not possible:
1284
1285
  imgui.begin_disabled()
1285
1286
  if imgui.selectable(icon+f" {a.displayable_name}", False)[0]:
@@ -1343,10 +1344,10 @@ class GUI:
1343
1344
  else:
1344
1345
  hover_text = process.get_impossible_reason_text(a, actions, for_recording=True)
1345
1346
  status = [sess.recordings[r].state[a] for r in to_run]
1346
- status = max(status) if status else process.State.Not_Run
1347
+ status = max(status) if status else process_pool.State.Not_Run
1347
1348
  if a.has_options and possible:
1348
1349
  hover_text += '\nShift-click to bring up a popup with configuration options for this run.'
1349
- icon = ifa6.ICON_FA_PLAY if status<process.State.Completed else ifa6.ICON_FA_ARROW_ROTATE_RIGHT
1350
+ icon = ifa6.ICON_FA_PLAY if status<process_pool.State.Completed else ifa6.ICON_FA_ARROW_ROTATE_RIGHT
1350
1351
  if not possible:
1351
1352
  imgui.begin_disabled()
1352
1353
  if imgui.selectable(icon+f" {a.displayable_name}##{session_name}", False)[0]:
@@ -1490,7 +1491,7 @@ class GUI:
1490
1491
  if session_level_actions and imgui.begin_table(f'##{sess.name}_session_level', 2, imgui.TableFlags_.sizing_fixed_fit):
1491
1492
  for a in session_level_actions:
1492
1493
  imgui.table_next_column()
1493
- session_lister.draw_process_state(sess.state[a])
1494
+ gt_gui.utils.draw_process_state(sess.state[a])
1494
1495
  imgui.table_next_column()
1495
1496
  imgui.selectable(a.displayable_name, False, imgui.SelectableFlags_.span_all_columns|imgui.SelectableFlags_.allow_overlap)
1496
1497
  if (a in menu_actions or a in menu_actions_running) and imgui.begin_popup_context_item(f"##{sess.name}_{a}_context"):
@@ -1506,7 +1507,7 @@ class GUI:
1506
1507
  if a.has_options and possible:
1507
1508
  hover_text += '\nShift-click to bring up a popup with configuration options for this run.'
1508
1509
  status = self.sessions[sess.name].state[a]
1509
- icon = ifa6.ICON_FA_PLAY if status<process.State.Completed else ifa6.ICON_FA_ARROW_ROTATE_RIGHT
1510
+ icon = ifa6.ICON_FA_PLAY if status<process_pool.State.Completed else ifa6.ICON_FA_ARROW_ROTATE_RIGHT
1510
1511
  if not possible:
1511
1512
  imgui.begin_disabled()
1512
1513
  if imgui.selectable(icon+f" {a.displayable_name}##{sess.name}", False)[0]: