Metadata-Version: 2.5
Name: focusedgaze
Version: 0.1.0
Summary: Webcam eye-gaze tracking: face landmarks, L2CS-Net ONNX inference, per-person calibration, smoothed screen coordinates.
Project-URL: Homepage, https://github.com/muhammad-asifkhan/focusedgaze
Project-URL: Source, https://github.com/muhammad-asifkhan/focusedgaze
Project-URL: Issues, https://github.com/muhammad-asifkhan/focusedgaze/issues
Author-email: Muhammad Asif Khan <asifcalm53@gmail.com>
Maintainer-email: Muhammad Asif Khan <asifcalm53@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Muhammad Asif Khan 
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
License-File: NOTICE
Keywords: computer-vision,eye-tracking,gaze,mediapipe,onnx,webcam
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Video :: Capture
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: mediapipe<1.1,>=0.10.30
Requires-Dist: numpy<3,>=1.26
Requires-Dist: opencv-python<6,>=4.8
Requires-Dist: platformdirs<5,>=4.0
Provides-Extra: calibration
Requires-Dist: scikit-learn<2,>=1.3; extra == 'calibration'
Requires-Dist: scipy<2,>=1.11; extra == 'calibration'
Provides-Extra: cpu
Requires-Dist: onnxruntime<2,>=1.17; extra == 'cpu'
Provides-Extra: cuda
Requires-Dist: onnxruntime-gpu<2,>=1.17; extra == 'cuda'
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine>=5; extra == 'dev'
Provides-Extra: directml
Requires-Dist: onnxruntime-directml<2,>=1.17; extra == 'directml'
Provides-Extra: export
Requires-Dist: onnx>=1.15; extra == 'export'
Requires-Dist: torch>=2.0; extra == 'export'
Requires-Dist: torchvision>=0.15; extra == 'export'
Provides-Extra: intel
Requires-Dist: openvino<2027,>=2024.0; extra == 'intel'
Provides-Extra: server
Requires-Dist: websockets<17,>=12; extra == 'server'
Description-Content-Type: text/markdown

# focusedgaze

Webcam eye-gaze tracking as a Python library. Point a laptop camera at a face and get a
screen coordinate.

> **Status: the pipeline is complete (0.0.0), and it is now runnable end to end.**
> `GazeEstimator`, `WebcamGazeTracker`, the capture layer, calibration, the asset registry,
> the WebSocket server and all eight CLI commands are implemented and tested. The
> extraction reproduces the original pipeline **bit-identically** on 60 recorded frames:
> 60/60 identical, zero crop-box differences. See [MIGRATION_AUDIT.md](MIGRATION_AUDIT.md) §49.
>
> **`focusedgaze calibrate` now runs a real session**: a positioning check, a smooth-pursuit
> sweep on a full-screen canvas, per-region coverage accounting, and a robust fit. Until
> that landed, the collection loops existed but nothing drew a dot, so the command
> refused to start and the library could not produce screen coordinates for anybody.
> `focusedgaze accuracy` draws its grid for the same reason.
>
> Still 0.0.0 and not yet released as a functional package: packaging verification and the
> release are Phases 9 and 10.
>
> If something is not working, run `focusedgaze setup` first, then
> `focusedgaze check --no-camera`.
>
> **CI is green** on Python 3.12, 3.13 and 3.14. It was red for five pushes on a platform
> assumption in the asset registry, since fixed. See `MIGRATION_AUDIT.md` §42.

### Documentation

| Start here | For |
|---|---|
| [docs/getting-started.md](docs/getting-started.md) | **Start here.** Install to a working gaze-controlled app, step by step, including swapping the model. |
| [docs/what-you-need.md](docs/what-you-need.md) | **What you must supply and what you get back.** The short version. |
| [docs/complete-usage.md](docs/complete-usage.md) | The full guide to the finished product, every section status-marked. |
| [docs/usage.md](docs/usage.md) | What runs **today**, with examples that were executed. |
| [docs/wire_format.md](docs/wire_format.md) | The WebSocket contract, read off the source. |

---

## What it does

```
webcam frame
  → MediaPipe face landmarks → smoothed square face crop
  → L2CS-Net gaze model (ONNX) → (pitch, yaw)
  → per-person polynomial calibration → (x, y) in [0, 1] over the screen
  → One Euro filter → steady coordinates
```

## What you need before any of it works

Three things, and the second and third are the ones that catch people out.

**A webcam, and light.** The face has to be detectable. A muted camera or an unlit room
produces no landmarks and therefore no gaze, and the failure looks identical to a bug. This
has cost this project two recording sessions already.

**A calibration, per person.** There is no useful uncalibrated mode. The model gives you a
gaze direction in radians, and turning that into a point on your screen depends on where
your screen is, how far away you sit, and your face. A calibration is specific to one
person, one machine, and roughly one seating position. Someone else sitting down in your
chair will get bad results until they calibrate for themselves.

