93 lines
4.2 KiB
HTML
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(3)).join(', ')} m (${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>
|