Skip to main content

Stampe (1993) gaze-mapping polynomial: per-eye 5-term biquadratic + per-quadrant corner correction, staged-fit variant matching EyeLink's stored coefficients.

Project description

Stampe (1993) gaze-mapping polynomial

PyPI version Downloads License

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.

Funded by EU Eyes4ICU

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stampe1993_gaze_mapping-1.0.0.tar.gz (424.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stampe1993_gaze_mapping-1.0.0-py3-none-any.whl (9.2 kB view details)

Uploaded Python 3

File details

Details for the file stampe1993_gaze_mapping-1.0.0.tar.gz.

File metadata

  • Download URL: stampe1993_gaze_mapping-1.0.0.tar.gz
  • Upload date:
  • Size: 424.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for stampe1993_gaze_mapping-1.0.0.tar.gz
Algorithm Hash digest
SHA256 86d0b7732351e1e15711d2c74be3417a9c96e40f0d6fdd00cb28348abc64e8ea
MD5 90315084612b360a14d37ea7c9e51863
BLAKE2b-256 4a33912b37e7c6ffd5cb8060f10dec347cf5a48632746048aad3a1ac23cc0e73

See more details on using hashes here.

File details

Details for the file stampe1993_gaze_mapping-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for stampe1993_gaze_mapping-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2b9b6cf11e8776ca990d840b96fada61fcadb0877fd97ae9c63d6193b07a70a4
MD5 8e2ff42e4d13ad08031220301a60dd60
BLAKE2b-256 a5ec34c2d9253df3bc32bacd1299ac9e9468ac960c7a93c8e62e7f302ba3d822

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page