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.
- creapy-0.1.0/LICENSE +21 -0
- creapy-0.1.0/PKG-INFO +199 -0
- creapy-0.1.0/README.md +164 -0
- creapy-0.1.0/creapy/__init__.py +67 -0
- creapy-0.1.0/creapy/aoi/__init__.py +0 -0
- creapy-0.1.0/creapy/aoi/aoi.py +142 -0
- creapy-0.1.0/creapy/aoi/sequence.py +174 -0
- creapy-0.1.0/creapy/examples/README.md +7 -0
- creapy-0.1.0/creapy/examples/__init__.py +127 -0
- creapy-0.1.0/creapy/examples/demo_fixations.csv +2151 -0
- creapy-0.1.0/creapy/examples/demo_picture_A.png +0 -0
- creapy-0.1.0/creapy/examples/demo_picture_B.png +0 -0
- creapy-0.1.0/creapy/formats/__init__.py +5 -0
- creapy-0.1.0/creapy/formats/detect.py +127 -0
- creapy-0.1.0/creapy/formats/table.py +84 -0
- creapy-0.1.0/creapy/formats/tobii.py +339 -0
- creapy-0.1.0/creapy/gaze/__init__.py +0 -0
- creapy-0.1.0/creapy/gaze/base.py +160 -0
- creapy-0.1.0/creapy/gaze/convert.py +82 -0
- creapy-0.1.0/creapy/gaze/fixation.py +289 -0
- creapy-0.1.0/creapy/gaze/saccade.py +311 -0
- creapy-0.1.0/creapy/io/__init__.py +0 -0
- creapy-0.1.0/creapy/io/__main__.py +4 -0
- creapy-0.1.0/creapy/io/aoi_metrics_window.py +155 -0
- creapy-0.1.0/creapy/io/glossary.py +419 -0
- creapy-0.1.0/creapy/io/help.py +201 -0
- creapy-0.1.0/creapy/io/main.py +1865 -0
- creapy-0.1.0/creapy/io/project.py +48 -0
- creapy-0.1.0/creapy/io/session_log.py +73 -0
- creapy-0.1.0/creapy/io/style/crea_logo.png +0 -0
- creapy-0.1.0/creapy/io/style/fix_thumbnail.png +0 -0
- creapy-0.1.0/creapy/io/style/main_dark.qss +336 -0
- creapy-0.1.0/creapy/io/style/main_light.qss +207 -0
- creapy-0.1.0/creapy/io/style/sac_thumbnail.png +0 -0
- creapy-0.1.0/creapy/io/style/start_dark.qss +139 -0
- creapy-0.1.0/creapy/io/style/start_light.qss +93 -0
- creapy-0.1.0/creapy/io/views.py +418 -0
- creapy-0.1.0/creapy/io/widgets/__init__.py +0 -0
- creapy-0.1.0/creapy/io/widgets/column_mapping.py +113 -0
- creapy-0.1.0/creapy/io/widgets/compare_dialog.py +81 -0
- creapy-0.1.0/creapy/io/widgets/filter_bar.py +157 -0
- creapy-0.1.0/creapy/io/widgets/grouping_dialog.py +88 -0
- creapy-0.1.0/creapy/io/widgets/half_dial.py +220 -0
- creapy-0.1.0/creapy/io/widgets/painter.py +58 -0
- creapy-0.1.0/creapy/io/widgets/stimulus_strip.py +82 -0
- creapy-0.1.0/creapy/io/widgets/stimulus_thumbnail.py +76 -0
- creapy-0.1.0/creapy/io/widgets/table_view.py +47 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/__init__.py +0 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/aoi.py +170 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/fix_heatmap.py +227 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/fix_scanpath.py +114 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/recurrence.py +83 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/rose.py +100 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/sac_heatmap.py +183 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/scanpath.py +153 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/scanpath_compare.py +162 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/similarity.py +175 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/stats.py +90 -0
- creapy-0.1.0/creapy/io/widgets/toolbars/time_series.py +117 -0
- creapy-0.1.0/creapy/io/widgets/utils.py +103 -0
- creapy-0.1.0/creapy/io/worker.py +96 -0
- creapy-0.1.0/creapy/misc/__init__.py +0 -0
- creapy-0.1.0/creapy/misc/colormapping.py +13 -0
- creapy-0.1.0/creapy/misc/contours.py +41 -0
- creapy-0.1.0/creapy/misc/plot_manager.py +73 -0
- creapy-0.1.0/creapy/misc/repulsion.py +106 -0
- creapy-0.1.0/creapy/misc/utils.py +66 -0
- creapy-0.1.0/creapy/similarity/__init__.py +0 -0
- creapy-0.1.0/creapy/similarity/grid.py +35 -0
- creapy-0.1.0/creapy/similarity/grid_fixation.py +66 -0
- creapy-0.1.0/creapy/similarity/grid_result.py +227 -0
- creapy-0.1.0/creapy/similarity/heatmatch_result.py +269 -0
- creapy-0.1.0/creapy/similarity/scanpath.py +189 -0
- creapy-0.1.0/creapy/stats/__init__.py +8 -0
- creapy-0.1.0/creapy/stats/descriptives.py +107 -0
- creapy-0.1.0/creapy/stats/recurrence.py +111 -0
- creapy-0.1.0/creapy/stats/spectrum.py +164 -0
- creapy-0.1.0/creapy/stats/timeseries.py +104 -0
- creapy-0.1.0/creapy.egg-info/PKG-INFO +199 -0
- creapy-0.1.0/creapy.egg-info/SOURCES.txt +94 -0
- creapy-0.1.0/creapy.egg-info/dependency_links.txt +1 -0
- creapy-0.1.0/creapy.egg-info/entry_points.txt +2 -0
- creapy-0.1.0/creapy.egg-info/requires.txt +17 -0
- creapy-0.1.0/creapy.egg-info/top_level.txt +1 -0
- creapy-0.1.0/pyproject.toml +55 -0
- creapy-0.1.0/setup.cfg +4 -0
- creapy-0.1.0/tests/test_datasets.py +147 -0
- creapy-0.1.0/tests/test_explanations.py +88 -0
- creapy-0.1.0/tests/test_formats.py +122 -0
- creapy-0.1.0/tests/test_gui.py +183 -0
- creapy-0.1.0/tests/test_library_analyses.py +149 -0
- creapy-0.1.0/tests/test_library_api.py +127 -0
- creapy-0.1.0/tests/test_prolab_real.py +36 -0
- creapy-0.1.0/tests/test_recurrence.py +110 -0
- creapy-0.1.0/tests/test_scanpath.py +107 -0
- 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
|
+

|
|
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
|
+
|  |  |  |
|
|
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
|
+

|
|
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
|
+
|  |  |  |
|
|
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}")
|