AR: sub-pixel corner refinement, ROI-first detection, anchor coplanarity check
This commit is contained in:
1 parent
7947eb61a0
commit
7fc2b02e6a
13 files changed
+1160
-10
No files matched your search
@@ -0,0 +1,107 @@
|
||||
/* geom.js — is this layout's crest geometry actually solvable without ambiguity?
|
||||
*
|
||||
* A coplanar set of markers admits two camera poses that reproject almost
|
||||
* identically: the true one and a mirrored twin. cv-worker.js suppresses the
|
||||
* twin with physics gates (camera above the table, no teleporting), but those
|
||||
* are a safety net over a geometry problem. The cure is a crest that does not
|
||||
* share the plane of the others — the A3 test rig does this with a 160mm tower
|
||||
* carrying crests at 10cm and 16cm.
|
||||
*
|
||||
* The thing is, "the anchors are coplanar" is invisible. A layout saved from the
|
||||
* editor, or a tower knocked over and re-entered as flat, looks completely
|
||||
* normal in the JSON and only shows up later as ghosts intermittently flipping
|
||||
* to the wrong side of the table — the most expensive bug in this project to
|
||||
* date. So: measure it once when the scene loads, and put the number on screen.
|
||||
*
|
||||
* The measure is the RMS distance of every anchor CORNER from the best-fit plane
|
||||
* through all of them. Corners rather than anchor centres, because a crest
|
||||
* mounted vertically among flat ones contributes out-of-plane spread through its
|
||||
* own extent even if its centre happens to sit near the common plane.
|
||||
*/
|
||||
|
||||
/* Eigen-decomposition of a symmetric 3x3 by cyclic Jacobi. Small, exact enough,
|
||||
* and no dependency — three.js has no symmetric eigensolver. */
|
||||
function jacobiEigen(m) {
|
||||
const a = [[m[0], m[1], m[2]], [m[1], m[3], m[4]], [m[2], m[4], m[5]]];
|
||||
const v = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
|
||||
for (let sweep = 0; sweep < 24; sweep++) {
|
||||
let off = 0;
|
||||
for (let p = 0; p < 2; p++) for (let q = p + 1; q < 3; q++) off += a[p][q] * a[p][q];
|
||||
if (off < 1e-20) break;
|
||||
for (let p = 0; p < 2; p++) {
|
||||
for (let q = p + 1; q < 3; q++) {
|
||||
if (Math.abs(a[p][q]) < 1e-18) continue;
|
||||
const theta = (a[q][q] - a[p][p]) / (2 * a[p][q]);
|
||||
const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
|
||||
const c = 1 / Math.sqrt(t * t + 1), s = t * c;
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const akp = a[k][p], akq = a[k][q];
|
||||
a[k][p] = c * akp - s * akq;
|
||||
a[k][q] = s * akp + c * akq;
|
||||
}
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const apk = a[p][k], aqk = a[q][k];
|
||||
a[p][k] = c * apk - s * aqk;
|
||||
a[q][k] = s * apk + c * aqk;
|
||||
}
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const vkp = v[k][p], vkq = v[k][q];
|
||||
v[k][p] = c * vkp - s * vkq;
|
||||
v[k][q] = s * vkp + c * vkq;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { values: [a[0][0], a[1][1], a[2][2]], vectors: v };
|
||||
}
|
||||
|
||||
/* points: flat [x,y,z, x,y,z, ...] in world cm.
|
||||
* Returns null if there is nothing meaningful to measure. */
|
||||
export function planarity(points) {
|
||||
const n = points.length / 3;
|
||||
if (n < 3) return null;
|
||||
|
||||
let cx = 0, cy = 0, cz = 0;
|
||||
for (let i = 0; i < n; i++) { cx += points[3 * i]; cy += points[3 * i + 1]; cz += points[3 * i + 2]; }
|
||||
cx /= n; cy /= n; cz /= n;
|
||||
|
||||
let xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const dx = points[3 * i] - cx, dy = points[3 * i + 1] - cy, dz = points[3 * i + 2] - cz;
|
||||
xx += dx * dx; xy += dx * dy; xz += dx * dz;
|
||||
yy += dy * dy; yz += dy * dz; zz += dz * dz;
|
||||
}
|
||||
const { values, vectors } = jacobiEigen([xx, xy, xz, yy, yz, zz]);
|
||||
|
||||
// smallest eigenvalue -> normal of the best-fit plane
|
||||
let k = 0;
|
||||
if (values[1] < values[k]) k = 1;
|
||||
if (values[2] < values[k]) k = 2;
|
||||
const nx = vectors[0][k], ny = vectors[1][k], nz = vectors[2][k];
|
||||
|
||||
let s2 = 0, mx = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = (points[3 * i] - cx) * nx + (points[3 * i + 1] - cy) * ny + (points[3 * i + 2] - cz) * nz;
|
||||
s2 += d * d;
|
||||
if (Math.abs(d) > mx) mx = Math.abs(d);
|
||||
}
|
||||
return {
|
||||
points: n,
|
||||
rmsCm: Math.sqrt(s2 / n),
|
||||
maxCm: mx,
|
||||
normal: [nx, ny, nz],
|
||||
};
|
||||
}
|
||||
|
||||
/* COPLANAR_CM: below this the layout is effectively flat and the mirrored pose
|
||||
* is live. The A3 rig's tower gives ~5cm RMS; print tolerance and a crest taped
|
||||
* slightly proud of the table give well under 0.5cm, so 1.5cm separates a real
|
||||
* height difference from noise without being so strict that a modest raised
|
||||
* crest fails to register. */
|
||||
export const COPLANAR_CM = 1.5;
|
||||
|
||||
export function describePlanarity(p) {
|
||||
if (!p) return 'geom —';
|
||||
if (p.rmsCm < COPLANAR_CM) return `geom COPLANAR ${p.rmsCm.toFixed(1)}cm — raise a crest`;
|
||||
return `geom spread ${p.rmsCm.toFixed(1)}cm (max ${p.maxCm.toFixed(1)})`;
|
||||
}
|
||||
Reference in new issue
Block a user