creapy 0.1.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.
Files changed (96) hide show
  1. creapy-0.1.0/LICENSE +21 -0
  2. creapy-0.1.0/PKG-INFO +199 -0
  3. creapy-0.1.0/README.md +164 -0
  4. creapy-0.1.0/creapy/__init__.py +67 -0
  5. creapy-0.1.0/creapy/aoi/__init__.py +0 -0
  6. creapy-0.1.0/creapy/aoi/aoi.py +142 -0
  7. creapy-0.1.0/creapy/aoi/sequence.py +174 -0
  8. creapy-0.1.0/creapy/examples/README.md +7 -0
  9. creapy-0.1.0/creapy/examples/__init__.py +127 -0
  10. creapy-0.1.0/creapy/examples/demo_fixations.csv +2151 -0
  11. creapy-0.1.0/creapy/examples/demo_picture_A.png +0 -0
  12. creapy-0.1.0/creapy/examples/demo_picture_B.png +0 -0
  13. creapy-0.1.0/creapy/formats/__init__.py +5 -0
  14. creapy-0.1.0/creapy/formats/detect.py +127 -0
  15. creapy-0.1.0/creapy/formats/table.py +84 -0
  16. creapy-0.1.0/creapy/formats/tobii.py +339 -0
  17. creapy-0.1.0/creapy/gaze/__init__.py +0 -0
  18. creapy-0.1.0/creapy/gaze/base.py +160 -0
  19. creapy-0.1.0/creapy/gaze/convert.py +82 -0
  20. creapy-0.1.0/creapy/gaze/fixation.py +289 -0
  21. creapy-0.1.0/creapy/gaze/saccade.py +311 -0
  22. creapy-0.1.0/creapy/io/__init__.py +0 -0
  23. creapy-0.1.0/creapy/io/__main__.py +4 -0
  24. creapy-0.1.0/creapy/io/aoi_metrics_window.py +155 -0
  25. creapy-0.1.0/creapy/io/glossary.py +419 -0
  26. creapy-0.1.0/creapy/io/help.py +201 -0
  27. creapy-0.1.0/creapy/io/main.py +1865 -0
  28. creapy-0.1.0/creapy/io/project.py +48 -0
  29. creapy-0.1.0/creapy/io/session_log.py +73 -0
  30. creapy-0.1.0/creapy/io/style/crea_logo.png +0 -0
  31. creapy-0.1.0/creapy/io/style/fix_thumbnail.png +0 -0
  32. creapy-0.1.0/creapy/io/style/main_dark.qss +336 -0
  33. creapy-0.1.0/creapy/io/style/main_light.qss +207 -0
  34. creapy-0.1.0/creapy/io/style/sac_thumbnail.png +0 -0
  35. creapy-0.1.0/creapy/io/style/start_dark.qss +139 -0
  36. creapy-0.1.0/creapy/io/style/start_light.qss +93 -0
  37. creapy-0.1.0/creapy/io/views.py +418 -0
  38. creapy-0.1.0/creapy/io/widgets/__init__.py +0 -0
  39. creapy-0.1.0/creapy/io/widgets/column_mapping.py +113 -0
  40. creapy-0.1.0/creapy/io/widgets/compare_dialog.py +81 -0
  41. creapy-0.1.0/creapy/io/widgets/filter_bar.py +157 -0
  42. creapy-0.1.0/creapy/io/widgets/grouping_dialog.py +88 -0
  43. creapy-0.1.0/creapy/io/widgets/half_dial.py +220 -0
  44. creapy-0.1.0/creapy/io/widgets/painter.py +58 -0
  45. creapy-0.1.0/creapy/io/widgets/stimulus_strip.py +82 -0
  46. creapy-0.1.0/creapy/io/widgets/stimulus_thumbnail.py +76 -0
  47. creapy-0.1.0/creapy/io/widgets/table_view.py +47 -0
  48. creapy-0.1.0/creapy/io/widgets/toolbars/__init__.py +0 -0
  49. creapy-0.1.0/creapy/io/widgets/toolbars/aoi.py +170 -0
  50. creapy-0.1.0/creapy/io/widgets/toolbars/fix_heatmap.py +227 -0
  51. creapy-0.1.0/creapy/io/widgets/toolbars/fix_scanpath.py +114 -0
  52. creapy-0.1.0/creapy/io/widgets/toolbars/recurrence.py +83 -0
  53. creapy-0.1.0/creapy/io/widgets/toolbars/rose.py +100 -0
  54. creapy-0.1.0/creapy/io/widgets/toolbars/sac_heatmap.py +183 -0
  55. creapy-0.1.0/creapy/io/widgets/toolbars/scanpath.py +153 -0
  56. creapy-0.1.0/creapy/io/widgets/toolbars/scanpath_compare.py +162 -0
  57. creapy-0.1.0/creapy/io/widgets/toolbars/similarity.py +175 -0
  58. creapy-0.1.0/creapy/io/widgets/toolbars/stats.py +90 -0
  59. creapy-0.1.0/creapy/io/widgets/toolbars/time_series.py +117 -0
  60. creapy-0.1.0/creapy/io/widgets/utils.py +103 -0
  61. creapy-0.1.0/creapy/io/worker.py +96 -0
  62. creapy-0.1.0/creapy/misc/__init__.py +0 -0
  63. creapy-0.1.0/creapy/misc/colormapping.py +13 -0
  64. creapy-0.1.0/creapy/misc/contours.py +41 -0
  65. creapy-0.1.0/creapy/misc/plot_manager.py +73 -0
  66. creapy-0.1.0/creapy/misc/repulsion.py +106 -0
  67. creapy-0.1.0/creapy/misc/utils.py +66 -0
  68. creapy-0.1.0/creapy/similarity/__init__.py +0 -0
  69. creapy-0.1.0/creapy/similarity/grid.py +35 -0
  70. creapy-0.1.0/creapy/similarity/grid_fixation.py +66 -0
  71. creapy-0.1.0/creapy/similarity/grid_result.py +227 -0
  72. creapy-0.1.0/creapy/similarity/heatmatch_result.py +269 -0
  73. creapy-0.1.0/creapy/similarity/scanpath.py +189 -0
  74. creapy-0.1.0/creapy/stats/__init__.py +8 -0
  75. creapy-0.1.0/creapy/stats/descriptives.py +107 -0
  76. creapy-0.1.0/creapy/stats/recurrence.py +111 -0
  77. creapy-0.1.0/creapy/stats/spectrum.py +164 -0
  78. creapy-0.1.0/creapy/stats/timeseries.py +104 -0
  79. creapy-0.1.0/creapy.egg-info/PKG-INFO +199 -0
  80. creapy-0.1.0/creapy.egg-info/SOURCES.txt +94 -0
  81. creapy-0.1.0/creapy.egg-info/dependency_links.txt +1 -0
  82. creapy-0.1.0/creapy.egg-info/entry_points.txt +2 -0
  83. creapy-0.1.0/creapy.egg-info/requires.txt +17 -0
  84. creapy-0.1.0/creapy.egg-info/top_level.txt +1 -0
  85. creapy-0.1.0/pyproject.toml +55 -0
  86. creapy-0.1.0/setup.cfg +4 -0
  87. creapy-0.1.0/tests/test_datasets.py +147 -0
  88. creapy-0.1.0/tests/test_explanations.py +88 -0
  89. creapy-0.1.0/tests/test_formats.py +122 -0
  90. creapy-0.1.0/tests/test_gui.py +183 -0
  91. creapy-0.1.0/tests/test_library_analyses.py +149 -0
  92. creapy-0.1.0/tests/test_library_api.py +127 -0
  93. creapy-0.1.0/tests/test_prolab_real.py +36 -0
  94. creapy-0.1.0/tests/test_recurrence.py +110 -0
  95. creapy-0.1.0/tests/test_scanpath.py +107 -0
  96. creapy-0.1.0/tests/test_spectrum.py +111 -0
