heatmatch 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 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.
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: heatmatch
3
+ Version: 1.0.0
4
+ Summary: Orientation-aware visualization and comparison of very dense saccade patterns (scanpath comparison for long free viewing)
5
+ Author: Xingyu Long
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/xysLong/HeatMatch
8
+ Project-URL: Paper, https://doi.org/10.1145/3803539
9
+ Project-URL: Data, https://osf.io/f2xhj/
10
+ Keywords: eye-tracking,saccades,scanpath,scanpath-comparison,gaze,heatmap,visualization,orientation-field,empirical-aesthetics,art-perception
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering :: Visualization
15
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: Pillow
22
+ Dynamic: license-file
23
+
24
+ # HeatMatch
25
+
26
+ **Visualize and compare very dense saccade patterns from long free-viewing eye-tracking data.**
27
+
28
+ HeatMatch turns hundreds or thousands of saccades into continuous *orientation fields*: at every location it estimates saccade density, mean orientation, and how consistently the saccades agree in orientation. From these fields it draws orientation-coded heatmaps that show *where* gaze moves and *in which direction*. It also computes a similarity score for comparing saccade patterns across participants, stimuli, or conditions, without AOIs, scanpath alignment, or manual segmentation.
29
+
30
+ It was developed for 60-second viewing of paintings (empirical aesthetics, art history) and works for any static stimulus. Python implementation accompanying the paper:
31
+
32
+ <table border="0" cellspacing="0" cellpadding="0"><tr>
33
+ <td width="72%" valign="top">
34
+
35
+ <blockquote>
36
+ Xingyu Long, Jozsef Arato, Sophia Kury, Anna Miscena, and Raphael Rosenberg. 2026.<br/>
37
+ <strong>HeatMatch: Orientation-Aware Visualization and Comparison of Very Dense Saccade Patterns.</strong><br/>
38
+ <em>Proceedings of the ACM on Computer Graphics and Interactive Techniques</em> (PACMCGIT), 9(2), Article 18.<br/>
39
+ <a href="https://doi.org/10.1145/3803539">https://doi.org/10.1145/3803539</a>
40
+ </blockquote>
41
+
42
+ </td>
43
+ <td width="28%" align="center" valign="middle">
44
+ <img src="https://raw.githubusercontent.com/xysLong/HeatMatch/main/assets/logos.png" width="200" alt="CReA Lab and Vienna Cognitive Science Hub"/>
45
+ </td>
46
+ </tr></table>
47
+
48
+ ---
49
+
50
+ ## Overview
51
+
52
+ 1. **OOI-coded heatmaps** — orientation fields visualized with researcher-defined color anchors and confidence-weighted opacity.
53
+ 2. **HeatMatch similarity** — comparison based on saccade density and orientation.
54
+
55
+ ![HeatMatch heatmap example](https://raw.githubusercontent.com/xysLong/HeatMatch/main/assets/example_heatmaps.jpg)
56
+ <sub>Per-participant (columns 1–5) and aggregated (column 6) heatmaps for three paintings. Hue = mean saccade orientation relative to OOI; opacity = local confidence. Stimulus images: public domain via [Wikimedia Commons](https://commons.wikimedia.org) — see paper for details.</sub>
57
+
58
+ See the [paper](https://doi.org/10.1145/3803539) for methodology.
59
+
60
+ ### When to use it
61
+
62
+ - **Long, dense viewing.** Scanpath comparison methods such as MultiMatch or ScanMatch align scanpaths sequence by sequence. With hundreds of saccades per trial and strongly diverging paths, they become slow and hard to interpret. HeatMatch aggregates instead of aligning, so it scales to arbitrarily dense data.
63
+ - **Orientation matters.** Fixation heatmaps and density-based metrics ignore saccade direction. HeatMatch keeps orientation, so you can ask whether gaze follows horizontal, vertical, diagonal, or symmetric structure in an image, e.g. composition lines in a painting.
64
+ - **Group, stimulus, and individual comparisons.** In the paper, the similarity score recovers the abstract vs. still-life distinction and differences in within-painting agreement. It also reveals stable individual "saccadic signatures" that hold across paintings.
65
+
66
+ It is **not** a replacement for sequence-based methods. HeatMatch is permutation-invariant (it ignores saccade order) and treats orientations as unsigned (left-to-right = right-to-left). Use it alongside MultiMatch, ScanMatch, or RQA when order or direction matters.
67
+
68
+ ---
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install heatmatch
74
+ ```
75
+
76
+ Dependencies: `numpy`, `matplotlib`, `Pillow`. For development, clone the repository and run `pip install -e .`.
77
+
78
+ ---
79
+
80
+ ## Quick Start
81
+
82
+ ```python
83
+ import matplotlib.pyplot as plt
84
+ from heatmatch import make_reference_grid, make_orientation_field, Heatmap, compute_similarity
85
+
86
+ W, H = 2880, 2160
87
+ onset = ... # (J, 2) saccade start coordinates
88
+ offset = ... # (J, 2) saccade end coordinates
89
+
90
+ pts, xx, yy = make_reference_grid(W, H, grid_resolution=200)
91
+ field = make_orientation_field(pts, onset, offset, sigma=50.0, grid_shape=yy.shape)
92
+
93
+ fig, ax = plt.subplots()
94
+ Heatmap(field, W, H).draw(ax, ooi=0.0) # ooi=0 → horizontal, ooi=90 → vertical
95
+ plt.show()
96
+
97
+ # Compare two patterns (fields built on the same grid, e.g. two participants)
98
+ result = compute_similarity(field_a, field_b)
99
+ s = (result.s_loc + result.s_dir) / 2 # composite HeatMatch score S in [0, 1]
100
+ print(result.s_loc, result.s_dir, s)
101
+ ```
102
+
103
+ A full worked example is in [`demo.ipynb`](https://github.com/xysLong/HeatMatch/blob/main/demo.ipynb). The dataset is not included — download `data_anonymized.csv` from **[osf.io/f2xhj](https://osf.io/f2xhj/)** and place it in `tests/`.
104
+
105
+ ---
106
+
107
+ ## API
108
+
109
+ ### `heatmatch.fields`
110
+
111
+ | | |
112
+ |---|---|
113
+ | `make_reference_grid(w, h, grid_resolution)` | Returns `(pts, xx, yy)`; int → square grid, `(ny, nx)` tuple → rectangular |
114
+ | `make_orientation_field(pts, onset, offset, sigma, grid_shape, ...)` | Returns `OrientationField` with `omega_mean`, `R`, `rho` |
115
+
116
+ ### `heatmatch.heatmapping`
117
+
118
+ | | |
119
+ |---|---|
120
+ | `Heatmap(field, w, h, image=None)` | Caches colormap, opacity, and (optionally) grayscale background image |
121
+ | `Heatmap.draw(ax, ooi, opacity_density_weight, base_opacity, cmap, ...)` | Renders onto `ax` |
122
+
123
+ OOI in degrees: 0 = east, 90 = north, 180 = west, 270 = south. Unsigned, so `ooi` and `ooi ± 180` are equivalent.
124
+
125
+ ### `heatmatch.matching`
126
+
127
+ | | |
128
+ |---|---|
129
+ | `compute_similarity(field_a, field_b, density_coherence_tradeoff)` | Returns `SimilarityResult(s_loc, s_dir)` |
130
+
131
+ ---
132
+
133
+ ## Key Parameters
134
+
135
+ | Parameter | Default | Notes |
136
+ |---|---|---|
137
+ | `sigma` | `50.0` | Gaussian bandwidth in px. Paper evaluates {50, 100, 150}. |
138
+ | `grid_resolution` | `200` | Int or `(ny, nx)`. Paper uses 200. |
139
+ | `ooi` | `0.0` | Orientation of Interest in degrees. |
140
+ | `opacity_density_weight` | `1.0` | Density vs. coherence for opacity. Recommended 0.5–1.0. |
141
+ | `base_opacity` | `0.85` | Global opacity ceiling; useful with a background image. |
142
+ | `density_coherence_tradeoff` | `0.5` | Density vs. coherence for *S*_dir. At 1.0, angular information is discarded. |
143
+
144
+ ---
145
+
146
+ ## Citation
147
+
148
+ ```bibtex
149
+ @article{long2026heatmatch,
150
+ author = {Long, Xingyu and Arato, Jozsef and Kury, Sophia and Miscena, Anna and Rosenberg, Raphael},
151
+ title = {HeatMatch: Orientation-Aware Visualization and Comparison of Very Dense Saccade Patterns},
152
+ journal = {Proc. ACM Comput. Graph. Interact. Tech.},
153
+ year = {2026},
154
+ volume = {9},
155
+ number = {2},
156
+ articleno = {18},
157
+ doi = {10.1145/3803539},
158
+ }
159
+ ```
@@ -0,0 +1,136 @@
1
+ # HeatMatch
2
+
3
+ **Visualize and compare very dense saccade patterns from long free-viewing eye-tracking data.**
4
+
5
+ HeatMatch turns hundreds or thousands of saccades into continuous *orientation fields*: at every location it estimates saccade density, mean orientation, and how consistently the saccades agree in orientation. From these fields it draws orientation-coded heatmaps that show *where* gaze moves and *in which direction*. It also computes a similarity score for comparing saccade patterns across participants, stimuli, or conditions, without AOIs, scanpath alignment, or manual segmentation.
6
+
7
+ It was developed for 60-second viewing of paintings (empirical aesthetics, art history) and works for any static stimulus. Python implementation accompanying the paper:
8
+
9
+ <table border="0" cellspacing="0" cellpadding="0"><tr>
10
+ <td width="72%" valign="top">
11
+
12
+ <blockquote>
13
+ Xingyu Long, Jozsef Arato, Sophia Kury, Anna Miscena, and Raphael Rosenberg. 2026.<br/>
14
+ <strong>HeatMatch: Orientation-Aware Visualization and Comparison of Very Dense Saccade Patterns.</strong><br/>
15
+ <em>Proceedings of the ACM on Computer Graphics and Interactive Techniques</em> (PACMCGIT), 9(2), Article 18.<br/>
16
+ <a href="https://doi.org/10.1145/3803539">https://doi.org/10.1145/3803539</a>
17
+ </blockquote>
18
+
19
+ </td>
20
+ <td width="28%" align="center" valign="middle">
21
+ <img src="https://raw.githubusercontent.com/xysLong/HeatMatch/main/assets/logos.png" width="200" alt="CReA Lab and Vienna Cognitive Science Hub"/>
22
+ </td>
23
+ </tr></table>
24
+
25
+ ---
26
+
27
+ ## Overview
28
+
29
+ 1. **OOI-coded heatmaps** — orientation fields visualized with researcher-defined color anchors and confidence-weighted opacity.
30
+ 2. **HeatMatch similarity** — comparison based on saccade density and orientation.
31
+
32
+ ![HeatMatch heatmap example](https://raw.githubusercontent.com/xysLong/HeatMatch/main/assets/example_heatmaps.jpg)
33
+ <sub>Per-participant (columns 1–5) and aggregated (column 6) heatmaps for three paintings. Hue = mean saccade orientation relative to OOI; opacity = local confidence. Stimulus images: public domain via [Wikimedia Commons](https://commons.wikimedia.org) — see paper for details.</sub>
34
+
35
+ See the [paper](https://doi.org/10.1145/3803539) for methodology.
36
+
37
+ ### When to use it
38
+
39
+ - **Long, dense viewing.** Scanpath comparison methods such as MultiMatch or ScanMatch align scanpaths sequence by sequence. With hundreds of saccades per trial and strongly diverging paths, they become slow and hard to interpret. HeatMatch aggregates instead of aligning, so it scales to arbitrarily dense data.
40
+ - **Orientation matters.** Fixation heatmaps and density-based metrics ignore saccade direction. HeatMatch keeps orientation, so you can ask whether gaze follows horizontal, vertical, diagonal, or symmetric structure in an image, e.g. composition lines in a painting.
41
+ - **Group, stimulus, and individual comparisons.** In the paper, the similarity score recovers the abstract vs. still-life distinction and differences in within-painting agreement. It also reveals stable individual "saccadic signatures" that hold across paintings.
42
+
43
+ It is **not** a replacement for sequence-based methods. HeatMatch is permutation-invariant (it ignores saccade order) and treats orientations as unsigned (left-to-right = right-to-left). Use it alongside MultiMatch, ScanMatch, or RQA when order or direction matters.
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install heatmatch
51
+ ```
52
+
53
+ Dependencies: `numpy`, `matplotlib`, `Pillow`. For development, clone the repository and run `pip install -e .`.
54
+
55
+ ---
56
+
57
+ ## Quick Start
58
+
59
+ ```python
60
+ import matplotlib.pyplot as plt
61
+ from heatmatch import make_reference_grid, make_orientation_field, Heatmap, compute_similarity
62
+
63
+ W, H = 2880, 2160
64
+ onset = ... # (J, 2) saccade start coordinates
65
+ offset = ... # (J, 2) saccade end coordinates
66
+
67
+ pts, xx, yy = make_reference_grid(W, H, grid_resolution=200)
68
+ field = make_orientation_field(pts, onset, offset, sigma=50.0, grid_shape=yy.shape)
69
+
70
+ fig, ax = plt.subplots()
71
+ Heatmap(field, W, H).draw(ax, ooi=0.0) # ooi=0 → horizontal, ooi=90 → vertical
72
+ plt.show()
73
+
74
+ # Compare two patterns (fields built on the same grid, e.g. two participants)
75
+ result = compute_similarity(field_a, field_b)
76
+ s = (result.s_loc + result.s_dir) / 2 # composite HeatMatch score S in [0, 1]
77
+ print(result.s_loc, result.s_dir, s)
78
+ ```
79
+
80
+ A full worked example is in [`demo.ipynb`](https://github.com/xysLong/HeatMatch/blob/main/demo.ipynb). The dataset is not included — download `data_anonymized.csv` from **[osf.io/f2xhj](https://osf.io/f2xhj/)** and place it in `tests/`.
81
+
82
+ ---
83
+
84
+ ## API
85
+
86
+ ### `heatmatch.fields`
87
+
88
+ | | |
89
+ |---|---|
90
+ | `make_reference_grid(w, h, grid_resolution)` | Returns `(pts, xx, yy)`; int → square grid, `(ny, nx)` tuple → rectangular |
91
+ | `make_orientation_field(pts, onset, offset, sigma, grid_shape, ...)` | Returns `OrientationField` with `omega_mean`, `R`, `rho` |
92
+
93
+ ### `heatmatch.heatmapping`
94
+
95
+ | | |
96
+ |---|---|
97
+ | `Heatmap(field, w, h, image=None)` | Caches colormap, opacity, and (optionally) grayscale background image |
98
+ | `Heatmap.draw(ax, ooi, opacity_density_weight, base_opacity, cmap, ...)` | Renders onto `ax` |
99
+
100
+ OOI in degrees: 0 = east, 90 = north, 180 = west, 270 = south. Unsigned, so `ooi` and `ooi ± 180` are equivalent.
101
+
102
+ ### `heatmatch.matching`
103
+
104
+ | | |
105
+ |---|---|
106
+ | `compute_similarity(field_a, field_b, density_coherence_tradeoff)` | Returns `SimilarityResult(s_loc, s_dir)` |
107
+
108
+ ---
109
+
110
+ ## Key Parameters
111
+
112
+ | Parameter | Default | Notes |
113
+ |---|---|---|
114
+ | `sigma` | `50.0` | Gaussian bandwidth in px. Paper evaluates {50, 100, 150}. |
115
+ | `grid_resolution` | `200` | Int or `(ny, nx)`. Paper uses 200. |
116
+ | `ooi` | `0.0` | Orientation of Interest in degrees. |
117
+ | `opacity_density_weight` | `1.0` | Density vs. coherence for opacity. Recommended 0.5–1.0. |
118
+ | `base_opacity` | `0.85` | Global opacity ceiling; useful with a background image. |
119
+ | `density_coherence_tradeoff` | `0.5` | Density vs. coherence for *S*_dir. At 1.0, angular information is discarded. |
120
+
121
+ ---
122
+
123
+ ## Citation
124
+
125
+ ```bibtex
126
+ @article{long2026heatmatch,
127
+ author = {Long, Xingyu and Arato, Jozsef and Kury, Sophia and Miscena, Anna and Rosenberg, Raphael},
128
+ title = {HeatMatch: Orientation-Aware Visualization and Comparison of Very Dense Saccade Patterns},
129
+ journal = {Proc. ACM Comput. Graph. Interact. Tech.},
130
+ year = {2026},
131
+ volume = {9},
132
+ number = {2},
133
+ articleno = {18},
134
+ doi = {10.1145/3803539},
135
+ }
136
+ ```
@@ -0,0 +1,3 @@
1
+ from heatmatch.fields import make_reference_grid, make_orientation_field, OrientationField
2
+ from heatmatch.heatmapping import Heatmap
3
+ from heatmatch.matching import compute_similarity, SimilarityResult
@@ -0,0 +1,197 @@
1
+ import numpy as np
2
+ from dataclasses import dataclass
3
+
4
+ """
5
+ Field Computation
6
+
7
+ Computes orientation fields over a reference grid from saccade data.
8
+
9
+ - Orientation — unsigned saccade orientation ω_j ∈ [0, π).
10
+ - Point-Saccade Distance — minimal Euclidean distance from a reference point p_i
11
+ to a saccade segment s_j.
12
+ - Gaussian Kernel — kernel K(p_i, s_j) and kernel mass m_i.
13
+ - Mean Orientation and Coherence — weighted mean orientation ω̄_i and mean
14
+ resultant length R_i via double-angle embedding.
15
+ - Density — normalized kernel mass ρ_i.
16
+
17
+ Public API : make_reference_grid, make_orientation_field, OrientationField
18
+ Internal : _compute_orientations, _point_to_segment_distances, _gaussian_kernel
19
+ """
20
+
21
+
22
+ @dataclass
23
+ class OrientationField:
24
+ """
25
+ Precomputed orientation field over a reference grid.
26
+
27
+ Holds the three per-point statistics returned by make_orientation_field
28
+ together with the grid shape so that callers need not carry it separately.
29
+
30
+ Attributes
31
+ ----------
32
+ omega_mean : (P,) array in [0, π), NaN where m_i = 0
33
+ R : (P,) array in [0, 1] — mean resultant length (coherence)
34
+ rho : (P,) array in [0, 1] — normalized kernel mass (density), sums to 1
35
+ grid_shape : (ny, nx) tuple — reshape arrays with .reshape(grid_shape)
36
+ """
37
+ omega_mean: np.ndarray
38
+ R: np.ndarray
39
+ rho: np.ndarray
40
+ grid_shape: tuple
41
+
42
+
43
+ def _compute_orientations(onset, offset):
44
+ """
45
+ Unsigned saccade orientations ω_j ∈ [0, π).
46
+
47
+ ω_j = atan2(y_{j1} − y_{j0}; x_{j1} − x_{j0}) mod π
48
+
49
+ Orientations are unsigned: ω and ω+π are treated as equivalent, so that
50
+ back-and-forth saccades along the same axis reinforce rather than cancel.
51
+
52
+ Used internally by make_orientation_field; not part of the public API.
53
+ """
54
+ d = offset - onset
55
+ return np.mod(np.arctan2(d[:, 1], d[:, 0]), np.pi)
56
+
57
+
58
+ def _point_to_segment_distances(points, onset, offset):
59
+ """
60
+ Minimal Euclidean distance d(p_i, s_j) from each reference point p_i to each
61
+ saccade segment s_j = [s_{j0}, s_{j1}].
62
+
63
+ Used internally by make_orientation_field; not part of the public API.
64
+ """
65
+ p = points[:, None, :] # (P, 1, 2)
66
+ a = onset[None, :, :] # (1, J, 2)
67
+ b = offset[None, :, :] # (1, J, 2)
68
+
69
+ ab = b - a # (1, J, 2)
70
+ ap = p - a # (P, J, 2)
71
+ ab_len2 = np.sum(ab * ab, axis=-1, keepdims=True) # (1, J, 1)
72
+ ab_len2 = np.where(ab_len2 == 0.0, 1.0, ab_len2) # guard zero-length
73
+
74
+ t = np.clip(np.sum(ap * ab, axis=-1, keepdims=True) / ab_len2, 0.0, 1.0) # (P, J, 1)
75
+ closest = a + t * ab # (P, J, 2)
76
+ return np.linalg.norm(p - closest, axis=-1) # (P, J)
77
+
78
+
79
+ def _gaussian_kernel(d, sigma):
80
+ """
81
+ Gaussian kernel K(p_i, s_j) = exp(−½ (d / σ)²).
82
+
83
+ Used internally by make_orientation_field; not part of the public API.
84
+ """
85
+ return np.exp(-0.5 * (d / sigma) ** 2)
86
+
87
+
88
+
89
+ def make_reference_grid(w, h, grid_resolution=200):
90
+ """
91
+ Uniform reference grid of positions {p_i} ⊂ [1..W] × [1..H].
92
+
93
+ Parameters
94
+ ----------
95
+ w, h : stimulus width and height in pixels
96
+ grid_resolution : int or (ny, nx) tuple
97
+ Number of grid points along each axis. A single integer
98
+ produces a square grid; a tuple sets rows and columns
99
+ independently. Follows numpy's (rows, cols) = (ny, nx)
100
+ convention, matching the shape of the returned YY array.
101
+ Default: 200 (i.e. 200×200 = 40 000 points).
102
+
103
+ Returns
104
+ -------
105
+ pts : (ny*nx, 2) array — flattened (x, y) positions, input to make_orientation_field
106
+ XX : (ny, nx) meshgrid of x-coordinates (use for reshaping field outputs)
107
+ YY : (ny, nx) meshgrid of y-coordinates (use for reshaping field outputs)
108
+ """
109
+ if isinstance(grid_resolution, int):
110
+ ny = nx = grid_resolution
111
+ else:
112
+ ny, nx = grid_resolution
113
+ xs = np.linspace(1, w, nx, dtype=np.float64)
114
+ ys = np.linspace(1, h, ny, dtype=np.float64)
115
+ xx, yy = np.meshgrid(xs, ys)
116
+ pts = np.column_stack([xx.ravel(), yy.ravel()])
117
+ return pts, xx, yy
118
+
119
+
120
+ def make_orientation_field(points, onset, offset, sigma=50.0,
121
+ grid_shape=None,
122
+ points_per_chunk=4096, segs_per_chunk=512):
123
+ """
124
+ Compute the orientation field (ω̄, R, ρ) over all reference points.
125
+
126
+ For each reference point p_i, accumulates:
127
+ sin_i = Σ_j K(p_i,s_j) · sin(2ω_j) (unnormalized; proportional to paper's s̄_i · m_i)
128
+ cos_i = Σ_j K(p_i,s_j) · cos(2ω_j) (unnormalized; proportional to paper's c̄_i · m_i)
129
+ m_i = Σ_j K(p_i,s_j) (kernel mass)
130
+
131
+ Then derives:
132
+ ω̄_i = ½ · (atan2(sin_i, cos_i) mod 2π) ∈ [0, π) mean orientation
133
+ R_i = √(sin_i² + cos_i²) / m_i ∈ [0, 1] coherence (MRL)
134
+ ρ_i = m_i / Σ_k m_k ∈ [0, 1] density
135
+
136
+ Points with no kernel support (m_i = 0) get ω̄_i = NaN, R_i = 0, ρ_i = 0.
137
+
138
+ Parameters
139
+ ----------
140
+ points : (P, 2) reference grid positions {p_i}
141
+ onset : (J, 2) saccade starting points {s_{j0}}
142
+ offset : (J, 2) saccade end points {s_{j1}}
143
+ sigma : float Gaussian bandwidth σ > 0, in pixels
144
+ grid_shape : (ny, nx) tuple — stored on the returned OrientationField for
145
+ convenient reshaping; pass yy.shape from make_reference_grid
146
+ points_per_chunk : int chunk size over grid points (memory/speed trade-off)
147
+ segs_per_chunk : int chunk size over saccades (memory/speed trade-off)
148
+
149
+ Returns
150
+ -------
151
+ OrientationField with omega_mean, R, rho (each (P,)) and grid_shape
152
+ """
153
+ n_pts = points.shape[0]
154
+ n_sac = onset.shape[0]
155
+
156
+ omega = _compute_orientations(onset, offset)
157
+
158
+ sin2 = np.sin(2.0 * omega) # (J,)
159
+ cos2 = np.cos(2.0 * omega) # (J,)
160
+
161
+ sin_acc = np.zeros(n_pts, dtype=np.float64)
162
+ cos_acc = np.zeros(n_pts, dtype=np.float64)
163
+ mass_acc = np.zeros(n_pts, dtype=np.float64)
164
+
165
+ for p0 in range(0, n_pts, points_per_chunk):
166
+ p1 = min(n_pts, p0 + points_per_chunk)
167
+ p_blk = points[p0:p1]
168
+
169
+ sin_blk = np.zeros(p1 - p0, dtype=np.float64)
170
+ cos_blk = np.zeros(p1 - p0, dtype=np.float64)
171
+ mass_blk = np.zeros(p1 - p0, dtype=np.float64)
172
+
173
+ for s0 in range(0, n_sac, segs_per_chunk):
174
+ s1 = min(n_sac, s0 + segs_per_chunk)
175
+ d_blk = _point_to_segment_distances(p_blk, onset[s0:s1], offset[s0:s1])
176
+ k_blk = _gaussian_kernel(d_blk, sigma)
177
+
178
+ mass_blk += k_blk.sum(axis=1)
179
+ sin_blk += k_blk @ sin2[s0:s1]
180
+ cos_blk += k_blk @ cos2[s0:s1]
181
+
182
+ sin_acc[p0:p1] = sin_blk
183
+ cos_acc[p0:p1] = cos_blk
184
+ mass_acc[p0:p1] = mass_blk
185
+
186
+ # ω̄_i = ½ · (atan2(sin_i, cos_i) mod 2π) — scale-invariant, so unnormalized sums work
187
+ omega_mean = 0.5 * np.mod(np.arctan2(sin_acc, cos_acc), 2.0 * np.pi)
188
+ omega_mean = np.where(mass_acc > 0.0, omega_mean, np.nan)
189
+
190
+ # R_i = √(s̄_i² + c̄_i²) where s̄_i = sin_i/m_i, c̄_i = cos_i/m_i
191
+ R = np.where(mass_acc > 0.0, np.sqrt(sin_acc**2 + cos_acc**2) / mass_acc, 0.0)
192
+
193
+ # ρ_i = m_i / Σ_k m_k
194
+ total_mass = mass_acc.sum()
195
+ rho = mass_acc / total_mass if total_mass > 0 else np.zeros_like(mass_acc)
196
+
197
+ return OrientationField(omega_mean=omega_mean, R=R, rho=rho, grid_shape=grid_shape)
@@ -0,0 +1,157 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import matplotlib.cm as cm
4
+ from matplotlib.colors import ListedColormap, Normalize
5
+ from PIL import Image
6
+ import math
7
+
8
+ """
9
+ Visualization
10
+
11
+ Implements the OOI-coded heatmap rendering pipeline.
12
+
13
+ Public API : Heatmap
14
+ Internal : _make_cyclic_colormap, _opacity_from_confidence, _load_grayscale
15
+ """
16
+
17
+
18
+ def _make_cyclic_colormap(base_cmap):
19
+ """
20
+ Cyclic diverging colormap for unsigned orientation display.
21
+
22
+ Concatenates the forward and reverse passes of a diverging colormap so that
23
+ ω = 0 and ω = π map to the same color, creating a seamless cycle over [0, π).
24
+ """
25
+ base = plt.get_cmap(base_cmap)
26
+ n = 256
27
+ forward = base(np.linspace(0, 1, n))
28
+ backward = base(np.linspace(1, 0, n))
29
+ colors = np.vstack([forward[:-1], backward[:-1]]) # avoid seam duplicate
30
+ return ListedColormap(colors, name=f"{base_cmap}_cyclic")
31
+
32
+
33
+ def _opacity_from_confidence(confidence, degree=2.0, quantile=0.8, eps=1e-12):
34
+ """
35
+ "Gentle nonlinearity" mapping a confidence signal to opacity ∈ [0, 1].
36
+
37
+ Uses a logistic function centered at the quantile-th percentile so that
38
+ low-confidence grid points fade toward transparency.
39
+ """
40
+ cmin, cmax = float(np.min(confidence)), float(np.max(confidence))
41
+ if cmax <= cmin + eps:
42
+ return np.zeros_like(confidence)
43
+ c = (confidence - cmin) / (cmax - cmin)
44
+ center = np.quantile(c, quantile)
45
+ c = np.clip(c, eps, 1.0 - eps)
46
+ return 1.0 / (1.0 + (center * (1.0 - c) / ((1.0 - center) * c)) ** degree)
47
+
48
+
49
+ def _load_grayscale(path):
50
+ """
51
+ Load an image from path and return a float32 grayscale array in [0, 1].
52
+
53
+ Uses PIL for format detection, so the actual file format takes precedence
54
+ over the file extension (handles e.g. JPEG files named .png).
55
+ """
56
+ img = Image.open(str(path)).convert('L') # 'L' = 8-bit grayscale
57
+ return np.asarray(img, dtype=np.float32) / 255.0
58
+
59
+
60
+ class Heatmap:
61
+ """
62
+ OOI-coded heatmap renderer with two-level caching.
63
+
64
+ Wraps an OrientationField and caches intermediate results to avoid redundant
65
+ computation when only display parameters change:
66
+
67
+ - Background image → loaded and converted to grayscale once at construction.
68
+ - Cyclic colormap → rebuilt only when `cmap` changes.
69
+ - Opacity array → rebuilt only when `opacity_density_weight`, `degree`, or `quantile` changes.
70
+ - OOI rotation → always cheap (one np.mod call), never cached.
71
+
72
+ Parameters
73
+ ----------
74
+ field : OrientationField
75
+ Precomputed orientation field from make_orientation_field.
76
+ w, h : stimulus width and height in pixels
77
+ image : str, path-like, or None
78
+ Optional path to a stimulus image. When provided it is converted to
79
+ grayscale once and rendered as a background; the heatmap is composited
80
+ on top with opacity scaled by `base_opacity`.
81
+ """
82
+
83
+ def __init__(self, field, w, h, image=None):
84
+ self._field = field
85
+ self._w = w
86
+ self._h = h
87
+
88
+ self._image_gray = _load_grayscale(image) if image is not None else None
89
+
90
+ self._cmap_name = None
91
+ self._cyclic_cmap = None
92
+
93
+ self._opacity_key = None # (opacity_density_weight, degree, quantile)
94
+ self._opacity = None
95
+
96
+ def draw(self, ax, ooi=0.0, opacity_density_weight=1.0, base_opacity=0.85,
97
+ cmap='coolwarm', degree=2.0, quantile=0.8):
98
+ """
99
+ Render the OOI-coded saccade heatmap onto a matplotlib Axes.
100
+
101
+ Hue encodes mean orientation ω̄_i relative to the chosen OOI.
102
+ Opacity encodes local confidence ρ^opacity_density_weight · R^(1−opacity_density_weight),
103
+ passed through a gentle nonlinearity and then scaled by base_opacity.
104
+
105
+ Parameters
106
+ ----------
107
+ ax : matplotlib Axes
108
+ ooi : float — Orientation of Interest in degrees, compass convention:
109
+ 0=east, 90=north, 180=west, 270=south (default 0.0 = horizontal).
110
+ Orientations are unsigned so ooi and ooi±180 are equivalent.
111
+ opacity_density_weight : float in [0, 1] — blend between density and coherence for opacity:
112
+ 1.0 → density only, 0.0 → coherence only. Recommended 0.5–1.0.
113
+ base_opacity : float in [0, 1] — global opacity ceiling applied on top of the
114
+ data-derived opacity; useful when a background image is shown.
115
+ The final per-point alpha = data_opacity × base_opacity.
116
+ cmap : str — base diverging colormap name (default 'coolwarm')
117
+ degree : float — sharpness of the opacity nonlinearity
118
+ quantile : float — center of the opacity nonlinearity
119
+ """
120
+ # -- colormap cache (invalidated on cmap change) -----------------------
121
+ if cmap != self._cmap_name:
122
+ self._cyclic_cmap = _make_cyclic_colormap(cmap)
123
+ self._cmap_name = cmap
124
+
125
+ # -- opacity cache (invalidated on opacity_density_weight / degree / quantile change) --
126
+ opacity_key = (opacity_density_weight, degree, quantile)
127
+ if opacity_key != self._opacity_key:
128
+ confidence = self._field.rho ** opacity_density_weight * self._field.R ** (1.0 - opacity_density_weight)
129
+ self._opacity = _opacity_from_confidence(confidence, degree=degree, quantile=quantile)
130
+ self._opacity_key = opacity_key
131
+
132
+ # -- background image (grayscale) --------------------------------------
133
+ if self._image_gray is not None:
134
+ ax.imshow(self._image_gray, extent=(1, self._w, self._h, 1),
135
+ origin='upper', cmap='gray', vmin=0.0, vmax=1.0,
136
+ interpolation='bilinear')
137
+
138
+ # -- OOI rotation (always cheap) ---------------------------------------
139
+ omega_display = np.mod(self._field.omega_mean - np.deg2rad(ooi), math.pi)
140
+
141
+ # -- compose RGBA and render -------------------------------------------
142
+ norm = Normalize(vmin=0.0, vmax=math.pi)
143
+ sm = cm.ScalarMappable(norm=norm, cmap=self._cyclic_cmap)
144
+
145
+ shape = self._field.grid_shape
146
+ rgba = sm.to_rgba(omega_display.reshape(shape))
147
+ alpha = np.where(np.isnan(self._field.omega_mean.reshape(shape)),
148
+ 0.0,
149
+ self._opacity.reshape(shape) * base_opacity)
150
+ rgba[..., -1] = alpha
151
+
152
+ ax.imshow(rgba, extent=(1, self._w, self._h, 1),
153
+ origin='upper', interpolation='gaussian')
154
+ ax.set_xlim(1, self._w)
155
+ ax.set_ylim(self._h, 1)
156
+ ax.set_xticks([])
157
+ ax.set_yticks([])
@@ -0,0 +1,99 @@
1
+ import numpy as np
2
+ from dataclasses import dataclass
3
+
4
+ """
5
+ HeatMatch Similarity
6
+
7
+ Computes pairwise similarity scores between two saccade orientation fields.
8
+
9
+ - Locational similarity S_loc: Pearson correlation of density maps ρ, rescaled to [0, 1].
10
+ - Directional similarity S_dir: density- and coherence-weighted sum of per-point axial
11
+ angular similarities, using geometric-mean overlap weights, with a density-coherence
12
+ trade-off parameter ∈ [0, 1].
13
+
14
+ Public API : compute_similarity, SimilarityResult
15
+ Internal : _locational_similarity, _directional_similarity
16
+ """
17
+
18
+
19
+ @dataclass
20
+ class SimilarityResult:
21
+ """
22
+ HeatMatch similarity scores between two saccade patterns.
23
+
24
+ Attributes
25
+ ----------
26
+ s_loc : float in [0, 1] — locational similarity S_loc
27
+ s_dir : float in [0, 1] — directional similarity S_dir
28
+ """
29
+ s_loc: float
30
+ s_dir: float
31
+
32
+
33
+ def _locational_similarity(rho_a, rho_b):
34
+ """
35
+ S_loc(A, B) = (corr(ρ^A, ρ^B) + 1) / 2 ∈ [0, 1].
36
+
37
+ Pearson correlation of the two density maps, shifted to [0, 1].
38
+ S_loc = 1: identical spatial layout; S_loc = 0: maximally anti-correlated.
39
+ """
40
+ corr = np.corrcoef(rho_a.ravel(), rho_b.ravel())[0, 1]
41
+ return float((corr + 1.0) / 2.0)
42
+
43
+
44
+ def _directional_similarity(omega_a, omega_b, R_a, R_b, rho_a, rho_b,
45
+ density_coherence_tradeoff=0.5):
46
+ """
47
+ S_dir(A, B) = Σ_i w_i · axial-sim(ω̄_i^A, ω̄_i^B) / Σ_i w_i ∈ [0, 1].
48
+
49
+ where w_i = ρ̄_i^dct · R̄_i^(1−dct), dct = density_coherence_tradeoff.
50
+
51
+ ρ̄_i = √(ρ_i^A · ρ_i^B) and R̄_i = √(R_i^A · R_i^B) are the geometric-mean
52
+ overlap weights for density and coherence.
53
+
54
+ density_coherence_tradeoff=1 weights by density only — at this extreme S_dir
55
+ becomes statistically redundant with S_loc since angular coherence has no
56
+ influence on the score. Recommended value ~0.5.
57
+
58
+ axial-sim(ω^A, ω^B) = 1 − (2/π) · δ where δ = min(Δ, π−Δ), Δ = |ω^A − ω^B|.
59
+
60
+ Grid points with no kernel support have ω̄ = NaN and ρ = 0; their contribution
61
+ is zero via the ρ̄ weight. axial-sim is set to 0 for NaN inputs as a safety guard.
62
+ """
63
+ dct = density_coherence_tradeoff
64
+
65
+ Delta = np.abs(omega_a - omega_b)
66
+ delta = np.minimum(Delta, np.pi - Delta)
67
+ axial_sim = 1.0 - (2.0 / np.pi) * delta
68
+ axial_sim = np.where(np.isnan(axial_sim), 0.0, axial_sim)
69
+
70
+ rho_bar = np.sqrt(rho_a * rho_b)
71
+ r_bar = np.sqrt(R_a * R_b)
72
+ w = rho_bar ** dct * r_bar ** (1.0 - dct)
73
+ W = float(w.sum())
74
+ return float(np.sum(w * axial_sim) / W) if W > 0.0 else 0.0
75
+
76
+
77
+ def compute_similarity(field_a, field_b, density_coherence_tradeoff=0.5):
78
+ """
79
+ Compute HeatMatch similarity between two saccade patterns.
80
+
81
+ Parameters
82
+ ----------
83
+ field_a, field_b : OrientationField
84
+ Precomputed orientation fields from make_orientation_field.
85
+ density_coherence_tradeoff : float in [0, 1]
86
+ Controls how much S_dir weights grid points by density vs. coherence.
87
+ At 1.0 the weight is purely density-based and angular coherence has no influence,
88
+ making S_dir statistically redundant with S_loc. Recommended ~0.5.
89
+
90
+ Returns
91
+ -------
92
+ SimilarityResult with s_loc and s_dir
93
+ """
94
+ s_loc = _locational_similarity(field_a.rho, field_b.rho)
95
+ s_dir = _directional_similarity(field_a.omega_mean, field_b.omega_mean,
96
+ field_a.R, field_b.R,
97
+ field_a.rho, field_b.rho,
98
+ density_coherence_tradeoff=density_coherence_tradeoff)
99
+ return SimilarityResult(s_loc=s_loc, s_dir=s_dir)
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: heatmatch
3
+ Version: 1.0.0
4
+ Summary: Orientation-aware visualization and comparison of very dense saccade patterns (scanpath comparison for long free viewing)
5
+ Author: Xingyu Long
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/xysLong/HeatMatch
8
+ Project-URL: Paper, https://doi.org/10.1145/3803539
9
+ Project-URL: Data, https://osf.io/f2xhj/
10
+ Keywords: eye-tracking,saccades,scanpath,scanpath-comparison,gaze,heatmap,visualization,orientation-field,empirical-aesthetics,art-perception
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering :: Visualization
15
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: Pillow
22
+ Dynamic: license-file
23
+
24
+ # HeatMatch
25
+
26
+ **Visualize and compare very dense saccade patterns from long free-viewing eye-tracking data.**
27
+
28
+ HeatMatch turns hundreds or thousands of saccades into continuous *orientation fields*: at every location it estimates saccade density, mean orientation, and how consistently the saccades agree in orientation. From these fields it draws orientation-coded heatmaps that show *where* gaze moves and *in which direction*. It also computes a similarity score for comparing saccade patterns across participants, stimuli, or conditions, without AOIs, scanpath alignment, or manual segmentation.
29
+
30
+ It was developed for 60-second viewing of paintings (empirical aesthetics, art history) and works for any static stimulus. Python implementation accompanying the paper:
31
+
32
+ <table border="0" cellspacing="0" cellpadding="0"><tr>
33
+ <td width="72%" valign="top">
34
+
35
+ <blockquote>
36
+ Xingyu Long, Jozsef Arato, Sophia Kury, Anna Miscena, and Raphael Rosenberg. 2026.<br/>
37
+ <strong>HeatMatch: Orientation-Aware Visualization and Comparison of Very Dense Saccade Patterns.</strong><br/>
38
+ <em>Proceedings of the ACM on Computer Graphics and Interactive Techniques</em> (PACMCGIT), 9(2), Article 18.<br/>
39
+ <a href="https://doi.org/10.1145/3803539">https://doi.org/10.1145/3803539</a>
40
+ </blockquote>
41
+
42
+ </td>
43
+ <td width="28%" align="center" valign="middle">
44
+ <img src="https://raw.githubusercontent.com/xysLong/HeatMatch/main/assets/logos.png" width="200" alt="CReA Lab and Vienna Cognitive Science Hub"/>
45
+ </td>
46
+ </tr></table>
47
+
48
+ ---
49
+
50
+ ## Overview
51
+
52
+ 1. **OOI-coded heatmaps** — orientation fields visualized with researcher-defined color anchors and confidence-weighted opacity.
53
+ 2. **HeatMatch similarity** — comparison based on saccade density and orientation.
54
+
55
+ ![HeatMatch heatmap example](https://raw.githubusercontent.com/xysLong/HeatMatch/main/assets/example_heatmaps.jpg)
56
+ <sub>Per-participant (columns 1–5) and aggregated (column 6) heatmaps for three paintings. Hue = mean saccade orientation relative to OOI; opacity = local confidence. Stimulus images: public domain via [Wikimedia Commons](https://commons.wikimedia.org) — see paper for details.</sub>
57
+
58
+ See the [paper](https://doi.org/10.1145/3803539) for methodology.
59
+
60
+ ### When to use it
61
+
62
+ - **Long, dense viewing.** Scanpath comparison methods such as MultiMatch or ScanMatch align scanpaths sequence by sequence. With hundreds of saccades per trial and strongly diverging paths, they become slow and hard to interpret. HeatMatch aggregates instead of aligning, so it scales to arbitrarily dense data.
63
+ - **Orientation matters.** Fixation heatmaps and density-based metrics ignore saccade direction. HeatMatch keeps orientation, so you can ask whether gaze follows horizontal, vertical, diagonal, or symmetric structure in an image, e.g. composition lines in a painting.
64
+ - **Group, stimulus, and individual comparisons.** In the paper, the similarity score recovers the abstract vs. still-life distinction and differences in within-painting agreement. It also reveals stable individual "saccadic signatures" that hold across paintings.
65
+
66
+ It is **not** a replacement for sequence-based methods. HeatMatch is permutation-invariant (it ignores saccade order) and treats orientations as unsigned (left-to-right = right-to-left). Use it alongside MultiMatch, ScanMatch, or RQA when order or direction matters.
67
+
68
+ ---
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install heatmatch
74
+ ```
75
+
76
+ Dependencies: `numpy`, `matplotlib`, `Pillow`. For development, clone the repository and run `pip install -e .`.
77
+
78
+ ---
79
+
80
+ ## Quick Start
81
+
82
+ ```python
83
+ import matplotlib.pyplot as plt
84
+ from heatmatch import make_reference_grid, make_orientation_field, Heatmap, compute_similarity
85
+
86
+ W, H = 2880, 2160
87
+ onset = ... # (J, 2) saccade start coordinates
88
+ offset = ... # (J, 2) saccade end coordinates
89
+
90
+ pts, xx, yy = make_reference_grid(W, H, grid_resolution=200)
91
+ field = make_orientation_field(pts, onset, offset, sigma=50.0, grid_shape=yy.shape)
92
+
93
+ fig, ax = plt.subplots()
94
+ Heatmap(field, W, H).draw(ax, ooi=0.0) # ooi=0 → horizontal, ooi=90 → vertical
95
+ plt.show()
96
+
97
+ # Compare two patterns (fields built on the same grid, e.g. two participants)
98
+ result = compute_similarity(field_a, field_b)
99
+ s = (result.s_loc + result.s_dir) / 2 # composite HeatMatch score S in [0, 1]
100
+ print(result.s_loc, result.s_dir, s)
101
+ ```
102
+
103
+ A full worked example is in [`demo.ipynb`](https://github.com/xysLong/HeatMatch/blob/main/demo.ipynb). The dataset is not included — download `data_anonymized.csv` from **[osf.io/f2xhj](https://osf.io/f2xhj/)** and place it in `tests/`.
104
+
105
+ ---
106
+
107
+ ## API
108
+
109
+ ### `heatmatch.fields`
110
+
111
+ | | |
112
+ |---|---|
113
+ | `make_reference_grid(w, h, grid_resolution)` | Returns `(pts, xx, yy)`; int → square grid, `(ny, nx)` tuple → rectangular |
114
+ | `make_orientation_field(pts, onset, offset, sigma, grid_shape, ...)` | Returns `OrientationField` with `omega_mean`, `R`, `rho` |
115
+
116
+ ### `heatmatch.heatmapping`
117
+
118
+ | | |
119
+ |---|---|
120
+ | `Heatmap(field, w, h, image=None)` | Caches colormap, opacity, and (optionally) grayscale background image |
121
+ | `Heatmap.draw(ax, ooi, opacity_density_weight, base_opacity, cmap, ...)` | Renders onto `ax` |
122
+
123
+ OOI in degrees: 0 = east, 90 = north, 180 = west, 270 = south. Unsigned, so `ooi` and `ooi ± 180` are equivalent.
124
+
125
+ ### `heatmatch.matching`
126
+
127
+ | | |
128
+ |---|---|
129
+ | `compute_similarity(field_a, field_b, density_coherence_tradeoff)` | Returns `SimilarityResult(s_loc, s_dir)` |
130
+
131
+ ---
132
+
133
+ ## Key Parameters
134
+
135
+ | Parameter | Default | Notes |
136
+ |---|---|---|
137
+ | `sigma` | `50.0` | Gaussian bandwidth in px. Paper evaluates {50, 100, 150}. |
138
+ | `grid_resolution` | `200` | Int or `(ny, nx)`. Paper uses 200. |
139
+ | `ooi` | `0.0` | Orientation of Interest in degrees. |
140
+ | `opacity_density_weight` | `1.0` | Density vs. coherence for opacity. Recommended 0.5–1.0. |
141
+ | `base_opacity` | `0.85` | Global opacity ceiling; useful with a background image. |
142
+ | `density_coherence_tradeoff` | `0.5` | Density vs. coherence for *S*_dir. At 1.0, angular information is discarded. |
143
+
144
+ ---
145
+
146
+ ## Citation
147
+
148
+ ```bibtex
149
+ @article{long2026heatmatch,
150
+ author = {Long, Xingyu and Arato, Jozsef and Kury, Sophia and Miscena, Anna and Rosenberg, Raphael},
151
+ title = {HeatMatch: Orientation-Aware Visualization and Comparison of Very Dense Saccade Patterns},
152
+ journal = {Proc. ACM Comput. Graph. Interact. Tech.},
153
+ year = {2026},
154
+ volume = {9},
155
+ number = {2},
156
+ articleno = {18},
157
+ doi = {10.1145/3803539},
158
+ }
159
+ ```
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ heatmatch/__init__.py
5
+ heatmatch/fields.py
6
+ heatmatch/heatmapping.py
7
+ heatmatch/matching.py
8
+ heatmatch.egg-info/PKG-INFO
9
+ heatmatch.egg-info/SOURCES.txt
10
+ heatmatch.egg-info/dependency_links.txt
11
+ heatmatch.egg-info/requires.txt
12
+ heatmatch.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ numpy
2
+ matplotlib
3
+ Pillow
@@ -0,0 +1 @@
1
+ heatmatch
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools]
6
+ packages = ["heatmatch"]
7
+
8
+ [project]
9
+ name = "heatmatch"
10
+ version = "1.0.0"
11
+ description = "Orientation-aware visualization and comparison of very dense saccade patterns (scanpath comparison for long free viewing)"
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ authors = [{name = "Xingyu Long"}]
15
+ requires-python = ">=3.9"
16
+ dependencies = [
17
+ "numpy",
18
+ "matplotlib",
19
+ "Pillow",
20
+ ]
21
+ keywords = [
22
+ "eye-tracking", "saccades", "scanpath", "scanpath-comparison",
23
+ "gaze", "heatmap", "visualization", "orientation-field",
24
+ "empirical-aesthetics", "art-perception",
25
+ ]
26
+ classifiers = [
27
+ "Programming Language :: Python :: 3",
28
+ "Operating System :: OS Independent",
29
+ "Intended Audience :: Science/Research",
30
+ "Topic :: Scientific/Engineering :: Visualization",
31
+ "Topic :: Scientific/Engineering :: Information Analysis",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/xysLong/HeatMatch"
36
+ Paper = "https://doi.org/10.1145/3803539"
37
+ Data = "https://osf.io/f2xhj/"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+