Stabilize multi-marker fusion (deterministic order + coarse-bucket weights) + expose inter-marker spread

This commit is contained in:
2026-08-04 11:31:27 +10:00
parent 80bae56948
commit bb049f684c
+27 -4
View File
@@ -70,11 +70,26 @@ export class WorldFuser {
const pos = new THREE.Vector3();
const quat = new THREE.Quaternion();
camWorld.decompose(pos, quat, _s);
cams.push({ pos, quat, w: Math.max(1, e.area) });
cams.push({ markerId: e.markerId, pos, quat, w: Math.max(1, e.area) });
}
if (!cams.length) return null;
// weighted position mean + incremental weighted slerp for orientation
/* Multi-marker fusion. Two visible markers each give an independent camera-in-world
* estimate; if they disagree (pose noise, or real-table spacing not matching the
* editor), naive area-weighting oscillates frame to frame because the areas jitter
* — this is the "steady on one marker, jittery on two" symptom. Fixes:
* 1. Deterministic order (sort by markerId) so the slerp base can't flip.
* 2. Robust weights: quantise area into coarse buckets so tiny per-frame area
* wobble doesn't shift the blend; a marker only dominates when genuinely much
* closer. Equal-ish markers then average to a STABLE midpoint, not a moving one. */
cams.sort((a, b) => a.markerId - b.markerId);
// Quantise confidence into coarse buckets from marker size (sqrt(area) ~ linear
// size). Near-equal markers land in the same bucket => equal weight => stable
// midpoint. A marker only outweighs another when it's a full bucket closer, so
// per-frame area jitter no longer shifts the blend.
for (const c of cams) c.w = Math.max(1, Math.round(Math.sqrt(c.w) / 8));
// weighted position mean + incremental weighted slerp (now order-stable)
let wSum = cams[0].w;
const pos = cams[0].pos.clone().multiplyScalar(cams[0].w);
const quat = cams[0].quat.clone();
@@ -89,6 +104,14 @@ export class WorldFuser {
}
pos.multiplyScalar(1 / wSum);
// spread = how far apart the individual marker estimates are (0 for one marker).
// Large spread (>~2cm) means the markers disagree — usually real-table spacing
// not matching the editor, which no fusion can fully hide; surfaced in dev HUD.
let spread = 0;
for (let i = 0; i < cams.length; i++)
for (let j = i + 1; j < cams.length; j++)
spread = Math.max(spread, cams[i].pos.distanceTo(cams[j].pos));
// ---- 1€ filter (position + orientation) ----
const now = performance.now();
const gap = now - this.lastFuseT;
@@ -100,7 +123,7 @@ export class WorldFuser {
if (!this.smoothPos || gap > 1500) {
this.smoothPos = pos.clone(); oe.prevPos = pos.clone(); oe.dPos.set(0, 0, 0);
this.smoothQuat = quat.clone(); oe.prevQuat = quat.clone(); oe.dAngRate = 0;
return { position: this.smoothPos.clone(), quaternion: this.smoothQuat.clone(), markerCount: cams.length };
return { position: this.smoothPos.clone(), quaternion: this.smoothQuat.clone(), markerCount: cams.length, spread };
}
// POSITION: derivative -> speed -> dynamic cutoff -> low-pass
@@ -123,6 +146,6 @@ export class WorldFuser {
this.smoothQuat.slerp(quat, aA);
oe.prevQuat.copy(quat);
return { position: this.smoothPos.clone(), quaternion: this.smoothQuat.clone(), markerCount: cams.length };
return { position: this.smoothPos.clone(), quaternion: this.smoothQuat.clone(), markerCount: cams.length, spread };
}
}