mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
frame-eyes, a separate project until now, becomes gaze/tracker:
- ft-eyegrab (was fe-bufprobe) copies the eye-camera frames, read-only, out of
SteamVR's eyetracking process. It runs as the system service
frametop-eyegrab.service, which gaze/tracker/install.sh installs to
/etc/frametop with sudo. It keeps only CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, and
CAP_CHOWN, and copies frames only while /dev/shm/frametop-eyes-want is fresh,
holding none of the tracker's buffers otherwise.
- ft-eyes (was fe-trackd) runs under ft-gazed in the dev container, with
build/venv's pinned numpy and OpenCV: while Eye tracker is Own tracker, or on
the probe's lease ("eyes SECONDS"). No sudo password or fe-live script at
run time any more.
- lab/ holds the research tools (ft-eyes-score, -e2e, -record, -replay,
-session) and findings.md. Recordings live outside the repo, in
~/.local/share/frametop/eyes/captures; .gitignore catches stray frame dumps.
Its socket is now @ft_eyes, its output /dev/shm/frametop-eyes-gaze, and its state
~/.local/state/frametop/gaze/eyes. On practice1 -> practice2 the whole live path
(ft-eyes-e2e) gives 1.30 deg median and 3.18 for the worst tenth, as before the
move (1.30, 3.21).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 lines
1.4 KiB
Python
42 lines
1.4 KiB
Python
"""Per-frame pupil ellipses through a recording, cached in the capture (numbers only).
|
|
|
|
ellipses(cap) -> {camera: array of rows (t, x, y, a, b, major, fill, glint mid x, y)}, every
|
|
EVERY-th frame of each camera; the glint midpoint is NaN when the pair isn't seen.
|
|
"""
|
|
import numpy as np
|
|
|
|
import eyes_lab # noqa: F401 (puts gaze/tracker on the path)
|
|
import eyes_pupil
|
|
|
|
EVERY = 2
|
|
VERSION = 2
|
|
|
|
|
|
def load_index(cap):
|
|
return np.array([[float(v) for v in l.split()]
|
|
for l in (cap / "index.txt").read_text().splitlines() if len(l.split()) == 4])
|
|
|
|
|
|
def ellipses(cap):
|
|
cache = cap / f"ellipses-v{VERSION}.npz"
|
|
if cache.exists():
|
|
z = np.load(cache)
|
|
return {0: z["cam0"], 1: z["cam1"]}
|
|
idx = load_index(cap)
|
|
frames = np.memmap(cap / "frames.raw", dtype=np.uint8, mode="r").reshape(-1, 400, 512)
|
|
idx = idx[:len(frames)]
|
|
out = {}
|
|
for c in (0, 1):
|
|
rows, prev = [], None
|
|
for i in np.where(idx[:, 2] == c)[0][::EVERY]:
|
|
p = eyes_pupil.find_pupil(frames[i], prev)
|
|
prev = p
|
|
if p is None:
|
|
continue
|
|
pair = eyes_pupil.glint_pair(p)
|
|
mx, my = ((pair[0][0] + pair[1][0]) / 2, (pair[0][1] + pair[1][1]) / 2) if pair else (np.nan, np.nan)
|
|
rows.append((idx[i, 3], p["x"], p["y"], p["a"], p["b"], p["major"], p["fill"], mx, my))
|
|
out[c] = np.array(rows).reshape(-1, 9)
|
|
np.savez(cache, cam0=out[0], cam1=out[1])
|
|
return out
|