creapy-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Xingyu Long
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.
creapy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: creapy
3
+ Version: 0.1.0
4
+ Summary: Eye-tracking visual analytics for art history and empirical aesthetics (CReA Lab, University of Vienna): library + creapy.io desktop app
5
+ Author-email: Xingyu Long <xingyu.long@univie.ac.at>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://crea.univie.ac.at
8
+ Project-URL: Repository, https://github.com/xysLong/creapy
9
+ Project-URL: Issues, https://github.com/xysLong/creapy/issues
10
+ Project-URL: Changelog, https://github.com/xysLong/creapy/blob/main/CHANGELOG.md
11
+ Keywords: eye-tracking,heatmap,scanpath,saccades,fixations,art-history,empirical-aesthetics,EyeLink,Tobii
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering :: Visualization
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy>=1.26
20
+ Requires-Dist: pandas>=2.2
21
+ Requires-Dist: scipy>=1.13
22
+ Requires-Dist: matplotlib>=3.9
23
+ Requires-Dist: shapely>=2.0
24
+ Requires-Dist: scikit-learn>=1.5
25
+ Requires-Dist: tqdm>=4.66
26
+ Requires-Dist: openpyxl>=3.1
27
+ Requires-Dist: Pillow>=10
28
+ Requires-Dist: heatmatch>=1.0.0
29
+ Provides-Extra: gui
30
+ Requires-Dist: PySide6>=6.7; extra == "gui"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8; extra == "dev"
33
+ Requires-Dist: PySide6>=6.7; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # creapy
37
+
38
+ **creapy** (pronounced /ˈkɹiə.paɪ/) is eye-tracking visual analytics for art history and empirical aesthetics, developed at the
39
+ **CReA Lab** (Lab for Cognitive Research in Art History, Department of Art History, University of Vienna).
40
+
41
+ Two layers for two audiences (the division announced at DHd 2026: maximal ease of use in the app, analytical
42
+ depth and flexibility in the library):
43
+
44
+ - **`creapy`**: a pip-installable, Qt-free Python library for researchers who know some Python: load
45
+ fixation/saccade tables, build per-stimulus datasets, draw heatmaps, scanpaths and orientation-aware saccade
46
+ heatmaps (via [HeatMatch](https://github.com/xysLong/HeatMatch)), AOI metrics and sequences, descriptives,
47
+ coefficient K, time series and spectra, recurrence quantification, HeatMatch / MultiMatch / ScanMatch similarity
48
+ with permutation tests. Every plot takes a matplotlib `ax`, every table is a DataFrame. Documented in
49
+ [`docs/LIBRARY.md`](https://github.com/xysLong/creapy/blob/main/docs/LIBRARY.md) and `tutorial.ipynb`.
50
+ - **`creapy.io`**: a desktop app (Qt / PySide6) for art historians and other researchers who do not program:
51
+ import a table, click through the views, filter by participant or time window, export figures and tables.
52
+ Everything the app shows is a library call; the app adds only windows, sliders, projects and the session log.
53
+
54
+ Status: prototype in active development, in use at the CReA Lab; the API may still change before 1.0.
55
+ Source: <https://github.com/xysLong/creapy> (MIT).
56
+ Presented as a poster at DHd 2026 (doi:10.5281/zenodo.18703017).
57
+
58
+ ## Installation
59
+
60
+ **Non-programmers: do not install Python.** Download the double-click app
61
+ (`creapy.io-<version>-macOS.zip`, `-macOS-intel.zip` or `-Windows.zip`) and follow
62
+ [`docs/INSTALL_APP.md`](https://github.com/xysLong/creapy/blob/main/docs/INSTALL_APP.md): two minutes, one security dialog on first start.
63
+ Building those bundles: [`packaging/README.md`](https://github.com/xysLong/creapy/blob/main/packaging/README.md).
64
+
65
+ **Developers:** Python 3.10 or newer. In a terminal (macOS: Terminal; Windows: PowerShell):
66
+
67
+ ```bash
68
+ python -m venv creapy-env
69
+ # macOS / Linux:
70
+ source creapy-env/bin/activate
71
+ # Windows:
72
+ creapy-env\Scripts\activate
73
+
74
+ pip install "creapy[gui]" # library + desktop app
75
+ pip install creapy # library only, no Qt
76
+
77
+ # from a checkout of the repository instead:
78
+ pip install -e ".[gui]"
79
+ ```
80
+
81
+ Start the app:
82
+
83
+ ```bash
84
+ creapy-io # or: python -m creapy.io
85
+ ```
86
+
87
+ The first screen offers an **example dataset** (synthetic, twelve invented viewers on two generated pictures)
88
+ so every view can be tried before any real data is at hand.
89
+
90
+ Conda users: `conda create -n creapy python=3.11 && conda activate creapy && pip install -e ".[gui]"`.
91
+
92
+ ## Data formats
93
+
94
+ | Source | What to export | Notes |
95
+ |---|---|---|
96
+ | **EyeLink DataViewer** | *Fixation report* or *Saccade report*, CSV/Excel, one row per event | needs a column with the stimulus image name; `TRIAL_INDEX` and `CURRENT_FIX_PUPIL` are picked up; `.` missing values are handled |
97
+ | **Tobii Pro Lab** (screen or glasses) | *Event-based metrics export* (TSV, one row per fixation; the file the lab shares) or the *Data export* (TSV, one row per gaze sample; files above 1.5 GB are read in chunks) | each snapshot / medium becomes one stimulus; pupil and viewing distance are carried along. The *AOI-based* metrics export opens as a table window with charts per AOI |
98
+ | **Anything else** | one row per fixation: participant, stimulus, start time, duration, x, y | CSV with `,` `;` or tab, decimal commas fine; map the columns in the import dialog |
99
+
100
+ Coordinates are pixels of the image you provide: use the images at the resolution shown (or the Pro Lab
101
+ snapshot files). Times may be in s, ms or µs; durations in ms or s; the units are detected.
102
+
103
+ ## The views
104
+
105
+ ![Fixation heatmap compared by group](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/compare_groups.png)
106
+
107
+ Fixation Heatmap (optional shared colour scale) · Fixation Scanpath (numbered, per participant) · Saccade
108
+ Scanpath · Saccade Heatmap (HeatMatch, orientation of interest) · Saccade Directions (rose diagram) · Areas of
109
+ Interest (draw rectangles / polygons; dwell time, fixations, visits, time to first fixation; scarf plot;
110
+ transition matrix; first-fixation order; save/load as JSON) · Time Series (fixation duration, saccade amplitude,
111
+ coefficient K, pupil size, viewing distance, share of fixations in AOIs; per group; periodogram and scalogram) ·
112
+ Saccade Similarity (HeatMatch: matrix, group boxplot with permutation test, viewer map, similarity over time) ·
113
+ Scanpath Comparison (MultiMatch, ScanMatch, cross-recurrence; same displays) · Recurrence (recurrence plot, RQA
114
+ table, boxplot by group) · Statistics (per-participant table, group comparison, AOI metrics, boxplots; export
115
+ CSV/Excel).
116
+
117
+ | | | |
118
+ |---|---|---|
119
+ | ![](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/scarf_plot.png) | ![](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/scanpath_comparison_matrix.png) | ![](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/periodogram.png) |
120
+ | scarf plot | MultiMatch matrix with groups | periodogram |
121
+
122
+ The **Show** bar above the canvas restricts every view to a participant, a group or a time window (e.g. the first
123
+ 3 s). Groups are assigned under *Analysis*, optionally prefilled from a column of the table. **Compare Side by
124
+ Side** draws the current view once per group, stimulus or time window; **Export This View for All Stimuli**
125
+ writes it once per stimulus into a folder. **Projects** remember table, images, mapping, groups and AOIs;
126
+ **Undo** covers AOI edits and group assignments. Long computations run in a worker thread with a Cancel button.
127
+ Help → *The Analyses Explained* describes every measure in plain words; `docs/QUICKSTART.pdf` is the two-page
128
+ hand-out. (Screenshots are offscreen renders of the synthetic example dataset.)
129
+
130
+ **Session log (opt-in).** *Logging > Record session log* writes the views, parameter values and filters you use
131
+ into a text file on your own computer (JSON lines). Nothing is sent anywhere. It exists for usability evaluations;
132
+ it is off by default.
133
+
134
+ ## Library quickstart
135
+
136
+ Full guide with a view-by-view map of app features to library calls and the API reference:
137
+ [`docs/LIBRARY.md`](https://github.com/xysLong/creapy/blob/main/docs/LIBRARY.md). `help(creapy.FixationData)` (or `fix.make_heatmap?` in Jupyter) shows the same
138
+ texts.
139
+
140
+ ```python
141
+ import creapy
142
+
143
+ df, mapping = creapy.load_fixations("fixation_report.csv") # EyeLink, Tobii Data export or any table
144
+ datasets = creapy.datasets_by_stimulus(df, mapping, "stimuli/") # {stimulus -> (FixationData, SaccadeData)}
145
+ fix, sac = next(iter(datasets.values()))
146
+ grouping = {p: "expert" if p.startswith("E") else "novice" for p in fix.subjects}
147
+
148
+ import matplotlib.pyplot as plt
149
+ fig, ax = plt.subplots()
150
+ fix.make_heatmap(ax, bw_method=0.1, cmap="coolwarm") # duration-weighted KDE, participants equal
151
+ fix.subset(t_range=(0, 3)).visualize_scanpath(ax, subjects=[fix.subjects[0]])
152
+
153
+ fix.set_top_down_aoi("face", creapy.AOI.rectangle("face", 600, 100, 800, 300).geom)
154
+ print(fix.aoi_metrics()) # participant x AOI (dwell, visits, latency)
155
+ creapy.show_scarf_plot(ax, fix); creapy.transition_matrix(fix) # AOI sequences
156
+ df_stats = creapy.participant_table(fix, sac) # descriptives + coefficient K
157
+ creapy.group_comparison(df_stats, grouping) # Welch t / Mann-Whitney / ANOVA per measure
158
+ creapy.show_group_boxplot(df_stats, "coefficient_K", grouping) # the Statistics view's boxplot
159
+ creapy.show_measure_curve(fix, sac, "fixation_duration", sigma=2) # the Time Series view's curve
160
+ result = sac.compute_heatmatch_similarity(sigma=50) # pairwise HeatMatch scores
161
+ result.matshow(grouping=grouping); result.permutation_test(grouping)
162
+ mm = fix.compute_scanpath_similarity("multimatch") # MultiMatch / ScanMatch / cross-recurrence
163
+ mm.show_boxplot(grouping); mm.score_overview(grouping)
164
+ creapy.participant_rqa(fix) # recurrence quantification
165
+ creapy.participant_periodogram(fix) # Lomb-Scargle rhythm per participant
166
+ ```
167
+
168
+ `tutorial.ipynb` walks through all of this on the example dataset; `demo_similarity.ipynb` covers the similarity
169
+ workflow in depth.
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ pip install -e ".[dev]"
175
+ pytest # formats, conversions, datasets, offscreen GUI
176
+ ```
177
+
178
+ Layout: `creapy/formats/` (table reading, format detection, Tobii loaders), `creapy/gaze/` (datasets,
179
+ conversions), `creapy/aoi/` (AOI geometry, sequences), `creapy/stats/` (descriptives, time series, recurrence, spectrum),
180
+ `creapy/similarity/` (HeatMatch results, MultiMatch/ScanMatch), `creapy/misc/` (contours, utils), `creapy/io/`
181
+ (the Qt app: `main.py` window, `views.py` prepare/draw functions, `worker.py`, `project.py`, `widgets/`,
182
+ `style/`), `creapy/examples/` (synthetic example data), `tests/`, `docs/`, `packaging/`.
183
+
184
+ Saccade orientation fields and similarity are computed by the `heatmatch` package and are not reimplemented here.
185
+ `docs/LIBRARY.md` is generated from the docstrings by `python docs/make_library_reference.py`; a test fails when
186
+ a public library function lacks a docstring or when the GUI gains logic the library does not have.
187
+
188
+ ## Citation
189
+
190
+ Long, X. & Rosenberg, R. (2026). Perspektiven für ein anfängerfreundliches Tool zur Eye-Tracking-Datenvisualisierung
191
+ in der kognitiven Kunstgeschichte. *Book of Abstracts – DHd 2026*, 606–607. https://doi.org/10.5281/zenodo.18703017
192
+
193
+ Long, X., Arató, J., Kury, S., Miscenà, A. & Rosenberg, R. (2026). HeatMatch: Orientation-Aware Visualization and
194
+ Comparison of Very Dense Saccade Patterns. *Proc. ACM Comput. Graph. Interact. Tech.* 9(2), Art. 18.
195
+ https://doi.org/10.1145/3803539
196
+
197
+ ## License
198
+
199
+ MIT (see `LICENSE`). Contact: Xingyu Long, xingyu.long@univie.ac.at.
creapy-0.1.0/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # creapy
2
+
3
+ **creapy** (pronounced /ˈkɹiə.paɪ/) is eye-tracking visual analytics for art history and empirical aesthetics, developed at the
4
+ **CReA Lab** (Lab for Cognitive Research in Art History, Department of Art History, University of Vienna).
5
+
6
+ Two layers for two audiences (the division announced at DHd 2026: maximal ease of use in the app, analytical
7
+ depth and flexibility in the library):
8
+
9
+ - **`creapy`**: a pip-installable, Qt-free Python library for researchers who know some Python: load
10
+ fixation/saccade tables, build per-stimulus datasets, draw heatmaps, scanpaths and orientation-aware saccade
11
+ heatmaps (via [HeatMatch](https://github.com/xysLong/HeatMatch)), AOI metrics and sequences, descriptives,
12
+ coefficient K, time series and spectra, recurrence quantification, HeatMatch / MultiMatch / ScanMatch similarity
13
+ with permutation tests. Every plot takes a matplotlib `ax`, every table is a DataFrame. Documented in
14
+ [`docs/LIBRARY.md`](https://github.com/xysLong/creapy/blob/main/docs/LIBRARY.md) and `tutorial.ipynb`.
15
+ - **`creapy.io`**: a desktop app (Qt / PySide6) for art historians and other researchers who do not program:
16
+ import a table, click through the views, filter by participant or time window, export figures and tables.
17
+ Everything the app shows is a library call; the app adds only windows, sliders, projects and the session log.
18
+
19
+ Status: prototype in active development, in use at the CReA Lab; the API may still change before 1.0.
20
+ Source: <https://github.com/xysLong/creapy> (MIT).
21
+ Presented as a poster at DHd 2026 (doi:10.5281/zenodo.18703017).
22
+
23
+ ## Installation
24
+
25
+ **Non-programmers: do not install Python.** Download the double-click app
26
+ (`creapy.io-<version>-macOS.zip`, `-macOS-intel.zip` or `-Windows.zip`) and follow
27
+ [`docs/INSTALL_APP.md`](https://github.com/xysLong/creapy/blob/main/docs/INSTALL_APP.md): two minutes, one security dialog on first start.
28
+ Building those bundles: [`packaging/README.md`](https://github.com/xysLong/creapy/blob/main/packaging/README.md).
29
+
30
+ **Developers:** Python 3.10 or newer. In a terminal (macOS: Terminal; Windows: PowerShell):
31
+
32
+ ```bash
33
+ python -m venv creapy-env
34
+ # macOS / Linux:
35
+ source creapy-env/bin/activate
36
+ # Windows:
37
+ creapy-env\Scripts\activate
38
+
39
+ pip install "creapy[gui]" # library + desktop app
40
+ pip install creapy # library only, no Qt
41
+
42
+ # from a checkout of the repository instead:
43
+ pip install -e ".[gui]"
44
+ ```
45
+
46
+ Start the app:
47
+
48
+ ```bash
49
+ creapy-io # or: python -m creapy.io
50
+ ```
51
+
52
+ The first screen offers an **example dataset** (synthetic, twelve invented viewers on two generated pictures)
53
+ so every view can be tried before any real data is at hand.
54
+
55
+ Conda users: `conda create -n creapy python=3.11 && conda activate creapy && pip install -e ".[gui]"`.
56
+
57
+ ## Data formats
58
+
59
+ | Source | What to export | Notes |
60
+ |---|---|---|
61
+ | **EyeLink DataViewer** | *Fixation report* or *Saccade report*, CSV/Excel, one row per event | needs a column with the stimulus image name; `TRIAL_INDEX` and `CURRENT_FIX_PUPIL` are picked up; `.` missing values are handled |
62
+ | **Tobii Pro Lab** (screen or glasses) | *Event-based metrics export* (TSV, one row per fixation; the file the lab shares) or the *Data export* (TSV, one row per gaze sample; files above 1.5 GB are read in chunks) | each snapshot / medium becomes one stimulus; pupil and viewing distance are carried along. The *AOI-based* metrics export opens as a table window with charts per AOI |
63
+ | **Anything else** | one row per fixation: participant, stimulus, start time, duration, x, y | CSV with `,` `;` or tab, decimal commas fine; map the columns in the import dialog |
64
+
65
+ Coordinates are pixels of the image you provide: use the images at the resolution shown (or the Pro Lab
66
+ snapshot files). Times may be in s, ms or µs; durations in ms or s; the units are detected.
67
+
68
+ ## The views
69
+
70
+ ![Fixation heatmap compared by group](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/compare_groups.png)
71
+
72
+ Fixation Heatmap (optional shared colour scale) · Fixation Scanpath (numbered, per participant) · Saccade
73
+ Scanpath · Saccade Heatmap (HeatMatch, orientation of interest) · Saccade Directions (rose diagram) · Areas of
74
+ Interest (draw rectangles / polygons; dwell time, fixations, visits, time to first fixation; scarf plot;
75
+ transition matrix; first-fixation order; save/load as JSON) · Time Series (fixation duration, saccade amplitude,
76
+ coefficient K, pupil size, viewing distance, share of fixations in AOIs; per group; periodogram and scalogram) ·
77
+ Saccade Similarity (HeatMatch: matrix, group boxplot with permutation test, viewer map, similarity over time) ·
78
+ Scanpath Comparison (MultiMatch, ScanMatch, cross-recurrence; same displays) · Recurrence (recurrence plot, RQA
79
+ table, boxplot by group) · Statistics (per-participant table, group comparison, AOI metrics, boxplots; export
80
+ CSV/Excel).
81
+
82
+ | | | |
83
+ |---|---|---|
84
+ | ![](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/scarf_plot.png) | ![](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/scanpath_comparison_matrix.png) | ![](https://raw.githubusercontent.com/xysLong/creapy/main/docs/screenshots/periodogram.png) |
85
+ | scarf plot | MultiMatch matrix with groups | periodogram |
86
+
87
+ The **Show** bar above the canvas restricts every view to a participant, a group or a time window (e.g. the first
88
+ 3 s). Groups are assigned under *Analysis*, optionally prefilled from a column of the table. **Compare Side by
89
+ Side** draws the current view once per group, stimulus or time window; **Export This View for All Stimuli**
90
+ writes it once per stimulus into a folder. **Projects** remember table, images, mapping, groups and AOIs;
91
+ **Undo** covers AOI edits and group assignments. Long computations run in a worker thread with a Cancel button.
92
+ Help → *The Analyses Explained* describes every measure in plain words; `docs/QUICKSTART.pdf` is the two-page
93
+ hand-out. (Screenshots are offscreen renders of the synthetic example dataset.)
94
+
95
+ **Session log (opt-in).** *Logging > Record session log* writes the views, parameter values and filters you use
96
+ into a text file on your own computer (JSON lines). Nothing is sent anywhere. It exists for usability evaluations;
97
+ it is off by default.
98
+
99
+ ## Library quickstart
100
+
101
+ Full guide with a view-by-view map of app features to library calls and the API reference:
102
+ [`docs/LIBRARY.md`](https://github.com/xysLong/creapy/blob/main/docs/LIBRARY.md). `help(creapy.FixationData)` (or `fix.make_heatmap?` in Jupyter) shows the same
103
+ texts.
104
+
105
+ ```python
106
+ import creapy
107
+
108
+ df, mapping = creapy.load_fixations("fixation_report.csv") # EyeLink, Tobii Data export or any table
109
+ datasets = creapy.datasets_by_stimulus(df, mapping, "stimuli/") # {stimulus -> (FixationData, SaccadeData)}
110
+ fix, sac = next(iter(datasets.values()))
111
+ grouping = {p: "expert" if p.startswith("E") else "novice" for p in fix.subjects}
112
+
113
+ import matplotlib.pyplot as plt
114
+ fig, ax = plt.subplots()
115
+ fix.make_heatmap(ax, bw_method=0.1, cmap="coolwarm") # duration-weighted KDE, participants equal
116
+ fix.subset(t_range=(0, 3)).visualize_scanpath(ax, subjects=[fix.subjects[0]])
117
+
118
+ fix.set_top_down_aoi("face", creapy.AOI.rectangle("face", 600, 100, 800, 300).geom)
119
+ print(fix.aoi_metrics()) # participant x AOI (dwell, visits, latency)
120
+ creapy.show_scarf_plot(ax, fix); creapy.transition_matrix(fix) # AOI sequences
121
+ df_stats = creapy.participant_table(fix, sac) # descriptives + coefficient K
122
+ creapy.group_comparison(df_stats, grouping) # Welch t / Mann-Whitney / ANOVA per measure
123
+ creapy.show_group_boxplot(df_stats, "coefficient_K", grouping) # the Statistics view's boxplot
124
+ creapy.show_measure_curve(fix, sac, "fixation_duration", sigma=2) # the Time Series view's curve
125
+ result = sac.compute_heatmatch_similarity(sigma=50) # pairwise HeatMatch scores
126
+ result.matshow(grouping=grouping); result.permutation_test(grouping)
127
+ mm = fix.compute_scanpath_similarity("multimatch") # MultiMatch / ScanMatch / cross-recurrence
128
+ mm.show_boxplot(grouping); mm.score_overview(grouping)
129
+ creapy.participant_rqa(fix) # recurrence quantification
130
+ creapy.participant_periodogram(fix) # Lomb-Scargle rhythm per participant
131
+ ```
132
+
133
+ `tutorial.ipynb` walks through all of this on the example dataset; `demo_similarity.ipynb` covers the similarity
134
+ workflow in depth.
135
+
136
+ ## Development
137
+
138
+ ```bash
139
+ pip install -e ".[dev]"
140
+ pytest # formats, conversions, datasets, offscreen GUI
141
+ ```
142
+
143
+ Layout: `creapy/formats/` (table reading, format detection, Tobii loaders), `creapy/gaze/` (datasets,
144
+ conversions), `creapy/aoi/` (AOI geometry, sequences), `creapy/stats/` (descriptives, time series, recurrence, spectrum),
145
+ `creapy/similarity/` (HeatMatch results, MultiMatch/ScanMatch), `creapy/misc/` (contours, utils), `creapy/io/`
146
+ (the Qt app: `main.py` window, `views.py` prepare/draw functions, `worker.py`, `project.py`, `widgets/`,
147
+ `style/`), `creapy/examples/` (synthetic example data), `tests/`, `docs/`, `packaging/`.
148
+
149
+ Saccade orientation fields and similarity are computed by the `heatmatch` package and are not reimplemented here.
150
+ `docs/LIBRARY.md` is generated from the docstrings by `python docs/make_library_reference.py`; a test fails when
151
+ a public library function lacks a docstring or when the GUI gains logic the library does not have.
152
+
153
+ ## Citation
154
+
155
+ Long, X. & Rosenberg, R. (2026). Perspektiven für ein anfängerfreundliches Tool zur Eye-Tracking-Datenvisualisierung
156
+ in der kognitiven Kunstgeschichte. *Book of Abstracts – DHd 2026*, 606–607. https://doi.org/10.5281/zenodo.18703017
157
+
158
+ Long, X., Arató, J., Kury, S., Miscenà, A. & Rosenberg, R. (2026). HeatMatch: Orientation-Aware Visualization and
159
+ Comparison of Very Dense Saccade Patterns. *Proc. ACM Comput. Graph. Interact. Tech.* 9(2), Art. 18.
160
+ https://doi.org/10.1145/3803539
161
+
162
+ ## License
163
+
164
+ MIT (see `LICENSE`). Contact: Xingyu Long, xingyu.long@univie.ac.at.
@@ -0,0 +1,67 @@
1
+ from creapy.formats import read_table, detect_format, guess_mapping, FIXATION_FIELDS, SACCADE_FIELDS
2
+ from creapy.formats import load_prolab_export, load_prolab_data_export, load_prolab_event_export
3
+ from creapy.formats import load_prolab_data_export_chunked, load_prolab_aoi_metrics_export, aoi_metrics_summary
4
+ from creapy.gaze.base import StimulusGaze
5
+ from creapy.gaze.fixation import FixationData
6
+ from creapy.gaze.saccade import SaccadeData
7
+ from creapy.gaze.convert import fixations_to_saccades, saccades_to_fixations
8
+ from creapy.aoi.aoi import AOI
9
+ from creapy.aoi.sequence import aoi_sequence, transition_matrix, show_scarf_plot, show_transition_matrix, first_fixation_order
10
+ from creapy.stats import coefficient_k, fixation_k, participant_table, group_comparison, show_group_boxplot
11
+ from creapy.stats import value_series, show_measure_curve, split_by_group
12
+ from creapy.stats import participant_rqa, cross_recurrence_table, show_recurrence_plot
13
+ from creapy.stats import participant_periodogram, dominant_period, show_periodogram, morlet_scalogram, show_scalogram
14
+ from creapy.similarity.heatmatch_result import PairwiseSimilarityResult, HeatMatchSimilarityResult
15
+ from creapy.misc.utils import natural_sorted, find_images
16
+ import os
17
+
18
+ __version__ = "0.1.0"
19
+
20
+
21
+ def load_fixations(path, mapping=None, **mapping_kwargs):
22
+ """Fixation table (CSV/TSV/Excel with one row per fixation, or a Tobii Pro Lab Data export) -> (df, mapping)."""
23
+ df = read_table(path)
24
+ if detect_format(df.columns) in ('tobii_data_export', 'tobii_event_export'):
25
+ df, guessed = load_prolab_export(df)
26
+ else:
27
+ guessed = guess_mapping(df, FIXATION_FIELDS)
28
+ guessed.update(mapping or {})
29
+ guessed.update(mapping_kwargs)
30
+ return df, guessed
31
+
32
+
33
+ def load_saccades(path, mapping=None, **mapping_kwargs):
34
+ """Saccade table (one row per saccade) -> (df, mapping) with the sac_* keys of SACCADE_FIELDS."""
35
+ df = read_table(path)
36
+ guessed = guess_mapping(df, SACCADE_FIELDS)
37
+ guessed.update(mapping or {})
38
+ guessed.update(mapping_kwargs)
39
+ return df, guessed
40
+
41
+
42
+ def datasets_by_stimulus(df, mapping, stimulus_paths=None, is_fixation=True):
43
+ """Split a table into per-stimulus (FixationData, SaccadeData) pairs: {stimulus -> (fix, sac)}."""
44
+ if isinstance(stimulus_paths, (str, os.PathLike)):
45
+ found = find_images(str(stimulus_paths))
46
+ stimulus_paths = {}
47
+ for stim in df[mapping['stim_col']].unique():
48
+ key = str(stim).lower()
49
+ stimulus_paths[stim] = found.get(key, found.get(os.path.splitext(key)[0], str(stim)))
50
+ datasets = {}
51
+ for stim, df_stim in df.groupby(mapping['stim_col']):
52
+ path = stimulus_paths[stim] if stimulus_paths is not None else str(stim)
53
+ datasets[stim] = build_pair(df_stim, path, mapping, is_fixation)
54
+ return datasets
55
+
56
+
57
+ def build_pair(df_stim, path, mapping, is_fixation=True):
58
+ """(FixationData, SaccadeData) of one stimulus from its rows and image path."""
59
+ if is_fixation:
60
+ fix = FixationData(df_stim, path, **mapping)
61
+ df_sac, sac_mapping = fixations_to_saccades(fix.df, fix.mapping)
62
+ sac = SaccadeData(df_sac, path, img=fix.img, scale=1., time_unit=fix.time_unit, **sac_mapping)
63
+ else:
64
+ sac = SaccadeData(df_stim, path, **mapping)
65
+ df_fix, fix_mapping = saccades_to_fixations(sac.df, sac.mapping)
66
+ fix = FixationData(df_fix, path, img=sac.img, scale=1., time_unit=sac.time_unit, **fix_mapping)
67
+ return fix, sac
File without changes
@@ -0,0 +1,142 @@
1
+ import numpy as np
2
+ import functools
3
+ import matplotlib.patches as patches
4
+ import matplotlib.patheffects as pe
5
+ import matplotlib.path as mpath
6
+ from shapely import area, centroid, covers
7
+ from shapely.ops import unary_union
8
+ from shapely.geometry import Point, Polygon, LinearRing, box
9
+
10
+
11
+ class AOI:
12
+ """One area of interest: a named shapely geometry in image pixels (x right, y down)."""
13
+
14
+ def __init__(self, name="Unnamed", geom=None, paths=None):
15
+ assert (geom is None) != (paths is None), "EITHER 'geom' OR 'paths' must be provided, but not both"
16
+ self.__name = name
17
+ self.__geom = geom if paths is None else AOI.__multiple_paths_to_single_geom(paths)
18
+ self.__path = AOI.__single_geom_to_single_path(self.__geom)
19
+ self.__disconnected_polygons = AOI.__extract_disconnected_polygons(self.__geom)
20
+
21
+ @property
22
+ def name(self):
23
+ """The AOI's name (read-only)."""
24
+ return self.__name
25
+
26
+ @property
27
+ def geom(self):
28
+ """The shapely geometry (Polygon or MultiPolygon) in image pixels, read-only."""
29
+ return self.__geom
30
+
31
+ def area(self):
32
+ """Area of the AOI in square pixels."""
33
+ return float(area(self.__geom))
34
+
35
+ def contains(self, points):
36
+ """True for each point inside or on the boundary of the AOI."""
37
+ return covers(self.__geom, points)
38
+
39
+ @staticmethod
40
+ def rectangle(name, x0, y0, x1, y1):
41
+ """Rectangular AOI from two opposite corners (x0, y0), (x1, y1) in pixels."""
42
+ return AOI(name=name, geom=box(min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1)))
43
+
44
+ @staticmethod
45
+ def polygon(name, vertices):
46
+ """Polygon AOI from a list of (x, y) vertices in pixels, at least three."""
47
+ assert len(vertices) >= 3, "A polygon AOI needs at least three corners."
48
+ return AOI(name=name, geom=Polygon(vertices).buffer(0))
49
+
50
+ def to_record(self):
51
+ """Dict {'name', 'wkt'} describing the AOI as text (WKT), for saving AOI sets to JSON; see from_record."""
52
+ return {'name': self.__name, 'wkt': self.__geom.wkt}
53
+
54
+ @staticmethod
55
+ def from_record(record):
56
+ """Rebuild an AOI from a to_record() dict."""
57
+ from shapely import wkt
58
+ return AOI(name=record['name'], geom=wkt.loads(record['wkt']))
59
+
60
+ def show(self, ax, color='yellow', alpha=0.4, label=True):
61
+ """Draw the AOI on a matplotlib axes as a translucent patch."""
62
+ ax.add_patch(patches.PathPatch(self.__path, facecolor=color, alpha=alpha, edgecolor=color, linewidth=1.5))
63
+ if not label:
64
+ return
65
+ polygons = sorted([(area(polygon), polygon) for polygon in self.__disconnected_polygons], reverse=True)
66
+ center = centroid(polygons[0][1])
67
+ ax.text(center.x, center.y, self.__name, ha='center', va='center', color="black",
68
+ path_effects=[
69
+ pe.Stroke(linewidth=4, foreground='white', alpha=0.7),
70
+ pe.Normal()
71
+ ])
72
+ for _, polygon in polygons[1:]:
73
+ center_ = centroid(polygon)
74
+ ax.plot([center.x, center_.x], [center.y, center_.y], color='black', alpha=0.3, linestyle='--')
75
+
76
+ @staticmethod
77
+ def __multiple_paths_to_single_geom(paths):
78
+ polygons = [AOI.__single_path_to_multiple_geoms(path) for path in paths]
79
+ flattened_polygons = functools.reduce(lambda a, b: a + b, polygons)
80
+ return unary_union(flattened_polygons)
81
+
82
+ @staticmethod
83
+ def __extract_disconnected_polygons(geom):
84
+ if geom.geom_type == 'Polygon':
85
+ return [geom]
86
+ elif geom.geom_type == 'MultiPolygon':
87
+ return list(geom.geoms)
88
+ elif geom.geom_type == 'GeometryCollection':
89
+ return [g for g in geom.geoms if isinstance(g, Polygon)]
90
+ else:
91
+ return []
92
+
93
+ @staticmethod
94
+ def __single_path_to_multiple_geoms(path):
95
+ polys = []
96
+ vertices = path.vertices
97
+ codes = path.codes
98
+ if codes is None:
99
+ if np.allclose(vertices[0], vertices[-1]):
100
+ polys.append(Polygon(vertices))
101
+ else:
102
+ polys.append(LinearRing(vertices))
103
+ return polys
104
+
105
+ current = []
106
+ for vert, code in zip(vertices, codes):
107
+ if code == mpath.Path.MOVETO:
108
+ if current:
109
+ ring = np.array(current)
110
+ if np.allclose(ring[0], ring[-1]):
111
+ polys.append(Polygon(ring))
112
+ else:
113
+ polys.append(LinearRing(ring))
114
+ current = [vert]
115
+ else:
116
+ current.append(vert)
117
+
118
+ if current:
119
+ ring = np.array(current)
120
+ if np.allclose(ring[0], ring[-1]):
121
+ polys.append(Polygon(ring))
122
+ else:
123
+ polys.append(LinearRing(ring))
124
+ return polys
125
+
126
+ @staticmethod
127
+ def __single_geom_to_single_path(geom):
128
+ if geom.geom_type == 'Polygon':
129
+ verts = np.array(geom.exterior.coords)
130
+ codes = ([mpath.Path.MOVETO] + [mpath.Path.LINETO] * (len(verts) - 2) + [mpath.Path.CLOSEPOLY])
131
+ return mpath.Path(verts, codes)
132
+ elif geom.geom_type == 'MultiPolygon':
133
+ all_verts = []
134
+ all_codes = []
135
+ for polygon in geom.geoms:
136
+ verts = np.array(polygon.exterior.coords)
137
+ codes = ([mpath.Path.MOVETO] + [mpath.Path.LINETO] * (len(verts) - 2) + [mpath.Path.CLOSEPOLY])
138
+ all_verts.extend(verts)
139
+ all_codes.extend(codes)
140
+ return mpath.Path(np.array(all_verts), all_codes)
141
+ else:
142
+ raise ValueError(f"Unsupported geometry type: {geom.geom_type}")