Switchable camera-frame correction + tap-to-cycle rot/cam modes on debug overlay

This commit is contained in:
2026-08-04 09:57:55 +10:00
parent 3a961d64fd
commit f4d51b6def
+37 -14
View File
@@ -5,7 +5,7 @@
*/ */
import * as THREE from 'three'; import * as THREE from 'three';
import { createDetector, detectMarkers, quadArea } from './ar/detect.js'; import { createDetector, detectMarkers, quadArea } from './ar/detect.js';
import { PoseEstimator } from './ar/pose.js'; import { PoseEstimator, ROT_MODES, setRotMode, getRotMode } from './ar/pose.js';
import { WorldFuser } from './ar/fuse.js'; import { WorldFuser } from './ar/fuse.js';
import { buildGhost } from './ghosts/loader.js'; import { buildGhost } from './ghosts/loader.js';
import { ghostTransform } from './ghosts/behavior.js'; import { ghostTransform } from './ghosts/behavior.js';
@@ -14,15 +14,19 @@ import { renderMarkdown } from './md.js';
const $ = (s) => document.querySelector(s); const $ = (s) => document.querySelector(s);
/* The fused pose is a camera-in-world transform in OpenCV convention /* Camera-frame correction applied to the fused quaternion (see pose.js for the
* (+X right, +Y DOWN, +Z FORWARD along the view axis). A Three.js camera uses * rotation-frame conjugation that happens upstream). Switchable for on-device A/B:
* OpenGL convention (+X right, +Y UP, looks down LOCAL -Z). The correct change of * once pose.js conjugates the rotation correctly, the right camera correction here
* basis between them is a 180° rotation about the camera's local Y axis: it flips * may be identity, Y180, X180, or Z180 depending on the net convention. The debug
* the forward axis (+Z_cv view -> three.js -Z) AND the up axis (+Y_cv down -> * overlay cycles CAM_MODES. */
* three.js +Y up) together, leaving roll correct. (A 180° about X fixes the view const CAM_CORR = {
* axis too but leaves roll inverted — ghosts render upside-down when the phone is none: new THREE.Quaternion(),
* upright.) Applied after copying the fused quaternion each frame. */ y180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI),
const CV_TO_GL = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI); x180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI),
z180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI),
};
const CAM_MODES = ['none', 'y180', 'x180', 'z180'];
let camMode = 'none'; // default: none — rely on pose.js conjugation alone
let info = { mode: 'dev' }; let info = { mode: 'dev' };
let scene3, camera3, renderer, video, canvas2d, ctx2d; let scene3, camera3, renderer, video, canvas2d, ctx2d;
@@ -50,6 +54,7 @@ function updateDbg(fusedQuat, markerCount) {
const up = _dbgV.set(0, 1, 0).applyQuaternion(camera3.quaternion).clone(); const up = _dbgV.set(0, 1, 0).applyQuaternion(camera3.quaternion).clone();
const v3 = v => `(${v.x.toFixed(2)},${v.y.toFixed(2)},${v.z.toFixed(2)})`; const v3 = v => `(${v.x.toFixed(2)},${v.y.toFixed(2)},${v.z.toFixed(2)})`;
dbgEl.textContent = dbgEl.textContent =
`rot ${getRotMode()} cam ${camMode} (tap to cycle)\n` +
`markers ${markerCount}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` + `markers ${markerCount}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` +
`pos ${v3(camera3.position)}`; `pos ${v3(camera3.position)}`;
} }
@@ -131,8 +136,25 @@ async function start() {
detector = createDetector(); detector = createDetector();
fuser = new WorldFuser(); fuser = new WorldFuser();
// dev-mode pose readout // dev-mode pose readout + tap-to-cycle correction modes
if (info.mode === 'dev') { dbgEl = $('#dbg'); if (dbgEl) dbgEl.style.display = 'block'; } if (info.mode === 'dev') {
dbgEl = $('#dbg');
if (dbgEl) {
dbgEl.style.display = 'block';
dbgEl.style.pointerEvents = 'auto';
// tap top ~60% cycles rotation mode; tap bottom cycles camera mode
dbgEl.addEventListener('click', (ev) => {
const r = dbgEl.getBoundingClientRect();
if ((ev.clientY - r.top) < r.height * 0.5) {
const i = ROT_MODES.indexOf(getRotMode());
setRotMode(ROT_MODES[(i + 1) % ROT_MODES.length]);
} else {
const i = CAM_MODES.indexOf(camMode);
camMode = CAM_MODES[(i + 1) % CAM_MODES.length];
}
});
}
}
// network // network
const net = new ExhibitNet(); const net = new ExhibitNet();
@@ -241,8 +263,9 @@ function loop(t) {
const fused = fuser.fuse(estimates); const fused = fuser.fuse(estimates);
if (fused) { if (fused) {
camera3.position.copy(fused.position); camera3.position.copy(fused.position);
// convert OpenCV camera frame -> Three.js (OpenGL) camera frame // pose.js already conjugated rotation into the three.js frame; apply any
camera3.quaternion.copy(fused.quaternion).multiply(CV_TO_GL); // residual camera-frame correction (switchable via overlay)
camera3.quaternion.copy(fused.quaternion).multiply(CAM_CORR[camMode]);
updateDbg(fused.quaternion, fused.markerCount); updateDbg(fused.quaternion, fused.markerCount);
} }
} }