Stampe (1993) gaze-mapping polynomial
Per-eye Stampe (1993) gaze-mapping polynomial in Python, generalised to arbitrary degree.
This package fits a polynomial without cross terms (1, x, y, x², y², ..., xᵈ, yᵈ) plus an optional per-quadrant corner correction that supplies the missing xy cross-term coefficient on outer points.
| degree | polynomial terms | min inner points | maps to |
|---|---|---|---|
d = 1 |
1, x, y |
3 | HV3 (bilinear) |
d = 2 |
1, x, y, x², y² |
5 | HV5, HV9 — Stampe 1993 canonical |
d = 3 |
1, x, y, x², y², x³, y³ |
7 | HV13-style extension |
Corner correction is independent of d: pass exactly 4 outer points (one per quadrant relative to the centroid) and the model adds one xy coefficient per quadrant. With no outer points, the polynomial alone is used.
The staged inner/outer fit matches what the EyeLink 1000 Plus does at calibration and has been verified against EyeLink HREF P-CR data.
Reference: Stampe, D. M. (1993). "Heuristic filtering and reliable calibration methods for video-based pupil-tracking systems." Behavior Research Methods, 25(2), 137-142.
Stampe's published mapping function (paper eq., p. 141, HV9 case):
x₁ = a + b·x + c·y + d·x² + e·y²
y₁ = f + g·x + h·y + i·x² + j·y²
X = x₁ + m[q]·x₁·y₁
Y = y₁ + n[q]·x₁·y₁
where (x, y) is the tracker feature, (X, Y) is the screen coordinate, and q is the quadrant into which (x₁, y₁) falls.
Installation
From PyPI:
pip install stampe1993-gaze-mapping
With uv:
uv add stampe1993-gaze-mapping
For a local checkout, install editable:
pip install -e .
# or
uv add --editable .
Quick start
import numpy as np
from stampe1993_gaze_mapping import StampeModel, angular_error
# HV9: 5 inner + 4 outer corner targets
inner_pcr = np.array([...]) # (5, 2) P-CR features at centre + 4 edges
inner_targets = np.array([...]) # (5, 2) screen-pixel targets
outer_pcr = np.array([...]) # (4, 2) P-CR features at the 4 corners
outer_targets = np.array([...]) # (4, 2) screen targets at the 4 corners
model = StampeModel(degree=2)
model.fit(inner_pcr, inner_targets, outer_pcr, outer_targets)
# Predict screen coordinates from a new (or training) P-CR vector
predicted_xy = model.predict(inner_pcr)
# Pixel error → degrees of visual angle (per-axis signed + magnitude)
err_px = predicted_xy - inner_targets
deg_x, deg_y, mag_deg = angular_error(
err_px,
pitch_x_mm=screen_width_mm / screen_res_x,
pitch_y_mm=screen_height_mm / screen_res_y,
viewing_dist_mm=eye_to_screen_mm,
)
print(f"calibration residual: mean {mag_deg.mean():.3f}° max {mag_deg.max():.3f}°")
For HV5 (no corners) skip the outer arguments — outer_* defaults to None:
model = StampeModel(degree=2)
model.fit(inner5_pcr, inner5_targets)
For HV3 (bilinear):
model = StampeModel(degree=1)
model.fit(inner3_pcr, inner3_targets)
For HV13 (bicubic), either use the model as a pure polynomial fitter on all 13 points, or split 9 inner + 4 outer for Stampe-style correction. The caller decides the split:
# pure polynomial: all 13 as inner, no corners
model = StampeModel(degree=3)
model.fit(all13_pcr, all13_targets)
# Stampe-style: caller picks which 9 are inner and which 4 are outer
model.fit(inner9_pcr, inner9_targets, corners4_pcr, corners4_targets)
Cross terms above xy (x²y, xy², etc.) are not modelled in any configuration. The Stampe-style polynomial drops them and corner correction only puts back xy per quadrant. If you need a full bicubic with all cross terms, use a different fitter.
The fitted model is fully described by these attributes after fit:
model.degree— polynomial degree.model.coef_x— shape(1 + 2*degree,), polynomial coefficients for the X axis.model.coef_y— shape(1 + 2*degree,), same for Y.model.centroid— shape(2,), the screen-coord centroid of the calibration targets (used as the quadrant origin).model.corner— shape(4, 2), the per-quadrant(cx, cy)corner-correction coefficients. Quadrants are indexed(top-left, top-right, bottom-left, bottom-right)in image-y-down coordinates. All zeros if no outer points were supplied.
API
| call | what it does |
|---|---|
StampeModel(degree=2) |
Unfitted model. degree=1 bilinear, degree=2 biquadratic (Stampe default), degree=3 bicubic. Raises ValueError if degree < 1. |
model.fit(inner_pcr, inner_targets, outer_pcr=None, outer_targets=None) |
Fits polynomial on inner; per-quadrant corner correction if outer_* given. All arrays (N, 2). |
model.predict(pcr_xy) |
Evaluate on (2,) or (N, 2) P-CR features; same shape out. Raises RuntimeError if called before fit. |
angular_error(err_px, pitch_x_mm, pitch_y_mm, viewing_dist_mm) |
Convert (N, 2) pixel-error to (deg_x, deg_y, mag_deg). Lives in stampe1993_gaze_mapping.angular, re-exported from package root. |
fit raises ValueError on: shape mismatch, fewer than 1 + 2*degree inner points, asymmetric outer (one None, one not), outer_* length ≠ 4, outer points not spanning all 4 quadrants around the centroid, or an outer-predicted point on a cardinal axis through the centroid.
Internals (also exported): design_row(pcr, degree=2) returns the [1, x, y, x², y², ..., xᵈ, yᵈ] feature row for one P-CR vector. quadrant_code(dx, dy) returns 0..3 from the sign of (dx, dy), image-y-down.
Acknowledgments
This project has received funding from the European Union's Horizon Europe research and innovation funding program under grant agreement No 101072410, Eyes4ICU project.
Metadata
Release files for stampe1993-gaze-mapping 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| stampe1993_gaze_mapping-1.0.1.tar.gz | 425.1 kB | Without provenance | Details for stampe1993_gaze_mapping-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| stampe1993_gaze_mapping-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for stampe1993_gaze_mapping-1.0.1-py3-none-any.whl |
Total release size: 434.4 kB
Release files / stampe1993_gaze_mapping-1.0.1.tar.gz
| Download URL | stampe1993_gaze_mapping-1.0.1.tar.gz |
|---|---|
| Size | 425.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
21a9fee83c3bc8d2005a76770c97f89444ff0d7c01d6b42afd5a91cc5f45079e
|
|
BLAKE2b-256 checksum How to use checksums |
f5f633862b5f1f1d45a58400aae42293ba2b98a2cad3b04009941ac15046129d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / stampe1993_gaze_mapping-1.0.1-py3-none-any.whl
| Download URL | stampe1993_gaze_mapping-1.0.1-py3-none-any.whl |
|---|---|
| Size | 9.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
81566ae2ea4efd38cdb430db6a6454cdad1789aff2c3d371508bb421f4fd4a00
|
|
BLAKE2b-256 checksum How to use checksums |
55816d9002fde41d60d4bb791847e3963f3e4f9e57203b91c11a9ed9bbaca655
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Security for stampe1993-gaze-mapping 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.