fix: FOV applies to the image's long axis (iOS gives portrait streams — focal was 1.78x short); cap detection canvas long side (portrait was 1.6MP not 0.5MP); show duplicate detections in HUD
This commit is contained in:
+28
-14
@@ -59,7 +59,7 @@ let detector, poseEst, fuser, cvSolver;
|
||||
let gradients = {}, modelManifest = null, characters = { defaults: {}, byId: {} };
|
||||
let ghostHeight = 4; // cm, from scene.ghostHeightCm
|
||||
const activeGhosts = new Map(); // uid -> { rec, group }
|
||||
let tracking = { markers: 0, raw: 0, rawIds: [], lastSeen: 0 };
|
||||
let tracking = { markers: 0, raw: 0, rawIds: [], dupes: 0, lastSeen: 0 };
|
||||
let netRef = null;
|
||||
let sceneLoaded = false;
|
||||
let anchorCount = 0;
|
||||
@@ -68,9 +68,17 @@ function rxTrace(t) { rxLog.push(t); if (rxLog.length > 6) rxLog.shift(); }
|
||||
const clockOffsetSamples = [];
|
||||
let clockOffset = 0; // serverNow - clientNow
|
||||
|
||||
/* Horizontal FOV assumption for the focal estimate; override for a specific device
|
||||
* via localStorage 'nbx.hfovDeg' (shared by both engines). */
|
||||
/* Field-of-view assumption behind every focal estimate; override per device via
|
||||
* localStorage 'nbx.hfovDeg' (shared by both engines).
|
||||
*
|
||||
* IT APPLIES TO THE LONG AXIS OF THE IMAGE, NOT TO videoWidth. iOS can hand back
|
||||
* a PORTRAIT stream (720x1280), and a phone lens has its wide field across the
|
||||
* sensor's long side either way. Assuming the angle spanned videoWidth made the
|
||||
* focal length 1280/720 = 1.78x too short on portrait streams — the exact factor
|
||||
* measured on device, and the reason ghosts still slid around after the camera
|
||||
* model was "fixed". */
|
||||
const HFOV_DEG = parseFloat(localStorage.getItem('nbx.hfovDeg')) || 60;
|
||||
const focalPxFor = (w, h) => (Math.max(w, h) / 2) / Math.tan(THREE.MathUtils.degToRad(HFOV_DEG) / 2);
|
||||
|
||||
/* Detection cadence. Marker detection (getImageData + ArUco decode over a 960px
|
||||
* frame) is by far the most expensive thing per frame, and running it on every
|
||||
@@ -147,7 +155,7 @@ function updateDbg(fusedQuat, markerCount, extra) {
|
||||
dbgPose =
|
||||
`eng ${engine} ${freezeGhosts ? 'FROZEN' : 'moving'}\n` +
|
||||
`${modeLine}\n` +
|
||||
`markers ${markerCount}/${tracking.raw} seen [${tracking.rawIds.join(',')}]${extra}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` +
|
||||
`markers ${markerCount}/${tracking.raw} seen [${tracking.rawIds.join(',')}]${tracking.dupes ? ' +' + tracking.dupes + 'dup' : ''}${extra}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` +
|
||||
`pos ${v3(camera3.position)}`;
|
||||
paintDbg();
|
||||
}
|
||||
@@ -346,10 +354,11 @@ function onResize() {
|
||||
* geometry project too far from centre — measured at 1.7x on device, which
|
||||
* reads as ghosts sliding the wrong way when you tilt.
|
||||
*
|
||||
* So: take the focal length implied by HFOV_DEG over the full video width, work
|
||||
* out how much of the frame survives the cover-crop, and set the fov from that
|
||||
* visible height. Cover crops symmetrically, so the principal point stays
|
||||
* centred and no lens shift is needed. */
|
||||
* So: take the focal length implied by HFOV_DEG over the image's LONG axis (see
|
||||
* focalPxFor — the stream may be portrait), work out how much of the frame
|
||||
* survives the cover-crop, and set the fov from that visible height. Cover crops
|
||||
* symmetrically, so the principal point stays centred and no lens shift is
|
||||
* needed. */
|
||||
const _crop = { x: 1, y: 1 }; // fraction of the video width/height still visible
|
||||
function updateCameraIntrinsics() {
|
||||
if (!camera3) return;
|
||||
@@ -357,7 +366,7 @@ function updateCameraIntrinsics() {
|
||||
if (!video || !video.videoWidth) { camera3.updateProjectionMatrix(); return; }
|
||||
|
||||
const vw = video.videoWidth, vh = video.videoHeight;
|
||||
const focal = (vw / 2) / Math.tan(THREE.MathUtils.degToRad(HFOV_DEG) / 2);
|
||||
const focal = focalPxFor(vw, vh);
|
||||
const scale = Math.max(innerWidth / vw, innerHeight / vh); // object-fit: cover
|
||||
const visW = Math.min(vw, innerWidth / scale);
|
||||
const visH = Math.min(vh, innerHeight / scale);
|
||||
@@ -463,20 +472,25 @@ function loop(t) {
|
||||
|
||||
if (video && video.readyState >= 2 && (t - lastDetectAt) >= DETECT_INTERVAL_MS) {
|
||||
lastDetectAt = t;
|
||||
// downscale for detection speed; corner precision scales with resolution.
|
||||
const W = 960;
|
||||
const H = Math.round(W * video.videoHeight / video.videoWidth);
|
||||
if (canvas2d.width !== W) { canvas2d.width = W; canvas2d.height = H; }
|
||||
/* Downscale for detection speed, capping the LONG side. Fixing the WIDTH at
|
||||
* 960 quietly tripled the work on a portrait stream (960x1707 = 1.6M px vs
|
||||
* 960x540 = 0.5M), which is most of the cost of a frame. */
|
||||
const vw = video.videoWidth, vh = video.videoHeight;
|
||||
const LONG = 960;
|
||||
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 = W / (2 * Math.tan(THREE.MathUtils.degToRad(HFOV_DEG) / 2));
|
||||
const focal = focalPxFor(W, H);
|
||||
if (!poseEst) poseEst = new PoseEstimator(focal);
|
||||
|
||||
const markers = detectMarkers(detector, img, fuser.knownIds());
|
||||
tracking.markers = markers.length;
|
||||
tracking.raw = markers.rawCount || 0;
|
||||
tracking.rawIds = markers.rawIds || [];
|
||||
tracking.dupes = markers.dupes || 0;
|
||||
if (dbgEl) updateAim(markers, W, H);
|
||||
if (markers.length) {
|
||||
tracking.lastSeen = performance.now();
|
||||
|
||||
Reference in New Issue
Block a user