orientation debug
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1" />
|
||||
<title>Newbury — orientation debug</title>
|
||||
<style>
|
||||
:root { --ink:#e8eaf2; --dim:#8a93ad; --cyan:#51eaf1; --green:#4ade80; --amber:#fbbf24; --red:#f65151;
|
||||
--panel:rgba(8,10,16,0.9); }
|
||||
* { box-sizing:border-box; }
|
||||
html,body { margin:0; height:100%; background:#05060a; color:var(--ink);
|
||||
font-family:'Inter',system-ui,sans-serif; overflow:hidden; }
|
||||
#video { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
|
||||
#three,#overlay { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
|
||||
#overlay { pointer-events:none; }
|
||||
|
||||
#hud { position:fixed; left:10px; top:calc(10px + env(safe-area-inset-top)); padding:11px 13px;
|
||||
background:var(--panel); border:1px solid rgba(255,255,255,0.1); border-radius:12px;
|
||||
font-family:ui-monospace,monospace; font-size:11px; line-height:1.65; backdrop-filter:blur(8px); max-width:88vw; }
|
||||
#hud h3 { margin:0 0 6px; font-family:'Inter',sans-serif; font-size:12px; }
|
||||
#hud .row { display:flex; gap:8px; }
|
||||
#hud .lab { color:var(--dim); width:66px; display:inline-block; }
|
||||
#hud .v { font-weight:600; }
|
||||
.rgb-x{color:#ff6b6b}.rgb-y{color:#7CFF6b}.rgb-z{color:#6b9cff}
|
||||
|
||||
#controls { position:fixed; left:50%; bottom:calc(14px + env(safe-area-inset-bottom));
|
||||
transform:translateX(-50%); display:flex; gap:8px; flex-wrap:wrap; justify-content:center; width:94vw; }
|
||||
#controls button { border:0; border-radius:10px; padding:12px 14px; font-size:13px; font-weight:600;
|
||||
cursor:pointer; background:#1a2033; color:var(--ink); flex:1; min-width:88px; max-width:150px; }
|
||||
#controls button.rec { background:var(--red); color:#fff; }
|
||||
#controls button.primary { background:linear-gradient(135deg,#529eff,#51eaf1); color:#06121f; }
|
||||
#count { position:fixed; left:50%; bottom:calc(66px + env(safe-area-inset-bottom)); transform:translateX(-50%);
|
||||
font-family:ui-monospace,monospace; font-size:12px; color:var(--dim); background:var(--panel);
|
||||
padding:5px 12px; border-radius:8px; }
|
||||
|
||||
#logout { position:fixed; inset:0; background:#080a12; padding:16px; display:none; flex-direction:column; gap:10px; z-index:20; }
|
||||
#logout.show { display:flex; }
|
||||
#logout textarea { flex:1; width:100%; background:#0e1220; color:var(--ink); border:1px solid #26304a;
|
||||
border-radius:10px; padding:12px; font-family:ui-monospace,monospace; font-size:11px; resize:none; }
|
||||
#logout .row { display:flex; gap:8px; }
|
||||
#logout button { flex:1; border:0; border-radius:10px; padding:13px; font-size:14px; font-weight:600; cursor:pointer; }
|
||||
#copyBtn { background:linear-gradient(135deg,#529eff,#51eaf1); color:#06121f; }
|
||||
#closeBtn { background:#1a2033; color:var(--ink); }
|
||||
#copied { text-align:center; color:var(--green); font-size:12px; height:14px; }
|
||||
|
||||
#start { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
gap:16px; text-align:center; padding:26px; background:radial-gradient(circle at 50% 40%,#141a2b,#05060a 72%); }
|
||||
#start h1 { font-size:22px; margin:0; } #start p { color:var(--dim); max-width:34ch; line-height:1.6; font-size:13.5px; margin:0; }
|
||||
#go { border:0; cursor:pointer; font-size:16px; font-weight:600; color:#06121f;
|
||||
background:linear-gradient(135deg,#529eff,#51eaf1); padding:14px 32px; border-radius:999px; }
|
||||
#err { color:var(--red); font-size:12.5px; max-width:34ch; }
|
||||
.hidden { display:none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video" playsinline muted autoplay></video>
|
||||
<canvas id="three"></canvas>
|
||||
<canvas id="overlay"></canvas>
|
||||
|
||||
<div id="hud" class="hidden">
|
||||
<h3>Orientation debug</h3>
|
||||
<div class="row"><span class="lab">marker</span><span class="v" id="dId">—</span></div>
|
||||
<div class="row"><span class="lab">RAW posit</span><span class="v" id="dRaw">—</span></div>
|
||||
<div class="row"><span class="lab">RENDER</span><span class="v" id="dRender">—</span></div>
|
||||
<div class="row"><span class="lab">gyro</span><span class="v" id="dGyro">—</span></div>
|
||||
<div class="row"><span class="lab" style="color:var(--amber)">watch</span><span id="dHint" style="color:var(--amber)">tilt phone up = pitch should ↑</span></div>
|
||||
</div>
|
||||
|
||||
<div id="count" class="hidden">not recording</div>
|
||||
<div id="controls" class="hidden">
|
||||
<button id="recBtn">● Record</button>
|
||||
<button id="markBtn">Mark</button>
|
||||
<button id="viewBtn" class="primary">Copy log</button>
|
||||
</div>
|
||||
|
||||
<div id="logout">
|
||||
<div id="copied"></div>
|
||||
<textarea id="logText" readonly></textarea>
|
||||
<div class="row">
|
||||
<button id="copyBtn">Copy to clipboard</button>
|
||||
<button id="closeBtn">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="start">
|
||||
<h1>Orientation debug</h1>
|
||||
<p>Shows raw POS-IT angles vs what we render, plus a 3D gizmo on the marker. Tilt & rotate the phone; the RGB axes should stay glued to the marker. Record and send me the log.</p>
|
||||
<button id="go">Start camera</button>
|
||||
<div id="err"></div>
|
||||
</div>
|
||||
|
||||
<script src="vendor/cv.js"></script>
|
||||
<script src="vendor/svd.js"></script>
|
||||
<script src="vendor/posit1.js"></script>
|
||||
<script src="vendor/aruco.js"></script>
|
||||
<script src="vendor/dictionaries/aruco_4x4_1000.js"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
||||
} }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
import { TunedDetector, MarkerTracker } from './js/detect-tuned.js?v=1';
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const video = $('video'), canvas = $('three'), overlay = $('overlay');
|
||||
const octx = overlay.getContext('2d');
|
||||
const grab = document.createElement('canvas'), gctx = grab.getContext('2d', { willReadFrequently: true });
|
||||
const MARKER_SIZE_MM = 96;
|
||||
|
||||
let detector, tracker, posit, focalPx = 0, running = false;
|
||||
let recording = false, log = [], frameN = 0;
|
||||
let gyro = { alpha: null, beta: null, gamma: null };
|
||||
|
||||
// ---- three.js: mirror ar-head EXACTLY, but draw an AXIS GIZMO not a ghost ----
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
const scene = new THREE.Scene();
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
|
||||
let camera = new THREE.PerspectiveCamera(50, 1, 1, 5000);
|
||||
|
||||
// anchor object driven by pose (exactly like ar-head)
|
||||
const anchor = new THREE.Object3D();
|
||||
anchor.matrixAutoUpdate = false;
|
||||
scene.add(anchor);
|
||||
|
||||
// A big RGB axis gizmo + an "UP" arrow, parented to anchor.
|
||||
const gizmo = new THREE.AxesHelper(MARKER_SIZE_MM * 1.2);
|
||||
anchor.add(gizmo);
|
||||
// a distinct arrow pointing along marker normal (local +Z) so we see "up off table"
|
||||
const arrowMat = new THREE.MeshBasicMaterial({ color: 0x51eaf1 });
|
||||
const arrow = new THREE.Mesh(new THREE.ConeGeometry(10, 40, 12), arrowMat);
|
||||
arrow.position.set(0, 0, MARKER_SIZE_MM); // +Z = marker normal
|
||||
arrow.rotation.x = Math.PI / 2;
|
||||
anchor.add(arrow);
|
||||
anchor.visible = false;
|
||||
|
||||
const flip = new THREE.Matrix4().makeScale(1, -1, -1);
|
||||
const poseMatrix = new THREE.Matrix4();
|
||||
|
||||
function centredCorners(m, w, h) { return m.corners.map((c) => ({ x: c.x - w / 2, y: h / 2 - c.y })); }
|
||||
|
||||
function applyPose(m) {
|
||||
const pose = posit.pose(centredCorners(m, grab.width, grab.height));
|
||||
if (!pose) return null;
|
||||
const R = pose.bestRotation, t = pose.bestTranslation;
|
||||
poseMatrix.set(
|
||||
R[0][0], R[0][1], R[0][2], t[0],
|
||||
R[1][0], R[1][1], R[1][2], t[1],
|
||||
R[2][0], R[2][1], R[2][2], t[2],
|
||||
0, 0, 0, 1
|
||||
);
|
||||
poseMatrix.premultiply(flip); // F*M (ar-head convention)
|
||||
anchor.matrix.copy(poseMatrix);
|
||||
anchor.matrixWorldNeedsUpdate = true;
|
||||
|
||||
// raw posit euler (before flip) and rendered euler (after flip)
|
||||
const rawM = new THREE.Matrix4().set(
|
||||
R[0][0], R[0][1], R[0][2], 0, R[1][0], R[1][1], R[1][2], 0, R[2][0], R[2][1], R[2][2], 0, 0,0,0,1);
|
||||
const rawE = new THREE.Euler().setFromRotationMatrix(rawM, 'YXZ');
|
||||
const renM = poseMatrix.clone(); renM.setPosition(0,0,0);
|
||||
const renE = new THREE.Euler().setFromRotationMatrix(renM, 'YXZ');
|
||||
return {
|
||||
dist: Math.hypot(t[0], t[1], t[2]),
|
||||
raw: { yaw: deg(rawE.y), pitch: deg(rawE.x), roll: deg(rawE.z) },
|
||||
ren: { yaw: deg(renE.y), pitch: deg(renE.x), roll: deg(renE.z) },
|
||||
};
|
||||
}
|
||||
const deg = (r) => Math.round(r * 180 / Math.PI);
|
||||
|
||||
function sizeAll() {
|
||||
const vw = video.videoWidth, vh = video.videoHeight; if (!vw) return;
|
||||
grab.width = vw; grab.height = vh;
|
||||
const scale = Math.max(innerWidth / vw, innerHeight / vh);
|
||||
const dw = vw * scale, dh = vh * scale;
|
||||
for (const el of [video, canvas, overlay]) { el.style.width = dw + 'px'; el.style.height = dh + 'px'; }
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setSize(dw, dh, false);
|
||||
overlay.width = dw; overlay.height = dh;
|
||||
focalPx = vw;
|
||||
const fovY = 2 * Math.atan((vh / 2) / focalPx) * (180 / Math.PI);
|
||||
camera.fov = fovY; camera.aspect = dw / dh; camera.updateProjectionMatrix();
|
||||
posit = new POS.Posit(MARKER_SIZE_MM, focalPx);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
$('err').textContent = '';
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false });
|
||||
video.srcObject = stream; await video.play();
|
||||
} catch (e) { $('err').textContent = 'Camera error: ' + (e.message || e.name); return; }
|
||||
detector = new TunedDetector({ dictionaryName: 'ARUCO_4X4_1000' }).setPreset('forgiving');
|
||||
tracker = new MarkerTracker({ coastMs: 150, smooth: true });
|
||||
// device orientation (gyro) — needs permission on iOS
|
||||
if (typeof DeviceOrientationEvent !== 'undefined' && DeviceOrientationEvent.requestPermission) {
|
||||
try { await DeviceOrientationEvent.requestPermission(); } catch (_) {}
|
||||
}
|
||||
addEventListener('deviceorientation', (e) => { gyro = { alpha: e.alpha, beta: e.beta, gamma: e.gamma }; });
|
||||
await new Promise((r) => { if (video.readyState >= 2) r(); else video.onloadeddata = () => r(); });
|
||||
sizeAll(); addEventListener('resize', sizeAll);
|
||||
$('start').classList.add('hidden');
|
||||
for (const id of ['hud','count','controls']) $(id).classList.remove('hidden');
|
||||
running = true; requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
const sx = () => overlay.width / grab.width, sy = () => overlay.height / grab.height;
|
||||
|
||||
function loop(now) {
|
||||
if (!running) return; requestAnimationFrame(loop);
|
||||
if (video.readyState < 2 || !grab.width) return;
|
||||
gctx.drawImage(video, 0, 0, grab.width, grab.height);
|
||||
const img = gctx.getImageData(0, 0, grab.width, grab.height);
|
||||
let raw = []; try { raw = detector.detect(img, { width: grab.width, height: grab.height }); } catch (_) {}
|
||||
const markers = tracker.update(raw, now);
|
||||
octx.clearRect(0, 0, overlay.width, overlay.height);
|
||||
|
||||
let main = markers[0] || null;
|
||||
if (main) {
|
||||
// draw outline + corner numbers
|
||||
octx.strokeStyle = '#51eaf1'; octx.lineWidth = 2; octx.beginPath();
|
||||
main.corners.forEach((p, i) => { const x = p.x*sx(), y = p.y*sy(); i ? octx.lineTo(x,y) : octx.moveTo(x,y); });
|
||||
octx.closePath(); octx.stroke();
|
||||
main.corners.forEach((p, i) => {
|
||||
const x = p.x*sx(), y = p.y*sy();
|
||||
octx.fillStyle = ['#fff','#ffd24a','#ff8a3a','#ff5a5a'][i];
|
||||
octx.beginPath(); octx.arc(x, y, 6, 0, 7); octx.fill();
|
||||
octx.fillStyle = '#000'; octx.font = 'bold 10px monospace'; octx.fillText(i, x-3, y+3);
|
||||
});
|
||||
|
||||
const res = applyPose(main);
|
||||
if (res) {
|
||||
anchor.visible = true;
|
||||
$('dId').textContent = 'id ' + main.id + (main.coasting ? ' coast' : '');
|
||||
$('dRaw').innerHTML = fmt(res.raw);
|
||||
$('dRender').innerHTML = fmt(res.ren);
|
||||
$('dGyro').textContent = gyro.beta != null
|
||||
? `β${Math.round(gyro.beta)} γ${Math.round(gyro.gamma)} α${Math.round(gyro.alpha)}` : 'n/a';
|
||||
|
||||
if (recording) {
|
||||
frameN++;
|
||||
if (frameN % 3 === 0) {
|
||||
log.push({ t: Math.round(now), id: main.id, dist: Math.round(res.dist),
|
||||
raw: res.raw, ren: res.ren, gyro: { b: r1(gyro.beta), g: r1(gyro.gamma), a: r1(gyro.alpha) } });
|
||||
$('count').textContent = `● REC · ${log.length}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
anchor.visible = false; $('dId').textContent = 'none';
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
function r1(v){ return v==null?null:Math.round(v); }
|
||||
function fmt(a){ return `<span class="rgb-y">P${a.pitch}</span> <span class="rgb-z">R${a.roll}</span> <span class="rgb-x">Y${a.yaw}</span>`; }
|
||||
|
||||
function buildLog() {
|
||||
const L = [];
|
||||
L.push('=== NEWBURY ORIENTATION LOG ===');
|
||||
L.push('conversion: F*M (premultiply flip) marker:' + MARKER_SIZE_MM + 'mm cam:' + grab.width + 'x' + grab.height);
|
||||
L.push('cols: t,id,dist, RAW(pitch,roll,yaw), RENDER(pitch,roll,yaw), GYRO(beta,gamma,alpha)');
|
||||
L.push('gyro beta=front/back tilt, gamma=left/right tilt, alpha=compass');
|
||||
L.push('samples:' + log.length);
|
||||
L.push('');
|
||||
for (const r of log) {
|
||||
L.push([r.t, r.id, r.dist,
|
||||
'RAW', r.raw.pitch, r.raw.roll, r.raw.yaw,
|
||||
'REN', r.ren.pitch, r.ren.roll, r.ren.yaw,
|
||||
'GYRO', r.gyro.b, r.gyro.g, r.gyro.a].join(','));
|
||||
}
|
||||
return L.join('\n');
|
||||
}
|
||||
|
||||
$('recBtn').addEventListener('click', () => {
|
||||
recording = !recording;
|
||||
$('recBtn').classList.toggle('rec', recording);
|
||||
$('recBtn').textContent = recording ? '■ Stop' : '● Record';
|
||||
if (recording) { log = []; $('count').textContent = '● REC · 0'; }
|
||||
});
|
||||
$('markBtn').addEventListener('click', () => { log.push({ t: Math.round(performance.now()), mark: true, raw:{}, ren:{}, gyro:{} }); });
|
||||
$('viewBtn').addEventListener('click', () => { $('logText').value = buildLog(); $('logout').classList.add('show'); });
|
||||
$('copyBtn').addEventListener('click', async () => {
|
||||
try { await navigator.clipboard.writeText($('logText').value); $('copied').textContent = 'Copied!'; }
|
||||
catch (_) { $('logText').select(); $('copied').textContent = 'Long-press → Copy'; }
|
||||
});
|
||||
$('closeBtn').addEventListener('click', () => $('logout').classList.remove('show'));
|
||||
$('go').addEventListener('click', start);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user