Custom Regression
Create your own gaze prediction algorithm and register it as a first-class regression module.
When to Use a Custom Regressor
The built-in ridge, weightedRidge, and threadedRidge regressors cover the majority of use cases. Consider a custom regressor when you need:
- A completely different algorithm (e.g. neural network, SVR, polynomial regression)
- Domain-specific feature engineering beyond eye patches
- Specialized weighting or regularization strategies
- Research into alternative gaze estimation methods
The IRegressor Interface
Every regressor must satisfy the IRegressor interface, exported from @webgazer-ts/core:
import type { IRegressor, EyeFeatures, GazePrediction, Point2D } from '@webgazer-ts/core';
interface IRegressor {
/** Called once when the regressor is first set. Perform initialization here. */
initialize(): void;
/**
* Predict gaze from current eye features.
* Called every animation frame while tracking is active.
* @returns null if insufficient data to predict
*/
predict(eyeFeatures: EyeFeatures): GazePrediction | null;
/**
* Store a calibration data point.
* Called automatically on every click (and optionally mousemove).
*/
addData(eyeFeatures: EyeFeatures, screenPos: Point2D, type: 'click' | 'move'): void;
/** Serialize internal training data for persistence. */
getData(): unknown;
/** Restore serialized training data (called on page load if saveDataAcrossSessions is on). */
setData(data: unknown): void;
}Minimal Example
This regressor always predicts the center of the screen — useful as a skeleton to build on:
import type { IRegressor, EyeFeatures, GazePrediction, Point2D } from '@webgazer-ts/core';
class CenterRegressor implements IRegressor {
initialize(): void {
// Nothing to set up
}
predict(_eyeFeatures: EyeFeatures): GazePrediction | null {
return {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
};
}
addData(_eyes: EyeFeatures, _screen: Point2D, _type: string): void {
// Not using calibration data
}
getData(): unknown { return null; }
setData(_data: unknown): void {}
}
// Register and activate
webgazer.addRegressionModule('center', CenterRegressor);
webgazer.setRegression('center');Full Example — Moving Average Regressor
This regressor collects calibration clicks and predicts gaze as the weighted average of the most recent click positions (no actual eye features used — purely for demonstration):
import webgazer from '@webgazer-ts/core';
import type { IRegressor, EyeFeatures, GazePrediction, Point2D } from '@webgazer-ts/core';
interface ClickRecord {
x: number;
y: number;
timestamp: number;
}
class MovingAverageRegressor implements IRegressor {
private clicks: ClickRecord[] = [];
private readonly windowMs = 5000; // Use last 5 seconds of clicks
initialize(): void {
this.clicks = [];
}
predict(_eyeFeatures: EyeFeatures): GazePrediction | null {
if (this.clicks.length === 0) return null;
const now = Date.now();
const recent = this.clicks.filter(c => now - c.timestamp < this.windowMs);
if (recent.length === 0) return null;
const x = recent.reduce((sum, c) => sum + c.x, 0) / recent.length;
const y = recent.reduce((sum, c) => sum + c.y, 0) / recent.length;
return { x: Math.round(x), y: Math.round(y) };
}
addData(_eyes: EyeFeatures, screen: Point2D, type: string): void {
if (type === 'click') {
this.clicks.push({ x: screen.x, y: screen.y, timestamp: Date.now() });
// Keep last 200 clicks
if (this.clicks.length > 200) {
this.clicks.shift();
}
}
}
getData(): unknown {
return { clicks: this.clicks };
}
setData(data: unknown): void {
const d = data as { clicks?: ClickRecord[] };
this.clicks = d?.clicks ?? [];
}
}
// Register globally (available for all setRegression calls)
webgazer.addRegressionModule('movingAverage', MovingAverageRegressor);
// Activate
await webgazer
.setRegression('movingAverage')
.begin();Using the Built-in Regressor Base Class
For regressors that do use eye features, extend the abstract Regressor base class. It provides the DataWindow circular buffers, Kalman filter wiring, and getData/setData already implemented:
import { Regressor } from '@webgazer-ts/core/regressors/base/Regressor';
import type { EyeFeatures, GazePrediction } from '@webgazer-ts/core';
class MyRidgeRegressor extends Regressor {
public readonly name = 'myRidge';
constructor() {
super({
ridgeParameter: 1e-5,
dataWindowSize: 500,
trailDataWindowSize: 20,
trailTimeWindow: 1000,
useKalmanFilter: true,
});
}
public initialize(): void {
// Set up your Kalman filter here
this.setState(RegressorState.Ready);
}
public predict(eyeFeatures: EyeFeatures): GazePrediction | null {
// Use this.eyeFeaturesClicks, this.screenXClicksArray, this.screenYClicksArray
// Run your algorithm here
// Call this.applyKalmanFilter(prediction) for smoothing
return null;
}
}Tip: The
Regressorbase class is exported as a deep import. This API may evolve — pin your package version if using internal classes.
Registering as a Static Module
For library authors, you can add modules before any instance is created:
import { Webgazer } from '@webgazer-ts/core';
// Static registration — no instance needed
Webgazer.addRegressionModule('myRidge', MyRidgeRegressor);For end users or runtime registration, the instance method is equivalent:
import webgazer from '@webgazer-ts/core';
webgazer.addRegressionModule('myRidge', MyRidgeRegressor);
webgazer.setRegression('myRidge');Switching Regressors at Runtime
You can switch regressors even while tracking is active. The new regressor will be initialized immediately and will start collecting its own training data from subsequent clicks:
await webgazer.begin(); // Starts with 'ridge' by default
// After some calibration...
webgazer.setRegression('weightedRidge'); // Switch mid-sessionNote: When switching regressors, the new regressor starts without the previous one's training data. Call
webgazer.clearData()first if you want a clean slate, or implement your owngetData()/setData()to transfer state.
Tips for Better Accuracy
- Bias toward click events —
addDatais called for both'click'and'move'events. Click events are more intentional; many regressors weight them higher. - Keep the data window small — Too many old data points can hurt accuracy as the user's position shifts. A window of 300–500 is a good starting point.
- Normalize features — Eye patch pixel values can vary widely with lighting. Normalize to zero mean and unit variance before regression.
- Add a Kalman filter — Raw regression output is noisy. The
KalmanFilterandKalmanFilter4Dclasses are exported from@webgazer-ts/corefor you to use directly.