mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-20 22:00:40 +02:00
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:
commit
7e222e5b84
3 files changed
+75
-5
No files matched your search
+28
-4
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user