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>
72 lines
2.5 KiB
Python
Executable File
72 lines
2.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""ft-eyes-replay: play a recording into the shared-frame layout, as ft-eyegrab --share
|
|
would, at the recorded pace, to test ft-eyes without the headset.
|
|
|
|
Usage: lab/py lab/ft-eyes-replay NAME [PATH] [--from S] [--for S]
|
|
NAME is a recording (a bare name is in eyes_lab.CAPTURES). PATH defaults to
|
|
/dev/shm/frametop-eyes-cams-replay; run ft-eyes with FT_EYES_CAMS=PATH (and FT_EYES_GAZE=...
|
|
so it doesn't overwrite the live output).
|
|
"""
|
|
import mmap
|
|
import os
|
|
import struct
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from eyes_lab import capture # noqa: E402
|
|
|
|
W, H, SLOTS = 512, 400, 8
|
|
ESIZE = 64 + W * H
|
|
|
|
|
|
def main(argv):
|
|
cap = capture(argv[0])
|
|
path = argv[1] if len(argv) > 1 and not argv[1].startswith("--") else "/dev/shm/frametop-eyes-cams-replay"
|
|
start = float(argv[argv.index("--from") + 1]) if "--from" in argv else 0.0
|
|
length = float(argv[argv.index("--for") + 1]) if "--for" in argv else 1e9
|
|
idx = np.array([[float(v) for v in l.split()] for l in (cap / "index.txt").read_text().splitlines()
|
|
if len(l.split()) == 4])
|
|
frames = np.memmap(cap / "frames.raw", dtype=np.uint8, mode="r").reshape(-1, H, W)
|
|
size = 64 + 2 * SLOTS * ESIZE
|
|
if os.path.exists(path):
|
|
os.unlink(path)
|
|
fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
|
|
os.ftruncate(fd, size)
|
|
mm = mmap.mmap(fd, size)
|
|
os.close(fd)
|
|
struct.pack_into("<6I2QI", mm, 0, 0x31434546, 1, W, H, SLOTS, ESIZE, 0, 0, os.getpid())
|
|
count = [0, 0]
|
|
t0 = idx[0, 3] + start
|
|
wall0 = time.monotonic()
|
|
try:
|
|
for i in range(len(frames)):
|
|
t = idx[i, 3]
|
|
if t < t0:
|
|
continue
|
|
if t - t0 > length:
|
|
break
|
|
delay = (t - t0) - (time.monotonic() - wall0)
|
|
if delay > 0:
|
|
time.sleep(delay)
|
|
cam = int(idx[i, 2])
|
|
n = count[cam]
|
|
off = 64 + (cam * SLOTS + n % SLOTS) * ESIZE
|
|
seq = struct.unpack_from("<Q", mm, off)[0]
|
|
struct.pack_into("<Q", mm, off, seq + 1)
|
|
mm[off + 64:off + 64 + W * H] = frames[i].tobytes()
|
|
struct.pack_into("<dQII", mm, off + 8, t, n, cam, int(idx[i, 1]))
|
|
struct.pack_into("<Q", mm, off, seq + 2)
|
|
count[cam] = n + 1
|
|
struct.pack_into("<Q", mm, 24 + 8 * cam, n + 1)
|
|
finally:
|
|
os.unlink(path)
|
|
print(f"replayed {sum(count)} frames")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|