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,92 @@
|
||||
/* test-roi.mjs — does an ROI crop hand back corners in the right place?
|
||||
*
|
||||
* node tools/test-roi.mjs
|
||||
*
|
||||
* roi.js crops from the NATIVE video and decodes there, then maps the corners
|
||||
* back into detection space. That is two scale changes and an origin shift per
|
||||
* crop. Get it slightly wrong and nothing throws — the ghosts just sit a
|
||||
* centimetre off, which on this project has historically cost days. So the
|
||||
* mapping is tested directly, with a stub decoder that returns a known quad
|
||||
* expressed in crop pixels; a correct roi.js must return the detection-space
|
||||
* quad it came from.
|
||||
*
|
||||
* Canvas and video are stubbed: no pixels are needed to verify arithmetic.
|
||||
*/
|
||||
import { RoiScanner } from '../public/js/ar/roi.js';
|
||||
|
||||
// ---- stub DOM: a canvas that only records how it was asked to draw ----
|
||||
let lastDraw = null;
|
||||
globalThis.document = {
|
||||
createElement() {
|
||||
return {
|
||||
width: 0, height: 0,
|
||||
getContext() {
|
||||
return {
|
||||
drawImage(_v, sx, sy, sw, sh, dx, dy, dw, dh) { lastDraw = { sx, sy, sw, sh, dx, dy, dw, dh }; },
|
||||
getImageData(x, y, w, h) { return { width: w, height: h, data: new Uint8ClampedArray(w * h * 4) }; },
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function run(name, { W, H, vw, vh, quadDet, maxLongPx }) {
|
||||
const roi = new RoiScanner(maxLongPx ? { maxLongPx } : {});
|
||||
// seed the scanner as though this quad was seen last tick
|
||||
roi.update([{ id: 7, corners: quadDet }], 1000);
|
||||
|
||||
const seen = [];
|
||||
const detectRaw = () => {
|
||||
/* Express the SAME physical quad in the crop's own pixels, using only the
|
||||
* draw parameters roi.js chose. If roi.js maps back correctly we get
|
||||
* quadDet out again. */
|
||||
const { sx, sy, sw, sh, dw, dh } = lastDraw;
|
||||
seen.push({ ...lastDraw });
|
||||
return [{
|
||||
id: 7,
|
||||
corners: quadDet.map(p => ({
|
||||
x: ((p.x * (vw / W)) - sx) * (dw / sw),
|
||||
y: ((p.y * (vh / H)) - sy) * (dh / sh),
|
||||
})),
|
||||
}];
|
||||
};
|
||||
|
||||
const r = roi.scan({}, { W, H, vw, vh }, detectRaw, null);
|
||||
let worst = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
worst = Math.max(worst, Math.hypot(r.markers[0].corners[i].x - quadDet[i].x,
|
||||
r.markers[0].corners[i].y - quadDet[i].y));
|
||||
}
|
||||
const d = seen[0];
|
||||
const upscale = d.dw / (d.sw / (vw / W));
|
||||
console.log(
|
||||
name.padEnd(38) +
|
||||
`crop ${String(d.dw).padStart(3)}x${String(d.dh).padStart(3)}` +
|
||||
` upscale ${upscale.toFixed(2)}x`.padEnd(18) +
|
||||
` max corner err ${worst.toFixed(6)}px` +
|
||||
(worst < 1e-9 ? ' PASS' : ' *** FAIL ***')
|
||||
);
|
||||
return worst < 1e-9;
|
||||
}
|
||||
|
||||
const q = (x, y, s) => [{ x, y }, { x: x + s, y }, { x: x + s, y: y + s }, { x, y: y + s }];
|
||||
|
||||
console.log('ROI crop -> detection space round trip\n');
|
||||
let ok = true;
|
||||
ok &= run('landscape 1280x720, det 960', { W: 960, H: 540, vw: 1280, vh: 720, quadDet: q(400, 220, 70) });
|
||||
ok &= run('portrait 720x1280, det 960', { W: 540, H: 960, vw: 720, vh: 1280, quadDet: q(200, 500, 60) });
|
||||
ok &= run('crest near left edge', { W: 960, H: 540, vw: 1280, vh: 720, quadDet: q(4, 200, 50) });
|
||||
ok &= run('crest near bottom-right edge', { W: 960, H: 540, vw: 1280, vh: 720, quadDet: q(890, 470, 55) });
|
||||
ok &= run('rotated quad (non-axis-aligned)', {
|
||||
W: 960, H: 540, vw: 1280, vh: 720,
|
||||
quadDet: [{ x: 410, y: 200 }, { x: 488, y: 228 }, { x: 460, y: 306 }, { x: 382, y: 278 }],
|
||||
});
|
||||
ok &= run('tiny distant crest', { W: 960, H: 540, vw: 1280, vh: 720, quadDet: q(500, 260, 18) });
|
||||
ok &= run('big near crest (hits long cap)', { W: 960, H: 540, vw: 1280, vh: 720, quadDet: q(300, 120, 300) });
|
||||
ok &= run('4K source, det 960', { W: 960, H: 540, vw: 3840, vh: 2160, quadDet: q(400, 220, 70) });
|
||||
ok &= run('cap raised to 384', { W: 960, H: 540, vw: 1280, vh: 720, quadDet: q(400, 220, 70), maxLongPx: 384 });
|
||||
|
||||
console.log('\nupscale > 1 means the crop carries more pixels per crest than a full');
|
||||
console.log('scan would, which is where the decode-rate gain is expected to come from.');
|
||||
console.log(ok ? '\nall mappings exact' : '\nMAPPING ERRORS PRESENT');
|
||||
process.exit(ok ? 0 : 1);
|
||||
Reference in new issue
Block a user