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
+57
-7
@@ -10,7 +10,10 @@
|
||||
* on-table A/B until the new engine is confirmed. Delete later.
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import { createDetector, detectMarkers, quadArea } from './ar/detect.js';
|
||||
import { createDetector, detectRaw, finaliseMarkers, quadArea } from './ar/detect.js';
|
||||
import { refineMarkers } from './ar/subpix.js';
|
||||
import { RoiScanner } from './ar/roi.js';
|
||||
import { describePlanarity } from './ar/geom.js';
|
||||
import { PoseEstimator, ROT_MODES, setRotMode, getRotMode } from './ar/pose.js';
|
||||
import { WorldFuser } from './ar/fuse.js';
|
||||
import { CvBoardSolver, CV_AXIS_MODES } from './ar/solve-cv.js';
|
||||
@@ -95,6 +98,19 @@ const DETECT_HZ = parseFloat(localStorage.getItem('nbx.detectHz')) || 20;
|
||||
const DETECT_LONG = parseFloat(localStorage.getItem('nbx.detectLong')) || 960;
|
||||
const DETECT_INTERVAL_MS = 1000 / DETECT_HZ;
|
||||
let lastDetectAt = 0;
|
||||
|
||||
/* Sub-pixel corner refinement (subpix.js). js-aruco2 returns corners quantised
|
||||
* to the detection canvas lattice; everything downstream inherits that error.
|
||||
* Measured on synthetic markers it cuts RMS corner error ~12x. Off: 'nbx.subpix'=0. */
|
||||
const SUBPIX = (localStorage.getItem('nbx.subpix') ?? '1') !== '0';
|
||||
|
||||
/* ROI-first detection (roi.js). Crop around the last known crest positions and
|
||||
* decode those at native video resolution instead of rescanning the whole
|
||||
* downscaled frame. Falls back to a full scan whenever the crop pass comes back
|
||||
* short, so it cannot lose a crest. Off: 'nbx.roi'=0. */
|
||||
const ROI_ON = (localStorage.getItem('nbx.roi') ?? '1') !== '0';
|
||||
const roiScanner = new RoiScanner();
|
||||
let lastSubpix = { refined: 0, total: 0, meanShiftPx: 0 };
|
||||
let fps = 0, fpsFrames = 0, fpsAt = 0;
|
||||
|
||||
// Debug pose readout (dev mode)
|
||||
@@ -107,7 +123,11 @@ function dbgStatus() {
|
||||
return `ws ${ws} rx ${rxLog.join(',') || '-'}\n` +
|
||||
`scene ${sceneLoaded ? anchorCount + ' anchors' : 'NOT LOADED'} ghost recs ${activeGhosts.size}\n` +
|
||||
`${fps} fps detect ${DETECT_HZ}Hz@${DETECT_LONG} fov ${camera3 ? camera3.fov.toFixed(0) : '?'}` +
|
||||
`${cvSolver && cvSolver.lastReject ? ' REJ:' + cvSolver.lastReject : ''}\n`;
|
||||
`${cvSolver && cvSolver.lastReject ? ' REJ:' + cvSolver.lastReject : ''}\n` +
|
||||
`scan ${roiScanner.stats.mode}` +
|
||||
`${roiScanner.stats.mode.startsWith('roi') ? ` ${roiScanner.stats.rois}x @${roiScanner.stats.upscale.toFixed(1)}x ${(roiScanner.stats.pixels / 1000).toFixed(0)}kpx` : ''}` +
|
||||
` subpix ${SUBPIX ? `${lastSubpix.refined}/${lastSubpix.total} ${lastSubpix.meanShiftPx.toFixed(2)}px` : 'off'}\n` +
|
||||
`${describePlanarity(cvSolver && cvSolver.planarity)}\n`;
|
||||
}
|
||||
function paintDbg() { if (dbgEl) dbgEl.textContent = dbgStatus() + dbgPose + dbgAim; }
|
||||
|
||||
@@ -396,6 +416,7 @@ function applyScene(scene) {
|
||||
anchorCount = (scene.anchors || []).filter(a => a.enabled !== false).length;
|
||||
fuser.setScene(scene);
|
||||
cvSolver.setScene(scene);
|
||||
roiScanner.reset();
|
||||
buildOcclusion(scene);
|
||||
ghostHeight = scene.ghostHeightCm || 4;
|
||||
for (const e of activeGhosts.values()) e.group.userData.setHeight(ghostHeight);
|
||||
@@ -487,14 +508,43 @@ function loop(t) {
|
||||
const LONG = DETECT_LONG;
|
||||
const W = vw >= vh ? LONG : Math.round(LONG * vw / vh);
|
||||
const H = vw >= vh ? Math.round(LONG * vh / vw) : LONG;
|
||||
if (canvas2d.width !== W || canvas2d.height !== H) { canvas2d.width = W; canvas2d.height = H; }
|
||||
ctx2d.drawImage(video, 0, 0, W, H);
|
||||
const img = ctx2d.getImageData(0, 0, W, H);
|
||||
|
||||
const focal = focalPxFor(W, H);
|
||||
if (!poseEst) poseEst = new PoseEstimator(focal);
|
||||
|
||||
const markers = detectMarkers(detector, img, fuser.knownIds());
|
||||
/* ROI first, full frame as the fallback. Both paths end in finaliseMarkers,
|
||||
* so dedupe, scene filtering and diagnostics are identical either way and
|
||||
* nothing below here needs to know which ran.
|
||||
*
|
||||
* Sub-pixel refinement runs on whichever image was actually decoded. In ROI
|
||||
* mode that is the crop, at native resolution — so its precision is divided
|
||||
* by the crop's upscale factor on the way back into detection space, and
|
||||
* refinement gets BETTER the closer you are, which is the opposite of how
|
||||
* the raw detector behaves. */
|
||||
let sRef = 0, sTot = 0, sShift = 0;
|
||||
const refine = SUBPIX ? (im, ms) => {
|
||||
const r = refineMarkers(im, ms);
|
||||
sRef += r.refined; sTot += r.total; sShift += r.meanShiftPx * r.refined;
|
||||
} : null;
|
||||
|
||||
let markers = null;
|
||||
if (ROI_ON && !roiScanner.wantsFull(t, W, H)) {
|
||||
const r = roiScanner.scan(video, { W, H, vw, vh }, im => detectRaw(detector, im), refine);
|
||||
if (r.complete) markers = finaliseMarkers(r.markers, fuser.knownIds());
|
||||
}
|
||||
if (!markers) {
|
||||
if (canvas2d.width !== W || canvas2d.height !== H) { canvas2d.width = W; canvas2d.height = H; }
|
||||
ctx2d.drawImage(video, 0, 0, W, H);
|
||||
const img = ctx2d.getImageData(0, 0, W, H);
|
||||
const raw = detectRaw(detector, img);
|
||||
if (refine && raw.length) refine(img, raw);
|
||||
markers = finaliseMarkers(raw, fuser.knownIds());
|
||||
roiScanner.noteFull(t);
|
||||
}
|
||||
roiScanner.update(markers, t);
|
||||
|
||||
const up = roiScanner.stats.mode.startsWith('roi') ? (roiScanner.stats.upscale || 1) : 1;
|
||||
lastSubpix = { refined: sRef, total: sTot, meanShiftPx: sRef ? (sShift / sRef) / up : 0 };
|
||||
|
||||
tracking.markers = markers.length;
|
||||
tracking.raw = markers.rawCount || 0;
|
||||
tracking.rawIds = markers.rawIds || [];
|
||||
|
||||
Reference in new issue
Block a user