gazeMapper 0.5.0__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.
- gazemapper-0.5.0/LICENSE +21 -0
- gazemapper-0.5.0/MANIFEST.in +1 -0
- gazemapper-0.5.0/PKG-INFO +77 -0
- gazemapper-0.5.0/README.md +37 -0
- gazemapper-0.5.0/pyproject.toml +6 -0
- gazemapper-0.5.0/requirements.txt +2 -0
- gazemapper-0.5.0/setup.cfg +4 -0
- gazemapper-0.5.0/setup.py +50 -0
- gazemapper-0.5.0/src/gazeMapper/__init__.py +0 -0
- gazemapper-0.5.0/src/gazeMapper/camera_recording.py +79 -0
- gazemapper-0.5.0/src/gazeMapper/config.py +82 -0
- gazemapper-0.5.0/src/gazeMapper/episode.py +106 -0
- gazemapper-0.5.0/src/gazeMapper/plane.py +109 -0
- gazemapper-0.5.0/src/gazeMapper/session.py +193 -0
- gazemapper-0.5.0/src/gazeMapper/version.py +13 -0
- gazemapper-0.5.0/src/gazeMapper.egg-info/PKG-INFO +77 -0
- gazemapper-0.5.0/src/gazeMapper.egg-info/SOURCES.txt +18 -0
- gazemapper-0.5.0/src/gazeMapper.egg-info/dependency_links.txt +1 -0
- gazemapper-0.5.0/src/gazeMapper.egg-info/requires.txt +2 -0
- gazemapper-0.5.0/src/gazeMapper.egg-info/top_level.txt +1 -0
gazemapper-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Diederick Niehorster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include requirements.txt
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: gazeMapper
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Automated world-based analysis of wearable eye tracker data.
|
|
5
|
+
Home-page: https://github.com/dcnieho/gazeMapper
|
|
6
|
+
Author: Diederick Niehorster
|
|
7
|
+
Author-email: diederick_c.niehorster@humlab.lu.se
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2024 Diederick Niehorster
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Source Code, https://github.com/dcnieho/gazeMapper
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Requires-Python: >=3.10
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: glassesTools[GUI]==1.4.0
|
|
39
|
+
Requires-Dist: glassesValidator==2.0.0
|
|
40
|
+
|
|
41
|
+
[](https://pepy.tech/project/gazeMapper)
|
|
42
|
+
[](https://scholar.google.com/citations?view_op=view_citation&citation_for_view=uRUYoVgAAAAJ:uWQEDVKXjbEC)
|
|
43
|
+
[](https://pypi.org/project/gazeMapper/)
|
|
44
|
+
[](https://pypi.org/project/gazeMapper/)
|
|
45
|
+
|
|
46
|
+
# gazeMapper v0.5.0
|
|
47
|
+
Tool for automated world-based analysis of wearable eye tracker data.
|
|
48
|
+
|
|
49
|
+
If you use this tool or any of the code in this repository, please cite:<br>
|
|
50
|
+
Niehorster, D.C., Hessels, R.S., Nyström, M., Benjamins, J.S. and Hooge, I.T.C. (in prep). gazeMapper: A tool for automated world-based analysis of wearable eye tracker data
|
|
51
|
+
|
|
52
|
+
# How to acquire
|
|
53
|
+
GlassesValidator is available from `https://github.com/dcnieho/gazeMapper`, and supports Python 3.10 and 3.11 on Windows, MacOS and Linux.
|
|
54
|
+
|
|
55
|
+
For Windows users who wish to use the gazeMapper GUI, the easiest way to acquire gazeMapper is to [download
|
|
56
|
+
a standalone executable](https://github.com/dcnieho/gazeMapper/releases/latest). The standalone executable is not
|
|
57
|
+
available for MacOS or Linux.
|
|
58
|
+
|
|
59
|
+
For users on Windows, Mac or Linux who wish to use gazeMapper in their Python code, the easiest way to acquire
|
|
60
|
+
gazeMapper is to install it directly into your Python distribution using the command
|
|
61
|
+
`python -m pip install gazeMapper`. Should that fail, this repository is pip-installable as well:
|
|
62
|
+
`python -m pip install git+https://github.com/dcnieho/gazeMapper.git#egg=gazeMapper`. NB: on some platforms you may have
|
|
63
|
+
to replace `python` with `python3` in the above command lines.
|
|
64
|
+
|
|
65
|
+
Once pip-installed in your Python distribution, there are three ways to run the GUI on any of the supported operating systems:
|
|
66
|
+
1. Directly in the terminal of your operating system, type `gazeMapper` and run it.
|
|
67
|
+
2. Open a Python console. From such a console, running the GUI requires only the following two lines of code:
|
|
68
|
+
```python
|
|
69
|
+
import gazeMapper
|
|
70
|
+
gazeMapper.GUI.run()
|
|
71
|
+
```
|
|
72
|
+
3. If you run the gazeMapper's GUI from a script, make sure to wrap your script in `if __name__=="__main__"`. This is required for correct operation from a script because the GUI uses multiprocessing functionality. Do as follows:
|
|
73
|
+
```python
|
|
74
|
+
if __name__=="__main__":
|
|
75
|
+
import gazeMapper
|
|
76
|
+
gazeMapper.GUI.run()
|
|
77
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[](https://pepy.tech/project/gazeMapper)
|
|
2
|
+
[](https://scholar.google.com/citations?view_op=view_citation&citation_for_view=uRUYoVgAAAAJ:uWQEDVKXjbEC)
|
|
3
|
+
[](https://pypi.org/project/gazeMapper/)
|
|
4
|
+
[](https://pypi.org/project/gazeMapper/)
|
|
5
|
+
|
|
6
|
+
# gazeMapper v0.5.0
|
|
7
|
+
Tool for automated world-based analysis of wearable eye tracker data.
|
|
8
|
+
|
|
9
|
+
If you use this tool or any of the code in this repository, please cite:<br>
|
|
10
|
+
Niehorster, D.C., Hessels, R.S., Nyström, M., Benjamins, J.S. and Hooge, I.T.C. (in prep). gazeMapper: A tool for automated world-based analysis of wearable eye tracker data
|
|
11
|
+
|
|
12
|
+
# How to acquire
|
|
13
|
+
GlassesValidator is available from `https://github.com/dcnieho/gazeMapper`, and supports Python 3.10 and 3.11 on Windows, MacOS and Linux.
|
|
14
|
+
|
|
15
|
+
For Windows users who wish to use the gazeMapper GUI, the easiest way to acquire gazeMapper is to [download
|
|
16
|
+
a standalone executable](https://github.com/dcnieho/gazeMapper/releases/latest). The standalone executable is not
|
|
17
|
+
available for MacOS or Linux.
|
|
18
|
+
|
|
19
|
+
For users on Windows, Mac or Linux who wish to use gazeMapper in their Python code, the easiest way to acquire
|
|
20
|
+
gazeMapper is to install it directly into your Python distribution using the command
|
|
21
|
+
`python -m pip install gazeMapper`. Should that fail, this repository is pip-installable as well:
|
|
22
|
+
`python -m pip install git+https://github.com/dcnieho/gazeMapper.git#egg=gazeMapper`. NB: on some platforms you may have
|
|
23
|
+
to replace `python` with `python3` in the above command lines.
|
|
24
|
+
|
|
25
|
+
Once pip-installed in your Python distribution, there are three ways to run the GUI on any of the supported operating systems:
|
|
26
|
+
1. Directly in the terminal of your operating system, type `gazeMapper` and run it.
|
|
27
|
+
2. Open a Python console. From such a console, running the GUI requires only the following two lines of code:
|
|
28
|
+
```python
|
|
29
|
+
import gazeMapper
|
|
30
|
+
gazeMapper.GUI.run()
|
|
31
|
+
```
|
|
32
|
+
3. If you run the gazeMapper's GUI from a script, make sure to wrap your script in `if __name__=="__main__"`. This is required for correct operation from a script because the GUI uses multiprocessing functionality. Do as follows:
|
|
33
|
+
```python
|
|
34
|
+
if __name__=="__main__":
|
|
35
|
+
import gazeMapper
|
|
36
|
+
gazeMapper.GUI.run()
|
|
37
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
import runpy
|
|
3
|
+
|
|
4
|
+
info = runpy.run_path("src/gazeMapper/version.py")
|
|
5
|
+
|
|
6
|
+
with open('README.md') as f:
|
|
7
|
+
readme = f.read()
|
|
8
|
+
|
|
9
|
+
with open('LICENSE') as f:
|
|
10
|
+
license = f.read()
|
|
11
|
+
|
|
12
|
+
with open('requirements.txt') as f:
|
|
13
|
+
requirements = f.read().splitlines()
|
|
14
|
+
|
|
15
|
+
required = []
|
|
16
|
+
|
|
17
|
+
# get required packages
|
|
18
|
+
for line in requirements:
|
|
19
|
+
required.append(line)
|
|
20
|
+
|
|
21
|
+
setuptools.setup(
|
|
22
|
+
name=info['__title__'],
|
|
23
|
+
version=info['__version__'],
|
|
24
|
+
author=info['__author__'],
|
|
25
|
+
author_email=info['__email__'],
|
|
26
|
+
description=info['__description__'],
|
|
27
|
+
long_description=readme,
|
|
28
|
+
long_description_content_type="text/markdown",
|
|
29
|
+
url=info['__url__'],
|
|
30
|
+
project_urls={
|
|
31
|
+
"Source Code": info['__url__'],
|
|
32
|
+
},
|
|
33
|
+
classifiers=[
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"License :: OSI Approved :: MIT License",
|
|
37
|
+
"Operating System :: OS Independent",
|
|
38
|
+
],
|
|
39
|
+
license=license,
|
|
40
|
+
package_dir={"": "src"},
|
|
41
|
+
packages=setuptools.find_packages(where="src"),
|
|
42
|
+
include_package_data=True,
|
|
43
|
+
python_requires=">=3.10",
|
|
44
|
+
install_requires=required,
|
|
45
|
+
# entry_points={
|
|
46
|
+
# "gui_scripts": [
|
|
47
|
+
# "gazeMapper = gazeMapper.GUI:run",
|
|
48
|
+
# ],
|
|
49
|
+
# },
|
|
50
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import pathlib
|
|
3
|
+
import typing
|
|
4
|
+
import shutil
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from glassesTools import utils, video_utils
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass
|
|
11
|
+
class Recording:
|
|
12
|
+
default_json_file_name : typing.ClassVar[str] = 'recording_info.json'
|
|
13
|
+
|
|
14
|
+
name : str
|
|
15
|
+
video_file : str
|
|
16
|
+
source_directory : pathlib.Path
|
|
17
|
+
working_directory : pathlib.Path = ""
|
|
18
|
+
duration : int = None
|
|
19
|
+
|
|
20
|
+
def store_as_json(self, path: str | pathlib.Path):
|
|
21
|
+
path = pathlib.Path(path)
|
|
22
|
+
if path.is_dir():
|
|
23
|
+
path /= self.default_json_file_name
|
|
24
|
+
with open(path, 'w') as f:
|
|
25
|
+
# remove any crap potentially added by subclasses
|
|
26
|
+
to_dump = dataclasses.asdict(self)
|
|
27
|
+
to_dump = {k:to_dump[k] for k in to_dump if k in Recording.__annotations__}
|
|
28
|
+
# dump to file
|
|
29
|
+
json.dump(to_dump, f, cls=utils.CustomTypeEncoder, indent=2)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def load_from_json(path: str | pathlib.Path):
|
|
33
|
+
path = pathlib.Path(path)
|
|
34
|
+
if path.is_dir():
|
|
35
|
+
path /= Recording.default_json_file_name
|
|
36
|
+
with open(path, 'r') as f:
|
|
37
|
+
return Recording(**json.load(f, object_hook=utils.json_reconstitute))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_video_path(self):
|
|
41
|
+
vid = self.working_directory / self.video_file
|
|
42
|
+
if not vid.is_file():
|
|
43
|
+
vid = self.source_directory / self.video_file
|
|
44
|
+
return vid
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def do_import(rec_info: Recording, cam_cal_file: str|pathlib.Path=None, copy_video=True):
|
|
49
|
+
assert rec_info.working_directory
|
|
50
|
+
rec_info.working_directory = pathlib.Path(rec_info.working_directory)
|
|
51
|
+
ifile = (rec_info.source_directory / rec_info.video_file)
|
|
52
|
+
assert ifile.is_file()
|
|
53
|
+
print(f'processing: {rec_info.video_file} -> {rec_info.working_directory}')
|
|
54
|
+
|
|
55
|
+
if not rec_info.working_directory.is_dir():
|
|
56
|
+
rec_info.working_directory.mkdir()
|
|
57
|
+
|
|
58
|
+
if copy_video:
|
|
59
|
+
ofile = rec_info.working_directory / rec_info.video_file
|
|
60
|
+
print(' Copy video file...')
|
|
61
|
+
shutil.copy2(ifile, ofile)
|
|
62
|
+
|
|
63
|
+
# also get its calibration
|
|
64
|
+
print(' Getting camera calibration...')
|
|
65
|
+
if cam_cal_file is not None:
|
|
66
|
+
shutil.copy2(str(cam_cal_file), str(rec_info.working_directory / 'calibration.xml'))
|
|
67
|
+
else:
|
|
68
|
+
print(' !! No camera calibration provided! Defaulting to hardcoded')
|
|
69
|
+
|
|
70
|
+
# and frame timestamps
|
|
71
|
+
print(' Getting frame timestamps...')
|
|
72
|
+
ts = video_utils.getFrameTimestampsFromVideo(rec_info.get_video_path())
|
|
73
|
+
ts.to_csv(str(rec_info.working_directory / 'frameTimestamps.tsv'), sep='\t')
|
|
74
|
+
rec_info.duration = (ts.timestamp.iat[-1]-ts.timestamp.iat[0])/1000. # ms -> s
|
|
75
|
+
|
|
76
|
+
# store recording info to folder
|
|
77
|
+
rec_info.store_as_json(rec_info.working_directory)
|
|
78
|
+
|
|
79
|
+
return rec_info
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from glassesTools import utils
|
|
6
|
+
|
|
7
|
+
from . import episode, plane, session
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Marker:
|
|
11
|
+
def __init__(self, id:int, size:float):
|
|
12
|
+
self.id = id
|
|
13
|
+
self.size = size
|
|
14
|
+
utils.register_type(utils.CustomTypeEntry(Marker,'__config.Marker__',lambda x: {'id': x.id, 'size': x.size}, lambda x: Marker(**x)))
|
|
15
|
+
|
|
16
|
+
def get_marker_dict_from_list(markers: list[Marker]) -> dict[int,dict[str,Any]]:
|
|
17
|
+
out = {}
|
|
18
|
+
for m in markers:
|
|
19
|
+
out[m.id] = {'marker_size': m.size}
|
|
20
|
+
return out
|
|
21
|
+
|
|
22
|
+
class Study:
|
|
23
|
+
default_json_file_name = 'study_def.json'
|
|
24
|
+
|
|
25
|
+
def __init__(self, session_def: session.SessionDefinition, planes: list[plane.Definition], planes_per_interval: dict[episode.Event,list[str]], individual_markers: list[Marker], working_directory: str|pathlib.Path):
|
|
26
|
+
self.session_def = session_def
|
|
27
|
+
self.planes = planes
|
|
28
|
+
self.planes_per_interval= planes_per_interval
|
|
29
|
+
self.working_directory = working_directory
|
|
30
|
+
self.individual_markers = individual_markers
|
|
31
|
+
|
|
32
|
+
self._check_planes_per_interval()
|
|
33
|
+
|
|
34
|
+
def _check_planes_per_interval(self):
|
|
35
|
+
for e in self.planes_per_interval:
|
|
36
|
+
for p in self.planes_per_interval[e]:
|
|
37
|
+
if not any([p==pl.name for pl in self.planes]):
|
|
38
|
+
raise ValueError(f'plane {p} not known')
|
|
39
|
+
|
|
40
|
+
def store_as_json(self, path: str | pathlib.Path):
|
|
41
|
+
path = pathlib.Path(path)
|
|
42
|
+
# this stores only the planes_per_interval variable to json, rest is read from other files
|
|
43
|
+
# instead to remain flexible and make it easy for users to rename, etc
|
|
44
|
+
d_path = path / self.default_json_file_name
|
|
45
|
+
with open(d_path, 'w') as f:
|
|
46
|
+
to_dump = {k:getattr(self,k) for k in ['planes_per_interval','individual_markers']} # only these fields. Name will be populated from name of session/provided folder, recordings from each subfolder in the session/provided folder, and working_directory as the provided path
|
|
47
|
+
to_dump['planes_per_interval'] = [(k, to_dump['planes_per_interval'][k]) for k in to_dump['planes_per_interval']] # pack as list of tuples for storage
|
|
48
|
+
# dump to file
|
|
49
|
+
json.dump(to_dump, f, cls=utils.CustomTypeEncoder, indent=2)
|
|
50
|
+
# this doesn't story any files itself, but triggers the contained info to be stored
|
|
51
|
+
self.session_def.store_as_json(self.working_directory / 'session_def.json')
|
|
52
|
+
for p in self.planes:
|
|
53
|
+
p_dir = self.working_directory / p.name
|
|
54
|
+
if not p_dir.is_dir():
|
|
55
|
+
p_dir.mkdir()
|
|
56
|
+
p.store_as_json(p_dir)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def load_from_json(path: str | pathlib.Path) -> 'Study':
|
|
60
|
+
path = pathlib.Path(path)
|
|
61
|
+
# get kwds
|
|
62
|
+
d_path = path / Study.default_json_file_name
|
|
63
|
+
with open(d_path, 'r') as f:
|
|
64
|
+
kwds = json.load(f, object_hook=utils.json_reconstitute)
|
|
65
|
+
kwds['planes_per_interval'] = {k:v for k,v in kwds['planes_per_interval']} # stored as list of tuples, unpack
|
|
66
|
+
# get session def
|
|
67
|
+
s_path = path / 'session_def.json'
|
|
68
|
+
if not s_path.is_file():
|
|
69
|
+
return None
|
|
70
|
+
sess_def = session.SessionDefinition.load_from_json(s_path)
|
|
71
|
+
|
|
72
|
+
# get planes
|
|
73
|
+
planes: list[plane.Definition] = []
|
|
74
|
+
for p_dir in path.iterdir():
|
|
75
|
+
if not p_dir.is_dir():
|
|
76
|
+
continue
|
|
77
|
+
p_file = p_dir / plane.Definition.default_json_file_name
|
|
78
|
+
if not p_file.is_file():
|
|
79
|
+
continue
|
|
80
|
+
planes.append(plane.Definition.load_from_json(p_file))
|
|
81
|
+
|
|
82
|
+
return Study(sess_def, planes, working_directory=path, **kwds)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
import pathlib
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from glassesTools import utils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Type(Enum):
|
|
11
|
+
Point = auto()
|
|
12
|
+
Interval = auto()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Event(utils.AutoName):
|
|
16
|
+
Validate = auto() # interval to be used for running glassesValidator
|
|
17
|
+
Sync_Camera = auto() # point to be used for synchronizing different cameras
|
|
18
|
+
Sync_VOR = auto() # episode to be used for VOR synchronization
|
|
19
|
+
Map = auto() # episode for which to map gaze to plane(s): output for files to be provided to user
|
|
20
|
+
events = [x.value for x in Event]
|
|
21
|
+
utils.register_type(utils.CustomTypeEntry(Event,'__enum.Event__',str, lambda x: getattr(Event, x.split('.')[1])))
|
|
22
|
+
|
|
23
|
+
type_map = {
|
|
24
|
+
Event.Validate : Type.Interval,
|
|
25
|
+
Event.Sync_Camera : Type.Point,
|
|
26
|
+
Event.Sync_VOR : Type.Interval,
|
|
27
|
+
Event.Map : Type.Interval,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Episode:
|
|
32
|
+
def __init__(self, event:Event, start_frame:int, end_frame:int=None):
|
|
33
|
+
self.event = event
|
|
34
|
+
self.start_frame = start_frame
|
|
35
|
+
|
|
36
|
+
if type_map[event]==Type.Interval:
|
|
37
|
+
assert end_frame is not None, f"end frame expected for an interval-type episode ({event.value}), but not provided"
|
|
38
|
+
if type_map[event]==Type.Point:
|
|
39
|
+
assert end_frame is None, f"end frame provided but not expected for a point-type episode ({event.value})"
|
|
40
|
+
self.end_frame = end_frame
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# for dealing with lists of events
|
|
44
|
+
def read_list_from_file(fileName: str|pathlib.Path) -> list[Episode]:
|
|
45
|
+
df = pd.read_csv(str(fileName), delimiter='\t', index_col=False, dtype=defaultdict(lambda: int, event=str, end_frame=pd.Int64Dtype()))
|
|
46
|
+
df['event'] = [Event(x) for x in df['event'].values]
|
|
47
|
+
df['end_frame'] = df['end_frame'].astype('object')
|
|
48
|
+
df.loc[pd.isnull(df['end_frame']),'end_frame'] = None # set missing to None
|
|
49
|
+
return [Episode(**kwargs) for kwargs in df.to_dict(orient='records')]
|
|
50
|
+
|
|
51
|
+
def write_list_to_file(episodes: list[Episode],
|
|
52
|
+
fileName: str|pathlib.Path):
|
|
53
|
+
if not episodes:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
records = [{k:getattr(p,k) for k in vars(p) if not k.startswith('_')} for p in episodes]
|
|
57
|
+
df = pd.DataFrame.from_records(records)
|
|
58
|
+
df['event'] = [x.value for x in df['event'].values]
|
|
59
|
+
|
|
60
|
+
# keep only columns to be written out and order them correctly
|
|
61
|
+
df = df[['event','start_frame','end_frame']]
|
|
62
|
+
df.to_csv(str(fileName), index=False, sep='\t', na_rep='nan')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_empty_marker_dict() -> dict[Event,list[int]]:
|
|
66
|
+
return {e:[] for e in Event}
|
|
67
|
+
|
|
68
|
+
def list_to_marker_dict(episodes: list[Episode]) -> dict[Event,list[list[int]]]:
|
|
69
|
+
e_dict = get_empty_marker_dict()
|
|
70
|
+
for e in episodes:
|
|
71
|
+
if e.end_frame is not None:
|
|
72
|
+
e_dict[e.event].append([e.start_frame, e.end_frame])
|
|
73
|
+
else:
|
|
74
|
+
e_dict[e.event].append([e.start_frame])
|
|
75
|
+
return e_dict
|
|
76
|
+
|
|
77
|
+
def marker_dict_to_list(episodes: dict[Event,list[int]]|dict[Event,list[list[int]]]) -> list[Episode]:
|
|
78
|
+
e_list: list[Episode] = []
|
|
79
|
+
for e in episodes:
|
|
80
|
+
if not episodes[e]:
|
|
81
|
+
continue
|
|
82
|
+
if isinstance(episodes[e][0],list):
|
|
83
|
+
e_list.extend([Episode(e, *v) for v in episodes[e]])
|
|
84
|
+
else:
|
|
85
|
+
if type_map[e]==Type.Interval:
|
|
86
|
+
for m in range(0,len(episodes[e])-1,2): # read in batches of two, and run until -1 to make sure we don't pick up incomplete intervals
|
|
87
|
+
e_list.append(Episode(e, *episodes[e][m:m+2]))
|
|
88
|
+
else:
|
|
89
|
+
e_list.extend([Episode(e,m) for m in episodes[e]])
|
|
90
|
+
|
|
91
|
+
return sorted(e_list, key=lambda x: x.start_frame)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_in_interval(episodes: dict[Event,list[int]]|list[Episode], idx: int) -> dict[Event, bool]:
|
|
95
|
+
if isinstance(episodes,dict):
|
|
96
|
+
episodes = marker_dict_to_list(episodes)
|
|
97
|
+
|
|
98
|
+
e_dict: dict[Event,bool] = {e:False for e in Event}
|
|
99
|
+
for e in episodes:
|
|
100
|
+
if type_map[e.event]==Type.Interval:
|
|
101
|
+
if idx>=e.start_frame and idx<=e.end_frame:
|
|
102
|
+
e_dict[e.event] = True
|
|
103
|
+
else:
|
|
104
|
+
if idx==e.start_frame:
|
|
105
|
+
e_dict[e.event] = True
|
|
106
|
+
return e_dict
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from enum import auto
|
|
2
|
+
import pathlib
|
|
3
|
+
import cv2
|
|
4
|
+
import json
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from glassesTools import plane, utils
|
|
8
|
+
from glassesValidator.config import get_validation_setup
|
|
9
|
+
from glassesValidator.config.poster import Poster
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Type(utils.AutoName):
|
|
13
|
+
GlassesValidator= auto()
|
|
14
|
+
Plane_2D = auto()
|
|
15
|
+
utils.register_type(utils.CustomTypeEntry(Type,'__enum.plane.Type__',str, lambda x: getattr(Type, x.split('.')[1])))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Definition:
|
|
19
|
+
default_json_file_name = 'plane_def.json'
|
|
20
|
+
|
|
21
|
+
def __init__(self, type:Type, name:str=None, use_default:bool=False, marker_file: str|pathlib.Path=None, marker_size: float=None, marker_border_bits: int=1, min_num_markers: int=3, center: np.ndarray=None, unit: str=None, aruco_dict: int=cv2.aruco.DICT_4X4_250, ref_image_width: int=1920):
|
|
22
|
+
self.type = type
|
|
23
|
+
self.name = name
|
|
24
|
+
self.use_default : bool = use_default # applies only to glassesValidator planes. If False, denotes this is the default/built-in glassesValidator plane, if True, denotes custom settings are expected
|
|
25
|
+
# the below are only for non-glassesValidator planes
|
|
26
|
+
self.marker_file : str|pathlib.Path = marker_file # if str or Path: file from which to read markers. Else direction N_markerx4 array. Should contain centers of markers
|
|
27
|
+
self.marker_size : float = marker_size # in "unit" units
|
|
28
|
+
self.marker_border_bits : int = marker_border_bits
|
|
29
|
+
self.min_num_markers : int = min_num_markers # minimum number of markers that should be to run pose estimation w.r.t. the plane
|
|
30
|
+
self.center : np.ndarray = center # center of plane, in coordinates of the input file
|
|
31
|
+
self.unit : str = unit
|
|
32
|
+
self.aruco_dict : int = aruco_dict
|
|
33
|
+
self.ref_image_width : int = ref_image_width
|
|
34
|
+
|
|
35
|
+
if isinstance(self.center,list):
|
|
36
|
+
self.center = np.array(self.center)
|
|
37
|
+
|
|
38
|
+
# check provided info
|
|
39
|
+
if self.type==Type.GlassesValidator:
|
|
40
|
+
# prevent bugs
|
|
41
|
+
assert self.marker_file is None, "The marker_file input argument should not be set when the plane is a GlassesValidator plane (would be ignored)"
|
|
42
|
+
assert self.marker_size is None, "The marker_size input argument should not be set when the plane is a GlassesValidator plane (would be ignored)"
|
|
43
|
+
# NB: all the other parameters are also ignored, but have meaningful defaults, so can't be checked
|
|
44
|
+
else:
|
|
45
|
+
assert self.marker_file is not None, "The marker_file input argument should be provided"
|
|
46
|
+
assert self.marker_size is not None, "The marker_size input argument should be provided"
|
|
47
|
+
# prevent bugs
|
|
48
|
+
assert self.use_default is False, "The use_default input argument is for GlassesValidator planes. It should be set to False (default) when the plane is not a GlassesValidator plane (would be ignored)"
|
|
49
|
+
|
|
50
|
+
def store_as_json(self, path: str | pathlib.Path):
|
|
51
|
+
path = pathlib.Path(path)
|
|
52
|
+
if path.is_dir():
|
|
53
|
+
path /= self.default_json_file_name
|
|
54
|
+
with open(path, 'w') as f:
|
|
55
|
+
json.dump(self, f, cls=utils.CustomTypeEncoder, indent=2)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def load_from_json(path: str | pathlib.Path) -> 'Definition':
|
|
59
|
+
path = pathlib.Path(path)
|
|
60
|
+
if path.is_dir():
|
|
61
|
+
path /= Definition.default_json_file_name
|
|
62
|
+
with open(path, 'r') as f:
|
|
63
|
+
dfntn = json.load(f, object_hook=utils.json_reconstitute)
|
|
64
|
+
dfntn.name=path.parent.name
|
|
65
|
+
return dfntn
|
|
66
|
+
|
|
67
|
+
def _to_dict(self):
|
|
68
|
+
return {
|
|
69
|
+
'type': self.type,
|
|
70
|
+
'use_default': self.use_default,
|
|
71
|
+
'marker_file': self.marker_file,
|
|
72
|
+
'marker_size': self.marker_size,
|
|
73
|
+
'marker_border_bits': self.marker_border_bits,
|
|
74
|
+
'min_num_markers': self.min_num_markers,
|
|
75
|
+
'center': self.center,
|
|
76
|
+
'unit': self.unit,
|
|
77
|
+
'aruco_dict': self.aruco_dict,
|
|
78
|
+
'ref_image_width': self.ref_image_width
|
|
79
|
+
}
|
|
80
|
+
utils.register_type(utils.CustomTypeEntry(Definition,'__plane.Definition',lambda x: x._to_dict(), lambda x: Definition(**x)))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_plane_from_path(path: str|pathlib.Path) -> plane.Plane:
|
|
84
|
+
path = pathlib.Path(path)
|
|
85
|
+
plane_def = Definition.load_from_json(path)
|
|
86
|
+
return get_plane_from_definition(plane_def, path)
|
|
87
|
+
|
|
88
|
+
def get_plane_from_definition(plane_def: Definition, path: str | pathlib.Path) -> plane.Plane:
|
|
89
|
+
# for loading a plane from a directory that doesn't contain a plane definition json file
|
|
90
|
+
# use the provided definition instead
|
|
91
|
+
if plane_def.type==Type.GlassesValidator:
|
|
92
|
+
validator_config_dir = None # use glassesValidator built-in/default
|
|
93
|
+
if not plane_def.use_default:
|
|
94
|
+
validator_config_dir = path
|
|
95
|
+
validation_setup = get_validation_setup(validator_config_dir)
|
|
96
|
+
return Poster(validator_config_dir, validation_setup, ref_image_store_path=path / plane.Plane.default_ref_image_name)
|
|
97
|
+
else:
|
|
98
|
+
pl = plane.Plane(
|
|
99
|
+
markers = path / plane_def.marker_file,
|
|
100
|
+
marker_size = plane_def.marker_size,
|
|
101
|
+
aruco_dict = plane_def.aruco_dict,
|
|
102
|
+
marker_border_bits = plane_def.marker_border_bits,
|
|
103
|
+
unit = plane_def.unit,
|
|
104
|
+
ref_image_store_path= path / plane.Plane.default_ref_image_name,
|
|
105
|
+
ref_image_width = plane_def.ref_image_width
|
|
106
|
+
)
|
|
107
|
+
if plane_def.center is not None:
|
|
108
|
+
pl.set_center(plane_def.center)
|
|
109
|
+
return pl
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
import pathlib
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from glassesTools import importing, utils
|
|
6
|
+
from glassesTools.recording import Recording as EyeTrackerRecording
|
|
7
|
+
|
|
8
|
+
from . import camera_recording
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RecordingType(Enum):
|
|
12
|
+
EyeTracker = auto()
|
|
13
|
+
Camera = auto()
|
|
14
|
+
utils.register_type(utils.CustomTypeEntry(RecordingType,'__enum.session.RecordingType__',str, lambda x: getattr(RecordingType, x.split('.')[1])))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RecordingDefinition:
|
|
19
|
+
def __init__(self, name:str, type:RecordingType):
|
|
20
|
+
self.name = name
|
|
21
|
+
self.type = type
|
|
22
|
+
utils.register_type(utils.CustomTypeEntry(RecordingDefinition,'__session.RecordingDefinition__',lambda x: {'name': x.name, 'type': x.type}, lambda x: RecordingDefinition(**x)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Recording:
|
|
26
|
+
def __init__(self, defition: RecordingDefinition, info:EyeTrackerRecording|camera_recording.Recording=None):
|
|
27
|
+
self.defition = defition
|
|
28
|
+
self.info = info
|
|
29
|
+
utils.register_type(utils.CustomTypeEntry(Recording,'__session.Recording__',lambda x: {'defition': x.defition, 'info': x.info}, lambda x: Recording(**x)))
|
|
30
|
+
|
|
31
|
+
def read_recording_info(working_dir: pathlib.Path, rec_type: RecordingType) -> tuple[EyeTrackerRecording|camera_recording.Recording, pathlib.Path]:
|
|
32
|
+
if rec_type==RecordingType.Camera:
|
|
33
|
+
rec_info = camera_recording.Recording.load_from_json(working_dir)
|
|
34
|
+
in_video = rec_info.get_video_path()
|
|
35
|
+
elif rec_type==RecordingType.EyeTracker:
|
|
36
|
+
rec_info = EyeTrackerRecording.load_from_json(working_dir)
|
|
37
|
+
in_video = rec_info.get_scene_video_path()
|
|
38
|
+
return rec_info, in_video
|
|
39
|
+
|
|
40
|
+
class SessionDefinition:
|
|
41
|
+
def __init__(self, recordings: list[RecordingDefinition]=None, sync_ref: str=None):
|
|
42
|
+
if recordings is None:
|
|
43
|
+
recordings = []
|
|
44
|
+
self.recordings = recordings
|
|
45
|
+
|
|
46
|
+
self.sync_ref: str = None
|
|
47
|
+
self.set_sync_ref(sync_ref)
|
|
48
|
+
|
|
49
|
+
def add_recording(self, recording: RecordingDefinition):
|
|
50
|
+
self.recordings.append(recording)
|
|
51
|
+
|
|
52
|
+
def set_sync_ref(self, which: str):
|
|
53
|
+
if not any([r.name==which for r in self.recordings]):
|
|
54
|
+
raise ValueError(f'recording "{which}" not known')
|
|
55
|
+
self.sync_ref = which
|
|
56
|
+
|
|
57
|
+
def get_recording(self, which: str) -> RecordingDefinition:
|
|
58
|
+
for r in self.recordings:
|
|
59
|
+
if r.name==which:
|
|
60
|
+
return r
|
|
61
|
+
raise ValueError(f'recording "{which}" not found')
|
|
62
|
+
|
|
63
|
+
def store_as_json(self, path: str | pathlib.Path):
|
|
64
|
+
path = pathlib.Path(path)
|
|
65
|
+
with open(path, 'w') as f:
|
|
66
|
+
json.dump(self, f, cls=utils.CustomTypeEncoder, indent=2)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def load_from_json(path: str | pathlib.Path) -> 'Session':
|
|
70
|
+
path = pathlib.Path(path)
|
|
71
|
+
with open(path, 'r') as f:
|
|
72
|
+
return json.load(f, object_hook=utils.json_reconstitute)
|
|
73
|
+
utils.register_type(utils.CustomTypeEntry(SessionDefinition,'__session.SessionDefinition__',lambda x: {'recordings': x.recordings, 'sync_ref': x.sync_ref}, lambda x: SessionDefinition(**x)))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Session:
|
|
77
|
+
default_json_file_name = 'session_info.json'
|
|
78
|
+
|
|
79
|
+
def __init__(self, definition: SessionDefinition, name: str, working_directory: str|pathlib.Path = None, recordings: dict[str,Recording] = None):
|
|
80
|
+
self.definition = definition
|
|
81
|
+
self.name = name
|
|
82
|
+
self.working_directory: pathlib.Path = pathlib.Path(working_directory) if working_directory else None
|
|
83
|
+
if not recordings:
|
|
84
|
+
recordings = {}
|
|
85
|
+
self.recordings = recordings
|
|
86
|
+
|
|
87
|
+
def create_working_directory(self, parent_directory: str|pathlib.Path):
|
|
88
|
+
self.working_directory = pathlib.Path(parent_directory) / self.name
|
|
89
|
+
if not self.working_directory.is_dir():
|
|
90
|
+
self.working_directory.mkdir()
|
|
91
|
+
|
|
92
|
+
def import_and_add_recording(self, which: str, rec_info: EyeTrackerRecording|camera_recording.Recording, copy_video = True) -> Recording:
|
|
93
|
+
rec_def = self.definition.get_recording(which)
|
|
94
|
+
self.check_recording_info(which, rec_info)
|
|
95
|
+
|
|
96
|
+
# do import
|
|
97
|
+
rec_info.working_directory = self.working_directory / rec_def.name
|
|
98
|
+
if rec_def.type==RecordingType.EyeTracker:
|
|
99
|
+
rec_info = importing.do_import(rec_info=rec_info, copy_scene_video=copy_video)
|
|
100
|
+
else:
|
|
101
|
+
rec_info = camera_recording.do_import(rec_info=rec_info, copy_video=copy_video)
|
|
102
|
+
|
|
103
|
+
# add recording
|
|
104
|
+
self.add_recording_from_info(which, rec_info)
|
|
105
|
+
return self.recordings[which]
|
|
106
|
+
|
|
107
|
+
def load_existing_recordings(self):
|
|
108
|
+
# load recordings that are present
|
|
109
|
+
for r in self.definition.recordings:
|
|
110
|
+
if (self.working_directory / r.name).is_dir():
|
|
111
|
+
self.add_existing_recording(r.name)
|
|
112
|
+
|
|
113
|
+
def add_existing_recording(self, which: str) -> Recording:
|
|
114
|
+
r_fold = self.working_directory / which
|
|
115
|
+
if not r_fold.is_dir():
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# get info about recording
|
|
119
|
+
rec_def = self.definition.get_recording(which)
|
|
120
|
+
if rec_def.type==RecordingType.EyeTracker:
|
|
121
|
+
rec_info = EyeTrackerRecording.load_from_json(r_fold)
|
|
122
|
+
else:
|
|
123
|
+
rec_info = camera_recording.Recording.load_from_json(r_fold)
|
|
124
|
+
|
|
125
|
+
# add recording
|
|
126
|
+
self.add_recording_from_info(which, rec_info)
|
|
127
|
+
return self.recordings[which]
|
|
128
|
+
|
|
129
|
+
def check_recording_info(self, which: str, rec_info: EyeTrackerRecording|camera_recording.Recording):
|
|
130
|
+
rec_def = self.definition.get_recording(which)
|
|
131
|
+
if rec_def.type==RecordingType.EyeTracker:
|
|
132
|
+
assert isinstance(rec_info,EyeTrackerRecording), f"The provided rec_info is not for an eye tracker recording, but {which} is an eye tracker recording"
|
|
133
|
+
elif rec_def.type==RecordingType.Camera:
|
|
134
|
+
assert isinstance(rec_info,camera_recording.Recording), f"The provided rec_info is not for a camera recording, but {which} is a camera recording"
|
|
135
|
+
|
|
136
|
+
def add_recording_from_info(self, which: str, rec_info: EyeTrackerRecording|camera_recording.Recording):
|
|
137
|
+
rec_def = self.definition.get_recording(which)
|
|
138
|
+
self.check_recording_info(which, rec_info)
|
|
139
|
+
self.recordings[which] = Recording(rec_def, rec_info)
|
|
140
|
+
|
|
141
|
+
def has_all_recordings(self) -> bool:
|
|
142
|
+
return all([r.name in self.recordings for r in self.definition.recordings])
|
|
143
|
+
|
|
144
|
+
def store_as_json(self, path: str | pathlib.Path = None):
|
|
145
|
+
if path is None:
|
|
146
|
+
path = self.working_directory
|
|
147
|
+
path = pathlib.Path(path)
|
|
148
|
+
if path.is_dir():
|
|
149
|
+
path /= self.default_json_file_name
|
|
150
|
+
with open(path, 'w') as f:
|
|
151
|
+
to_dump = {k:getattr(self,k) for k in ['definition']} # only this field. Name will be populated from name of session/provided folder, recordings from each subfolder in the session/provided folder, and working_directory as the provided path
|
|
152
|
+
# dump to file
|
|
153
|
+
json.dump(to_dump, f, cls=utils.CustomTypeEncoder, indent=2)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def load_from_json(path: str | pathlib.Path) -> 'Session':
|
|
157
|
+
path = pathlib.Path(path)
|
|
158
|
+
if path.is_dir():
|
|
159
|
+
path /= Session.default_json_file_name
|
|
160
|
+
# load session setup
|
|
161
|
+
with open(path, 'r') as f:
|
|
162
|
+
sess = Session(**json.load(f, object_hook=utils.json_reconstitute), name=path.parent.name, working_directory=path.parent)
|
|
163
|
+
# load recordings that are present
|
|
164
|
+
sess.load_existing_recordings()
|
|
165
|
+
|
|
166
|
+
return sess
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def from_definition(definition: SessionDefinition, path: str | pathlib.Path) -> 'Session':
|
|
170
|
+
# for loading a recording directory that doesn't contain a session json file
|
|
171
|
+
# use the provided definition instead
|
|
172
|
+
sess = Session(definition, name=path.name, working_directory=path)
|
|
173
|
+
sess.load_existing_recordings()
|
|
174
|
+
return sess
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_sessions_from_directory(path: str|pathlib.Path, session_json_name=Session.default_json_file_name) -> list[Session]:
|
|
178
|
+
path = pathlib.Path(path)
|
|
179
|
+
|
|
180
|
+
# iterate through all folders in the provided path and check if the folder contains a file with the session_json_name
|
|
181
|
+
# then its potentially a session
|
|
182
|
+
sessions: list[Session] = []
|
|
183
|
+
for d in path.iterdir():
|
|
184
|
+
if not d.is_dir():
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
f = d / session_json_name
|
|
188
|
+
if not f.is_file():
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
sessions.append(Session.load_from_json(f))
|
|
192
|
+
|
|
193
|
+
return sessions
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
2
|
+
|
|
3
|
+
__title__ = "gazeMapper"
|
|
4
|
+
__description__ = "Automated world-based analysis of wearable eye tracker data."
|
|
5
|
+
__url__ = "https://github.com/dcnieho/gazeMapper"
|
|
6
|
+
__uri__ = __url__
|
|
7
|
+
__doc__ = __description__ + " <" + __url__ + ">"
|
|
8
|
+
|
|
9
|
+
__author__ = "Diederick Niehorster"
|
|
10
|
+
__email__ = "diederick_c.niehorster@humlab.lu.se"
|
|
11
|
+
|
|
12
|
+
__license__ = "MIT"
|
|
13
|
+
__copyright__ = "Copyright (c) 2024 " + __author__
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: gazeMapper
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Automated world-based analysis of wearable eye tracker data.
|
|
5
|
+
Home-page: https://github.com/dcnieho/gazeMapper
|
|
6
|
+
Author: Diederick Niehorster
|
|
7
|
+
Author-email: diederick_c.niehorster@humlab.lu.se
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2024 Diederick Niehorster
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Source Code, https://github.com/dcnieho/gazeMapper
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Requires-Python: >=3.10
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: glassesTools[GUI]==1.4.0
|
|
39
|
+
Requires-Dist: glassesValidator==2.0.0
|
|
40
|
+
|
|
41
|
+
[](https://pepy.tech/project/gazeMapper)
|
|
42
|
+
[](https://scholar.google.com/citations?view_op=view_citation&citation_for_view=uRUYoVgAAAAJ:uWQEDVKXjbEC)
|
|
43
|
+
[](https://pypi.org/project/gazeMapper/)
|
|
44
|
+
[](https://pypi.org/project/gazeMapper/)
|
|
45
|
+
|
|
46
|
+
# gazeMapper v0.5.0
|
|
47
|
+
Tool for automated world-based analysis of wearable eye tracker data.
|
|
48
|
+
|
|
49
|
+
If you use this tool or any of the code in this repository, please cite:<br>
|
|
50
|
+
Niehorster, D.C., Hessels, R.S., Nyström, M., Benjamins, J.S. and Hooge, I.T.C. (in prep). gazeMapper: A tool for automated world-based analysis of wearable eye tracker data
|
|
51
|
+
|
|
52
|
+
# How to acquire
|
|
53
|
+
GlassesValidator is available from `https://github.com/dcnieho/gazeMapper`, and supports Python 3.10 and 3.11 on Windows, MacOS and Linux.
|
|
54
|
+
|
|
55
|
+
For Windows users who wish to use the gazeMapper GUI, the easiest way to acquire gazeMapper is to [download
|
|
56
|
+
a standalone executable](https://github.com/dcnieho/gazeMapper/releases/latest). The standalone executable is not
|
|
57
|
+
available for MacOS or Linux.
|
|
58
|
+
|
|
59
|
+
For users on Windows, Mac or Linux who wish to use gazeMapper in their Python code, the easiest way to acquire
|
|
60
|
+
gazeMapper is to install it directly into your Python distribution using the command
|
|
61
|
+
`python -m pip install gazeMapper`. Should that fail, this repository is pip-installable as well:
|
|
62
|
+
`python -m pip install git+https://github.com/dcnieho/gazeMapper.git#egg=gazeMapper`. NB: on some platforms you may have
|
|
63
|
+
to replace `python` with `python3` in the above command lines.
|
|
64
|
+
|
|
65
|
+
Once pip-installed in your Python distribution, there are three ways to run the GUI on any of the supported operating systems:
|
|
66
|
+
1. Directly in the terminal of your operating system, type `gazeMapper` and run it.
|
|
67
|
+
2. Open a Python console. From such a console, running the GUI requires only the following two lines of code:
|
|
68
|
+
```python
|
|
69
|
+
import gazeMapper
|
|
70
|
+
gazeMapper.GUI.run()
|
|
71
|
+
```
|
|
72
|
+
3. If you run the gazeMapper's GUI from a script, make sure to wrap your script in `if __name__=="__main__"`. This is required for correct operation from a script because the GUI uses multiprocessing functionality. Do as follows:
|
|
73
|
+
```python
|
|
74
|
+
if __name__=="__main__":
|
|
75
|
+
import gazeMapper
|
|
76
|
+
gazeMapper.GUI.run()
|
|
77
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
requirements.txt
|
|
6
|
+
setup.py
|
|
7
|
+
src/gazeMapper/__init__.py
|
|
8
|
+
src/gazeMapper/camera_recording.py
|
|
9
|
+
src/gazeMapper/config.py
|
|
10
|
+
src/gazeMapper/episode.py
|
|
11
|
+
src/gazeMapper/plane.py
|
|
12
|
+
src/gazeMapper/session.py
|
|
13
|
+
src/gazeMapper/version.py
|
|
14
|
+
src/gazeMapper.egg-info/PKG-INFO
|
|
15
|
+
src/gazeMapper.egg-info/SOURCES.txt
|
|
16
|
+
src/gazeMapper.egg-info/dependency_links.txt
|
|
17
|
+
src/gazeMapper.egg-info/requires.txt
|
|
18
|
+
src/gazeMapper.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gazeMapper
|