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
@@ -6,8 +6,11 @@ Current as of 2026-09-23. Supersedes the v2 tracking design throughout.
|
||||
|
||||
```
|
||||
camera video
|
||||
-> detection canvas (long side capped, ~20Hz)
|
||||
-> ArUco decode, maxHamming 0, filtered to scene anchor ids, deduped by id
|
||||
-> ROI crops around last-seen crests, at NATIVE resolution (steady state)
|
||||
or full detection canvas, long side capped (acquire / every 600ms / on loss)
|
||||
-> ArUco decode, maxHamming 0
|
||||
-> sub-pixel corner refinement (gradient centroid per border edge)
|
||||
-> filtered to scene anchor ids, deduped by id
|
||||
-> TRACKING ENGINE -> camera pose in world
|
||||
-> three.js render: server-driven ghosts + depth-only occluder meshes
|
||||
```
|
||||
@@ -45,6 +48,69 @@ Pinned to `4.12.0-release.1`. Note for future work: this build lacks
|
||||
`solvePnPGeneric` and `cornerSubPix`, which is why pose ambiguity is handled by
|
||||
gating rather than by comparing both IPPE solutions.
|
||||
|
||||
## Detection: ROI first, full frame as fallback
|
||||
|
||||
The crests do not move; the phone does. Once a crest has been seen, the next
|
||||
tick crops around where it was and decodes that, rather than rescanning 518k
|
||||
pixels to re-find something that shifted by a few. Crops are taken from the
|
||||
**native** video, so each is both smaller than the corresponding patch of a full
|
||||
scan and higher resolution than it (`@1.3x` on a 1280 stream against a 960
|
||||
canvas). Five typical crests come to ~40k pixels against 518k.
|
||||
|
||||
Mapping back into detection space is two scale changes and an origin shift per
|
||||
crop, which is exactly the kind of arithmetic that fails silently and puts
|
||||
ghosts a centimetre off. It is tested directly in `tools/test-roi.mjs`.
|
||||
|
||||
A full scan runs on first acquisition, when targets go stale, at least every
|
||||
600ms, when there are more than 6 targets, and when the crops would cover more
|
||||
than 55% of the frame. **If an ROI pass returns fewer markers than it had
|
||||
targets, a full scan runs in the same tick** — dropping from 4 crests to 2 is
|
||||
precisely what makes the pose ambiguous, so it is never allowed to happen
|
||||
silently. Worst case is one wasted crop pass. Off: `localStorage.nbx.roi = 0`.
|
||||
|
||||
## Corner precision: sub-pixel refinement
|
||||
|
||||
js-aruco2 returns corners on the detection canvas lattice. At 960px a 60mm crest
|
||||
spans ~140px, so one pixel of corner error is ~0.7% of the marker — millimetres
|
||||
of ghost placement, re-rolled every frame.
|
||||
|
||||
`subpix.js` refines each quad by fitting its four border edges rather than by
|
||||
looking at corners. Along each edge it scans perpendicular at ~12 points, takes
|
||||
the **first moment of the gradient** at each (not the gradient peak), fits a line
|
||||
through the crossings by total least squares with one outlier pass, and
|
||||
intersects consecutive lines.
|
||||
|
||||
The centroid matters. Parabolic interpolation of the gradient peak — the obvious
|
||||
method, and what a `cornerSubPix`-style operator reduces to — is biased by
|
||||
sampling phase, because the gradient of an anti-aliased step is roughly
|
||||
triangular. Measured against synthetic edges it carried up to 0.29px of bias
|
||||
sharp and 0.75px defocused, i.e. **worse than the whole-pixel corners it was
|
||||
meant to improve**. The gradient centroid is unbiased for any symmetric edge
|
||||
profile: 0.018px on the same test. End to end on synthetic markers, RMS corner
|
||||
error drops ~12x (`tools/test-subpix.mjs`).
|
||||
|
||||
`cv.cornerSubPix` was not an option: it is absent from the pinned build. Probed
|
||||
directly rather than assumed — `solvePnPRefineLM` IS present, though it refines
|
||||
a pose from corners and so cannot help here.
|
||||
|
||||
Refinement is all-or-nothing per marker and falls back to the raw corners if any
|
||||
edge is too short, too flat, too diffuse, or if a corner would move more than
|
||||
3px. A bad refinement is worse than none, because the pose gates are tuned
|
||||
against honest corner noise. Off: `localStorage.nbx.subpix = 0`.
|
||||
|
||||
## Anchor conditioning
|
||||
|
||||
`geom.js` measures, once per scene load, the RMS distance of every anchor corner
|
||||
from the best-fit plane through all of them, and the HUD shows it. Below 1.5cm
|
||||
the layout is effectively coplanar and the mirrored pose is live — the physics
|
||||
gates below become load-bearing rather than a safety net.
|
||||
|
||||
This exists because "the anchors are coplanar" is otherwise invisible: it looks
|
||||
normal in the JSON and shows up days later as ghosts intermittently flipping
|
||||
across the table. A layout re-saved by an editor that does not understand wall
|
||||
anchors can silently become flat. Reference values: A3 rig with the tower ~3.5cm,
|
||||
a town layout with one wall crest at 8cm ~2.7cm, any all-tabletop layout 0.0cm.
|
||||
|
||||
## Camera model
|
||||
|
||||
Everything derives from one assumption — `HFOV_DEG` (default 60, override via
|
||||
|
||||
Reference in new issue
Block a user