**The gaze model weights, which you fetch yourself.** focusedgaze will not download them.
They derive from the Gaze360 dataset, which its authors restrict to non-commercial research
use, so this project does not distribute or mirror them. This is a deliberate refusal, not
a missing feature. See [Licence](#licence) below, and read [NOTICE](NOTICE) before you use
this commercially.

## Install

```bash
pip install focusedgaze[directml]   # Windows GPU via DirectX 12
pip install focusedgaze[cuda]       # NVIDIA
pip install focusedgaze[cpu]        # anywhere
```

Pick one. The base install is deliberately provider-agnostic: focusedgaze does not choose
an ONNX execution provider for you, because the right choice depends on hardware it cannot
see. Installing the base package with no provider extra still imports cleanly, which CI
checks on every run. A missing provider is reported as a named `ProviderError` naming the
extras that fix it, never as a bare `ImportError`.

The provider is worth getting right. On the reference machine, an RTX 4060 running Windows,
the gaze model takes about 15 ms per frame through DirectML and about 104 ms on CPU. That
is the difference between roughly 30 frames per second end to end and roughly 5.

Other extras: `[calibration]` to fit a profile, `[server]` for the WebSocket bridge,
`[export]` to convert the PyTorch weights to ONNX.

Python 3.12–3.14. Tested on 3.12, 3.13 and 3.14 in CI.

## The API

Two layers. You still need the model weights and a calibration, as described above.

```python
from focusedgaze import GazeEstimator, WebcamGazeTracker, CalibrationProfile

# Pure: you supply frames. No camera, no network, testable anywhere.
est = GazeEstimator(profile=CalibrationProfile.load("alice"))
result = est.process(frame_bgr, timestamp=t)
if result.ok:
    print(result.x, result.y)

# Convenience: it owns the webcam and always gives it back.
with WebcamGazeTracker(profile="alice") as tracker:
    for result in tracker.stream():
        if result.ok:
            print(result.x, result.y)
```

`GazeEstimator` never reaches the network: a missing model raises with the command that
fetches it rather than downloading anything, which is what lets the whole pipeline run in
CI against a recorded fixture.

Smaller pieces are usable on their own. `focusedgaze.core.filters.OneEuroFilter2D` smooths
any jittery 2D signal, and `focusedgaze.core.positioning.PositioningGate` reports whether a
face is close enough, far enough and centred enough, working on MediaPipe landmarks without
the gaze model.

The pure path is the point of the design. Anything that already has frames can use this
library: a video file, another capture library, a camera shared with a hand tracker, or a
test that needs to be reproducible. Owning the webcam is the convenience layer, not the
foundation.

## Accuracy

The honest summary is that accuracy is uneven across the screen, and **which part is worst
varies between calibrations**.

The originating project's documentation reports held-out validation error around **8.9% of
screen size**, spread as roughly **3–8% across the top and centre** and **13–14% along the
bottom edge**. Our own two measured runs did not reproduce that pattern consistently: one was
worst at the bottom-right, the other was among its best there and worst at the top-left.

So the design advice is the durable part: keep small or important targets away from the edges
and corners, and give anything out there a generous hit area.

Accuracy degrades when the lighting changes, when you move closer or further than you
calibrated at, and when a different person sits down. The positioning gate exists to catch
the distance case: it enforces the 45–65 cm range the calibration was collected in.

> **Measured in centimetres, and it is a range on purpose.** Roughly **3 to 6 cm average**
> on a 34 cm-wide screen, best at the centre (1.0 cm in the better run), worst at whichever
> corner the calibration sweep covered least. Two runs by the same person on the same machine
> twenty minutes apart differed by a factor of two, and the failure pattern *inverted* between
> them, so a single number would mislead. Accuracy depends more on how well your calibration
> covered the screen than on anything else measured here. See
> [docs/accuracy.md](docs/accuracy.md) and `MIGRATION_AUDIT.md` section 50.
>
> This replaces an earlier README claim of 2.0–2.4 cm, which had no source in this repository
> and was deleted rather than repeated.

## Platform support

| Platform | Status |
|---|---|
| Windows 10/11 | Tested |
| Linux | Structurally supported, untested. CI runs the non-hardware suite only |
| macOS | Structurally supported, untested |

Camera backends and ONNX providers are abstracted, so other platforms should work. A
classifier is a claim though, and only Windows is claimed for v0.1. CI does prove the pure
core computes identical results on Linux, which is evidence the abstractions are real
rather than aspirational, but nobody has pointed a camera at it there.

## Licence

The code is MIT. See [LICENSE](LICENSE).

The model weights are not, and this matters if you are evaluating focusedgaze for a product.
The gaze model is an ONNX export of L2CS-Net trained on the **Gaze360** dataset, whose
authors state that use of the dataset and code is for non-commercial research only. Weights
trained on it are normally treated as a derived work carrying the same restriction. So:
focusedgaze does not ship them, does not mirror them, and will not download them for you.
You obtain them from the official L2CS-Net distribution and convert them locally.

This is a conservative reading of the upstream terms and not legal advice. Full detail is in
[NOTICE](NOTICE).

Author: Muhammad Asif Khan, <https://github.com/muhammad-asifkhan>
