Skip to main content

Stampe (1993) gaze-mapping polynomial

PyPI version Downloads License DOI

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

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)

Source distribution for stampe1993-gaze-mapping 1.0.1
File Size Uploaded
stampe1993_gaze_mapping-1.0.1.tar.gz 425.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for stampe1993-gaze-mapping 1.0.1
File Interpreter ABI Platform
stampe1993_gaze_mapping-1.0.1-py3-none-any.whl Python 3 none any Details

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

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page