Debug Flipping Fixes
This commit is contained in:
@@ -22,12 +22,16 @@
|
|||||||
/* global AR, CV */
|
/* global AR, CV */
|
||||||
|
|
||||||
export const DETECT_PRESETS = {
|
export const DETECT_PRESETS = {
|
||||||
|
// maxHamming: reject markers the detector had to bit-correct. Logging showed
|
||||||
|
// every phantom id (998, 692, ...) had hamming==1 while real crests read at 0,
|
||||||
|
// so hamming==0 kills phantoms with ~no cost. Bump to 1 only if a real marker
|
||||||
|
// is genuinely hard to read (dim/worn print).
|
||||||
// crisp, fast — good light, marker fills a decent part of frame
|
// crisp, fast — good light, marker fills a decent part of frame
|
||||||
strict: { minSizeRatio: 0.04, epsilon: 0.05, threshKernel: 2, threshBias: 7, warp: 49, multiScale: false },
|
strict: { minSizeRatio: 0.04, epsilon: 0.05, threshKernel: 2, threshBias: 7, warp: 49, multiScale: false, maxHamming: 0 },
|
||||||
// the sensible default for an exhibit — noticeably more forgiving
|
// the sensible default for an exhibit — noticeably more forgiving
|
||||||
forgiving:{ minSizeRatio: 0.015, epsilon: 0.06, threshKernel: 2, threshBias: 7, warp: 49, multiScale: true },
|
forgiving:{ minSizeRatio: 0.015, epsilon: 0.06, threshKernel: 2, threshBias: 7, warp: 49, multiScale: true, maxHamming: 0 },
|
||||||
// last resort — distant/dim/awkward; costs more CPU
|
// last resort — distant/dim/awkward; costs more CPU
|
||||||
greedy: { minSizeRatio: 0.008, epsilon: 0.08, threshKernel: 3, threshBias: 9, warp: 49, multiScale: true },
|
greedy: { minSizeRatio: 0.008, epsilon: 0.08, threshKernel: 3, threshBias: 9, warp: 49, multiScale: true, maxHamming: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TunedDetector {
|
export class TunedDetector {
|
||||||
@@ -91,6 +95,12 @@ export class TunedDetector {
|
|||||||
try { extra = this._passDownscaled(imageData, dims.width, dims.height); } catch (_) { extra = []; }
|
try { extra = this._passDownscaled(imageData, dims.width, dims.height); } catch (_) { extra = []; }
|
||||||
for (const m of extra) if (!seen.has(m.id)) markers.push(m);
|
for (const m of extra) if (!seen.has(m.id)) markers.push(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject bit-corrected reads: a hamming distance above the preset threshold
|
||||||
|
// means the detector guessed at the code -> phantom ids. Real crests read at 0.
|
||||||
|
const maxH = this.preset.maxHamming ?? 0;
|
||||||
|
markers = markers.filter((m) => (m.hammingDistance ?? 0) <= maxH);
|
||||||
|
|
||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-11
@@ -77,10 +77,10 @@ function centredCorners(m, w, h) {
|
|||||||
// (right) multiply by F fixes the rotation handedness while keeping translation
|
// (right) multiply by F fixes the rotation handedness while keeping translation
|
||||||
// correct (F*M*F's translation column is F*t, i.e. flipped exactly once).
|
// correct (F*M*F's translation column is F*t, i.e. flipped exactly once).
|
||||||
const flipYZ = new THREE.Matrix4().makeScale(1, -1, -1);
|
const flipYZ = new THREE.Matrix4().makeScale(1, -1, -1);
|
||||||
function markerToCameraMatrix(m) {
|
|
||||||
const pose = posit.pose(centredCorners(m, grab.width, grab.height));
|
// Convert one POS-IT (R,t) solution into a three.js marker->camera matrix via
|
||||||
if (!pose) return null;
|
// the F*M*F conjugation (see note above).
|
||||||
const R = pose.bestRotation, t = pose.bestTranslation;
|
function poseToMatrix(R, t) {
|
||||||
const M = new THREE.Matrix4().set(
|
const M = new THREE.Matrix4().set(
|
||||||
R[0][0], R[0][1], R[0][2], t[0],
|
R[0][0], R[0][1], R[0][2], t[0],
|
||||||
R[1][0], R[1][1], R[1][2], t[1],
|
R[1][0], R[1][1], R[1][2], t[1],
|
||||||
@@ -88,8 +88,24 @@ function markerToCameraMatrix(m) {
|
|||||||
0, 0, 0, 1
|
0, 0, 0, 1
|
||||||
);
|
);
|
||||||
M.premultiply(flipYZ); // F * M
|
M.premultiply(flipYZ); // F * M
|
||||||
M.multiply(flipYZ); // F * M * F (conjugation: fixes pitch handedness)
|
M.multiply(flipYZ); // F * M * F
|
||||||
return { M, err: pose.bestError, dist: Math.hypot(t[0], t[1], t[2]) };
|
return M;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POS-IT returns TWO solutions for a planar marker (the real pose and a mirror
|
||||||
|
// twin). Up close they have near-equal error and the solver flip-flops between
|
||||||
|
// them -> "rotate 90deg, ghost spins 180deg". We return both; alignWorld picks
|
||||||
|
// the one whose resulting world orientation is closest to the previous frame.
|
||||||
|
function markerToCameraMatrix(m) {
|
||||||
|
const pose = posit.pose(centredCorners(m, grab.width, grab.height));
|
||||||
|
if (!pose) return null;
|
||||||
|
const best = { M: poseToMatrix(pose.bestRotation, pose.bestTranslation), err: pose.bestError };
|
||||||
|
let alt = null;
|
||||||
|
if (pose.alternativeRotation && pose.alternativeTranslation) {
|
||||||
|
alt = { M: poseToMatrix(pose.alternativeRotation, pose.alternativeTranslation), err: pose.alternativeError };
|
||||||
|
}
|
||||||
|
const t = pose.bestTranslation;
|
||||||
|
return { best, alt, dist: Math.hypot(t[0], t[1], t[2]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// anchor world placement -> matrix mapping the MARKER's local frame (as POS-IT
|
// anchor world placement -> matrix mapping the MARKER's local frame (as POS-IT
|
||||||
@@ -121,16 +137,49 @@ function anchorToWorldMatrix(anchor) {
|
|||||||
|
|
||||||
// Given a detected marker with a known anchor, compute world->camera and apply
|
// Given a detected marker with a known anchor, compute world->camera and apply
|
||||||
// it to the `world` group so everything in world coords renders correctly.
|
// it to the `world` group so everything in world coords renders correctly.
|
||||||
|
//
|
||||||
|
// Planar-ambiguity defeat: POS-IT gives two candidate poses. We compute the
|
||||||
|
// resulting world->camera for each, and keep whichever is closest to LAST
|
||||||
|
// frame's transform (temporal consistency). This stops the mirror-twin flip
|
||||||
|
// that made the ghost spin the wrong way. On first lock we take `best`.
|
||||||
const tmpAnchorInv = new THREE.Matrix4();
|
const tmpAnchorInv = new THREE.Matrix4();
|
||||||
|
let lastWorldToCamera = null; // previous frame's chosen transform (for continuity)
|
||||||
|
|
||||||
|
function worldToCameraFor(mMatrix, anchorInv) {
|
||||||
|
return new THREE.Matrix4().multiplyMatrices(mMatrix, anchorInv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// crude distance between two 4x4s: sum of squared element differences of the
|
||||||
|
// rotation part (enough to tell the real pose from its mirror twin).
|
||||||
|
function matDist(a, b) {
|
||||||
|
const ea = a.elements, eb = b.elements;
|
||||||
|
let s = 0;
|
||||||
|
for (const i of [0, 1, 2, 4, 5, 6, 8, 9, 10]) { const d = ea[i] - eb[i]; s += d * d; }
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
function alignWorld(m2c, anchor) {
|
function alignWorld(m2c, anchor) {
|
||||||
// camera = markerToCamera * anchorToWorld^-1 (as a world->camera transform)
|
|
||||||
// world point -> anchor-local -> camera:
|
|
||||||
// worldToCamera = markerToCamera * (anchorToWorld)^-1
|
|
||||||
const aToW = anchorToWorldMatrix(anchor);
|
const aToW = anchorToWorldMatrix(anchor);
|
||||||
tmpAnchorInv.copy(aToW).invert();
|
tmpAnchorInv.copy(aToW).invert();
|
||||||
const worldToCamera = new THREE.Matrix4().multiplyMatrices(m2c.M, tmpAnchorInv);
|
|
||||||
world.matrix.copy(worldToCamera);
|
const candBest = worldToCameraFor(m2c.best.M, tmpAnchorInv);
|
||||||
|
let chosen = candBest;
|
||||||
|
|
||||||
|
if (m2c.alt) {
|
||||||
|
const candAlt = worldToCameraFor(m2c.alt.M, tmpAnchorInv);
|
||||||
|
if (lastWorldToCamera) {
|
||||||
|
// pick the candidate closest to where the world was last frame
|
||||||
|
chosen = matDist(candAlt, lastWorldToCamera) < matDist(candBest, lastWorldToCamera)
|
||||||
|
? candAlt : candBest;
|
||||||
|
} else {
|
||||||
|
// first lock: trust the lower-error (best) solution
|
||||||
|
chosen = candBest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
world.matrix.copy(chosen);
|
||||||
world.matrixWorldNeedsUpdate = true;
|
world.matrixWorldNeedsUpdate = true;
|
||||||
|
lastWorldToCamera = chosen.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Build occluders + anchor viz once scene arrives ----------------------
|
// ---- Build occluders + anchor viz once scene arrives ----------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user