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 gradients = {}, modelManifest = null, characters = { defaults: {}, byId: {} };
|
||||||
let ghostHeight = 4; // cm, from scene.ghostHeightCm
|
let ghostHeight = 4; // cm, from scene.ghostHeightCm
|
||||||
const activeGhosts = new Map(); // uid -> { rec, group }
|
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 netRef = null;
|
||||||
let sceneLoaded = false;
|
let sceneLoaded = false;
|
||||||
let anchorCount = 0;
|
let anchorCount = 0;
|
||||||
@@ -68,9 +68,17 @@ function rxTrace(t) { rxLog.push(t); if (rxLog.length > 6) rxLog.shift(); }
|
|||||||
const clockOffsetSamples = [];
|
const clockOffsetSamples = [];
|
||||||
let clockOffset = 0; // serverNow - clientNow
|
let clockOffset = 0; // serverNow - clientNow
|
||||||
|
|
||||||
/* Horizontal FOV assumption for the focal estimate; override for a specific device
|
/* Field-of-view assumption behind every focal estimate; override per device via
|
||||||
* via localStorage 'nbx.hfovDeg' (shared by both engines). */
|
* 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 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
|
/* 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
|
* 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 =
|
dbgPose =
|
||||||
`eng ${engine} ${freezeGhosts ? 'FROZEN' : 'moving'}\n` +
|
`eng ${engine} ${freezeGhosts ? 'FROZEN' : 'moving'}\n` +
|
||||||
`${modeLine}\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)}`;
|
`pos ${v3(camera3.position)}`;
|
||||||
paintDbg();
|
paintDbg();
|
||||||
}
|
}
|
||||||
@@ -346,10 +354,11 @@ function onResize() {
|
|||||||
* geometry project too far from centre — measured at 1.7x on device, which
|
* geometry project too far from centre — measured at 1.7x on device, which
|
||||||
* reads as ghosts sliding the wrong way when you tilt.
|
* 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
|
* So: take the focal length implied by HFOV_DEG over the image's LONG axis (see
|
||||||
* out how much of the frame survives the cover-crop, and set the fov from that
|
* focalPxFor — the stream may be portrait), work out how much of the frame
|
||||||
* visible height. Cover crops symmetrically, so the principal point stays
|
* survives the cover-crop, and set the fov from that visible height. Cover crops
|
||||||
* centred and no lens shift is needed. */
|
* 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
|
const _crop = { x: 1, y: 1 }; // fraction of the video width/height still visible
|
||||||
function updateCameraIntrinsics() {
|
function updateCameraIntrinsics() {
|
||||||
if (!camera3) return;
|
if (!camera3) return;
|
||||||
@@ -357,7 +366,7 @@ function updateCameraIntrinsics() {
|
|||||||
if (!video || !video.videoWidth) { camera3.updateProjectionMatrix(); return; }
|
if (!video || !video.videoWidth) { camera3.updateProjectionMatrix(); return; }
|
||||||
|
|
||||||
const vw = video.videoWidth, vh = video.videoHeight;
|
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 scale = Math.max(innerWidth / vw, innerHeight / vh); // object-fit: cover
|
||||||
const visW = Math.min(vw, innerWidth / scale);
|
const visW = Math.min(vw, innerWidth / scale);
|
||||||
const visH = Math.min(vh, innerHeight / 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) {
|
if (video && video.readyState >= 2 && (t - lastDetectAt) >= DETECT_INTERVAL_MS) {
|
||||||
lastDetectAt = t;
|
lastDetectAt = t;
|
||||||
// downscale for detection speed; corner precision scales with resolution.
|
/* Downscale for detection speed, capping the LONG side. Fixing the WIDTH at
|
||||||
const W = 960;
|
* 960 quietly tripled the work on a portrait stream (960x1707 = 1.6M px vs
|
||||||
const H = Math.round(W * video.videoHeight / video.videoWidth);
|
* 960x540 = 0.5M), which is most of the cost of a frame. */
|
||||||
if (canvas2d.width !== W) { canvas2d.width = W; canvas2d.height = H; }
|
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);
|
ctx2d.drawImage(video, 0, 0, W, H);
|
||||||
const img = ctx2d.getImageData(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);
|
if (!poseEst) poseEst = new PoseEstimator(focal);
|
||||||
|
|
||||||
const markers = detectMarkers(detector, img, fuser.knownIds());
|
const markers = detectMarkers(detector, img, fuser.knownIds());
|
||||||
tracking.markers = markers.length;
|
tracking.markers = markers.length;
|
||||||
tracking.raw = markers.rawCount || 0;
|
tracking.raw = markers.rawCount || 0;
|
||||||
tracking.rawIds = markers.rawIds || [];
|
tracking.rawIds = markers.rawIds || [];
|
||||||
|
tracking.dupes = markers.dupes || 0;
|
||||||
if (dbgEl) updateAim(markers, W, H);
|
if (dbgEl) updateAim(markers, W, H);
|
||||||
if (markers.length) {
|
if (markers.length) {
|
||||||
tracking.lastSeen = performance.now();
|
tracking.lastSeen = performance.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user