Merge one-eye: a new calibration of our tracker starts the eye weights over

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-18 21:21:15 -06:00
commit 7e222e5b84
3 files changed
+75 -5

No files matched your search

+28 -4
View File
@@ -10,9 +10,10 @@ again when the file changes:
grabber, and ft-eyes' Python in this checkout), else SteamVR's; it
switches when ours is installed or removed.
GAZE_EYE=auto|left|right the eye bias (gazecal.EyeWeights): auto weights each eye by how far
off it was at your recent nudges; left or right counts that eye twice
as much as the other. Either eye alone carries the gaze when the
other isn't seen.
off it was at your recent nudges (since our tracker's last
calibration: a new one starts them over); left or right counts that
eye twice as much as the other. Either eye alone carries the gaze
when the other isn't seen.
GAZE_FUSION=off|on with our tracker: SteamVR's gaze too (gazecal.Fusion, fusion.json).
SteamVR's two eyes' mean, corrected by an affine fitted at our
calibration's dots, is mixed half and half with ours where both our
@@ -272,6 +273,7 @@ class Service:
self.cal_mtime = None
self.lives = {name: PointerLessons() for name in SOURCES}
self.weights = {t: EyeWeights(self.bias) for t in TRACKERS}
self.own_made = None # when our tracker's calibration was made, as its misses knew it
self.dirty = False
self.fusion_on = read_fusion()
self.fusion, self.fusion_mtime = None, None
@@ -437,12 +439,14 @@ class Service:
for t, misses in (d.get("misses") or {}).items():
if t in self.weights:
self.weights[t] = EyeWeights(self.bias, misses)
self.own_made = d.get("own_made")
log(", ".join(f"{n} {len(self.lives[n].samples)}" for n in (self.source,) + SIDES) + " lessons")
def save_lessons(self):
tmp = LESSONS.with_suffix(".tmp")
tmp.write_text(json.dumps({"version": 2, "sources": {n: lv.samples for n, lv in self.lives.items() if lv.samples},
"misses": {t: w.misses for t, w in self.weights.items()}}))
"misses": {t: w.misses for t, w in self.weights.items()},
"own_made": self.own_made}))
tmp.replace(LESSONS)
self.dirty = False
@@ -996,9 +1000,29 @@ class Service:
self.own, self.own_at = json.loads(data), time.monotonic()
except ValueError:
pass
else:
self.note_own_calibration()
else:
log(f"our tracker: {data}")
def note_own_calibration(self):
"""Our tracker's status names a calibration other than the one its eye misses were
measured on: they start over, and the eyes count alike until each has EyeWeights.MIN
again. The misses say how far each eye's old mapping was off, and a new calibration
replaces both mappings (it can drop an eye, or be someone else's). Live 2026-10-18 a
calibration's own dots fed the right eye misses of the right-only one before it, and the
balance after it was that one's. Whatever made the calibration: the panel, the probe."""
made = (self.own.get("calibration") or {}).get("made")
if not made or made == self.own_made:
return
w = self.weights["own"]
if self.own_made is not None and any(w.misses):
log(f"our tracker's calibration from {made}: the eye weights start over "
f"(were {', '.join(f'{v:.2f}' for v in w.weights())} from {', '.join(str(len(m)) for m in w.misses)} misses)")
self.weights["own"] = EyeWeights(self.bias)
self.own_made = made
self.dirty = True
def status(self):
kind = self.kind
tracker = "own" if kind == "own" else "steam"
+4 -1
View File
@@ -675,7 +675,10 @@ class EyeWeights:
alike until then. Each miss is measured before its lesson teaches anything, so each is a
fresh test. On SteamVR's own test (2026-09-29) its calibration dots said the left eye was
the better one and new spots said the right, so the misses come from lessons, not the fit.
An eye that isn't seen (None) drops out, and the other carries the gaze alone."""
An eye that isn't seen (None) drops out, and the other carries the gaze alone. Our
tracker's start over with each new calibration of it (ft-gazed's note_own_calibration).
Not from the new fit's per-eye spread: on 2026-10-18's replays (frame-eyes
one_eye_replay.py) spread and equal weights came out the same, 2.29 degrees median."""
LEAN = 2.0
KEEP = 20
+43
View File
@@ -206,6 +206,49 @@ check("ft-gazed, our tracker: both eyes, then the left lost: no jump",
(svc.sent[-2], abs(svc.sent[-1][0] - 5.0) < 0.05), ((5.0, 2.0), True))
check("...counted as one eye", svc.counts["one_eye"], 1)
# --- a new calibration starts the eye weights over (ft-gazed's note_own_calibration) ---
logs = []
gazed.log = lambda msg: logs.append(msg)
svc = service()
svc.bias, svc.own_made, svc.dirty = "auto", None, False
svc.own = {"calibration": {"made": "2026-10-18 20:58:26", "dots": 27}}
svc.note_own_calibration()
check("the first calibration it hears of: adopted, nothing dropped", (svc.own_made, logs), ("2026-10-18 20:58:26", []))
for _ in range(6):
svc.weights["own"].add([2.0, 0.5]) # the left eye's mapping was the worse one
before = svc.weights["own"].weights()
check("misses on that calibration lean toward the right eye", before[1] > 0.9, True)
svc.note_own_calibration()
check("the same calibration again: kept", svc.weights["own"].weights(), before)
svc.own = {"calibration": {"made": "2026-10-18 21:11:33", "dots": 26}}
svc.note_own_calibration()
check("a new calibration: the misses start over", svc.weights["own"].misses, [[], []])
check("...the eyes count alike until each has MIN again", tuple(svc.weights["own"].weights()), (0.5, 0.5))
check("...said in the log", sum("eye weights start over" in m for m in logs), 1)
check("...and saved with the stamp", (svc.own_made, svc.dirty), ("2026-10-18 21:11:33", True))
svc.own = {}
svc.note_own_calibration()
check("a status without a calibration changes nothing", svc.own_made, "2026-10-18 21:11:33")
# The stamp lives with the misses, so a restart keeps them and a calibration made meanwhile doesn't.
svc.lives = {name: gazed.PointerLessons() for name in gazed.SOURCES}
svc.weights["own"].add([1.0, 1.0])
gazed.STATE.mkdir(parents=True, exist_ok=True)
svc.save_lessons()
again = service()
again.bias, again.lives = "auto", {name: gazed.PointerLessons() for name in gazed.SOURCES}
again.source = "mmap1"
again.load_lessons()
check("after a restart: the stamp and the misses", (again.own_made, again.weights["own"].misses),
("2026-10-18 21:11:33", [[1.0], [1.0]]))
again.own = {"calibration": {"made": "2026-10-18 21:11:33"}}
again.note_own_calibration()
check("...the same calibration: kept", again.weights["own"].misses, [[1.0], [1.0]])
again.own = {"calibration": {"made": "2026-10-18 21:30:00"}}
again.note_own_calibration()
check("...one made while it was down: started over", again.weights["own"].misses, [[], []])
shutil.rmtree(HOME, ignore_errors=True)
print("FAILED: " + ", ".join(failures) if failures else "all passed", flush=True)
sys.exit(1 if failures else 0)