Files
newbury-exhibit-v2/public/admin/calibrate.html
T

93 lines
4.2 KiB
HTML

<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Calibrate — Newbury Exhibit</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#000;color:#fff;font-family:monospace}
#cam{position:fixed;inset:0;width:100%;height:100%;object-fit:cover;opacity:.85}
#ov{position:fixed;inset:0;pointer-events:none}
#out{position:fixed;top:0;left:0;right:0;background:rgba(0,0,0,.65);padding:8px 10px;font-size:12px;white-space:pre;line-height:1.5}
</style></head>
<body>
<video id="cam" playsinline muted></video>
<canvas id="ov"></canvas>
<div id="out">Starting camera…</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 { createDetector, detectMarkers, quadArea } from '/js/ar/detect.js';
import { PoseEstimator } from '/js/ar/pose.js';
import { WorldFuser } from '/js/ar/fuse.js';
const scene = await (await fetch('/api/scene')).json();
const fuser = new WorldFuser(); fuser.setScene(scene);
const detector = createDetector();
const video = document.getElementById('cam');
const ov = document.getElementById('ov'); const octx = ov.getContext('2d');
const out = document.getElementById('out');
const cv2 = document.createElement('canvas'); const ctx = cv2.getContext('2d', { willReadFrequently: true });
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 } }, audio: false });
video.srcObject = stream; await video.play();
let poseEst = null;
const jitterBuf = new Map(); // markerId -> recent positions
function loop() {
requestAnimationFrame(loop);
if (video.readyState < 2) return;
const W = 640, H = Math.round(W * video.videoHeight / video.videoWidth);
if (cv2.width !== W) { cv2.width = W; cv2.height = H; }
ov.width = innerWidth; ov.height = innerHeight;
ctx.drawImage(video, 0, 0, W, H);
const img = ctx.getImageData(0, 0, W, H);
if (!poseEst) poseEst = new PoseEstimator(W / (2 * Math.tan(30 * Math.PI / 180)));
const markers = detectMarkers(detector, img, fuser.knownIds());
const sx = innerWidth / W, sy = innerHeight / H;
octx.clearRect(0, 0, ov.width, ov.height);
const lines = [];
const estimates = [];
for (const m of markers) {
// outline
octx.strokeStyle = '#51eaf1'; octx.lineWidth = 3; octx.beginPath();
m.corners.forEach((c, i) => i ? octx.lineTo(c.x * sx, c.y * sy) : octx.moveTo(c.x * sx, c.y * sy));
octx.closePath(); octx.stroke();
const e = poseEst.estimate(m.id, m.corners, W / 2, H / 2, fuser.sizeFor(m.id));
if (!e) continue;
estimates.push({ markerId: m.id, position: e.position, quaternion: e.quaternion, area: quadArea(m.corners) });
const distMM = e.position.length();
const eul = new THREE.Euler().setFromQuaternion(e.quaternion, 'YXZ');
const buf = jitterBuf.get(m.id) || []; buf.push(e.position.clone()); if (buf.length > 30) buf.shift(); jitterBuf.set(m.id, buf);
const mean = buf.reduce((a, p) => a.add(p), new THREE.Vector3()).multiplyScalar(1 / buf.length);
const jit = Math.sqrt(buf.reduce((a, p) => a + p.distanceToSquared(mean), 0) / buf.length);
lines.push(`crest #${String(m.id).padStart(3)} dist ${(distMM / 10).toFixed(1)} cm` +
` yaw ${THREE.MathUtils.radToDeg(eul.y).toFixed(0).padStart(4)}°` +
` pitch ${THREE.MathUtils.radToDeg(eul.x).toFixed(0).padStart(4)}°` +
` jitter ${jit.toFixed(1)} mm err ${e.error.toFixed(1)}`);
}
const fused = fuser.fuse(estimates);
if (fused) lines.push('', `FUSED cam @ ${fused.position.toArray().map(v => v.toFixed(1)).join(', ')} cm (${fused.markerCount} crest${fused.markerCount === 1 ? '' : 's'})`);
out.textContent = lines.join('\n') || 'No crests in view — show a printed Newbury Crest to the camera.';
}
loop();
</script>
</body></html>