debug logging
This commit is contained in:
@@ -0,0 +1,297 @@
|
|||||||
|
<!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 — pose logger</title>
|
||||||
|
<style>
|
||||||
|
:root { --ink:#e8eaf2; --dim:#8a93ad; --cyan:#51eaf1; --green:#4ade80; --amber:#fbbf24; --red:#f65151;
|
||||||
|
--panel:rgba(8,10,16,0.92); }
|
||||||
|
* { 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%); }
|
||||||
|
#overlay { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
|
||||||
|
|
||||||
|
#live { position:fixed; left:10px; top:calc(10px + env(safe-area-inset-top)); padding:10px 12px;
|
||||||
|
background:var(--panel); border:1px solid rgba(255,255,255,0.1); border-radius:12px;
|
||||||
|
font-family:ui-monospace,monospace; font-size:11.5px; line-height:1.6; backdrop-filter:blur(8px); max-width:82vw; }
|
||||||
|
#live .k { color:var(--dim); }
|
||||||
|
#live .bad { color:var(--red); font-weight:700; }
|
||||||
|
#live .ok { color:var(--green); }
|
||||||
|
#live .warn { color:var(--amber); }
|
||||||
|
|
||||||
|
#tagbar { position:fixed; right:10px; top:calc(10px + env(safe-area-inset-top)); }
|
||||||
|
#tagbar select { background:#141a2c; color:var(--ink); border:1px solid #26304a; border-radius:8px;
|
||||||
|
padding:8px 10px; font-size:12px; }
|
||||||
|
|
||||||
|
#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:90px; 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 h2 { margin:0; font-size:15px; }
|
||||||
|
#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="overlay"></canvas>
|
||||||
|
|
||||||
|
<div id="live" class="hidden">
|
||||||
|
<div><span class="k">raw ids:</span> <span id="lIds">—</span></div>
|
||||||
|
<div><span class="k">main:</span> <span id="lMain">—</span> <span class="k">ham:</span> <span id="lHam">—</span></div>
|
||||||
|
<div><span class="k">bestErr:</span> <span id="lBest">—</span> <span class="k">ratio:</span> <span id="lRatio">—</span> <span id="lFlag"></span></div>
|
||||||
|
<div><span class="k">yaw:</span> <span id="lYaw">—</span> <span class="k">pitch:</span> <span id="lPitch">—</span> <span class="k">roll:</span> <span id="lRoll">—</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tagbar" class="hidden">
|
||||||
|
<select id="tag">
|
||||||
|
<option value="crest-0">Testing crest id 0</option>
|
||||||
|
<option value="crest-1">Testing crest id 1</option>
|
||||||
|
<option value="crest-2">Testing crest id 2</option>
|
||||||
|
<option value="crest-3">Testing crest id 3</option>
|
||||||
|
<option value="crest-4">Testing crest id 4</option>
|
||||||
|
<option value="walk-around">Walking around</option>
|
||||||
|
<option value="rotate">Rotating phone</option>
|
||||||
|
<option value="from-north">Viewing from north</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="count" class="hidden">not recording</div>
|
||||||
|
<div id="controls" class="hidden">
|
||||||
|
<button id="recBtn">● Record</button>
|
||||||
|
<button id="markBtn">Mark event</button>
|
||||||
|
<button id="viewBtn" class="primary">View / copy log</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="logout">
|
||||||
|
<h2>Session log</h2>
|
||||||
|
<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>AR pose logger</h1>
|
||||||
|
<p>Records raw detection + pose data as you move around each crest. Pick a label, hit Record, walk/rotate, then View to copy the log and paste it back.</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="module">
|
||||||
|
import { TunedDetector } from './js/detect-tuned.js?v=1';
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const video = $('video'), overlay = $('overlay'), ctx = overlay.getContext('2d');
|
||||||
|
const grab = document.createElement('canvas'), gctx = grab.getContext('2d', { willReadFrequently: true });
|
||||||
|
const MARKER_SIZE_MM = 96;
|
||||||
|
const KNOWN_IDS = [0, 1, 2, 3, 4]; // anchors we expect; anything else is a phantom
|
||||||
|
|
||||||
|
let detector, posit, focalPx = 0, running = false;
|
||||||
|
let recording = false, log = [], frameN = 0, sampleEvery = 3; // log every 3rd frame to keep size sane
|
||||||
|
let stats = null;
|
||||||
|
|
||||||
|
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, overlay]) { el.style.width = dw + 'px'; el.style.height = dh + 'px'; }
|
||||||
|
overlay.width = dw; overlay.height = dh;
|
||||||
|
focalPx = vw;
|
||||||
|
posit = new POS.Posit(MARKER_SIZE_MM, focalPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signedArea(c){let a=0;for(let i=0;i<c.length;i++){const j=(i+1)%c.length;a+=c[i].x*c[j].y-c[j].x*c[i].y;}return a/2;}
|
||||||
|
function polyArea(c){return Math.abs(signedArea(c));}
|
||||||
|
function eulerish(R){
|
||||||
|
const pitch=Math.atan2(-R[2][0],Math.hypot(R[2][1],R[2][2]))*180/Math.PI;
|
||||||
|
const yaw=Math.atan2(R[1][0],R[0][0])*180/Math.PI;
|
||||||
|
const roll=Math.atan2(R[2][1],R[2][2])*180/Math.PI;
|
||||||
|
return {yaw,pitch,roll};
|
||||||
|
}
|
||||||
|
|
||||||
|
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) + '. iOS needs HTTPS.'; return; }
|
||||||
|
detector = new TunedDetector({ dictionaryName: 'ARUCO_4X4_1000' }).setPreset('forgiving');
|
||||||
|
await new Promise((r) => { if (video.readyState >= 2) r(); else video.onloadeddata = () => r(); });
|
||||||
|
sizeAll(); addEventListener('resize', sizeAll);
|
||||||
|
$('start').classList.add('hidden');
|
||||||
|
for (const el of ['live','tagbar','count','controls']) $(el).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);
|
||||||
|
// RAW detector output (keeps id + hammingDistance; NOT tracked/smoothed)
|
||||||
|
let raw = []; try { raw = detector.detect(img, { width: grab.width, height: grab.height }); } catch (_) {}
|
||||||
|
ctx.clearRect(0, 0, overlay.width, overlay.height);
|
||||||
|
|
||||||
|
let main = null, area = 0;
|
||||||
|
for (const m of raw) { const a = polyArea(m.corners); if (a > area) { area = a; main = m; } }
|
||||||
|
|
||||||
|
// draw outlines
|
||||||
|
for (const m of raw) {
|
||||||
|
const known = KNOWN_IDS.includes(m.id);
|
||||||
|
ctx.strokeStyle = m === main ? (known ? '#51eaf1' : '#f65151') : 'rgba(255,255,255,0.35)';
|
||||||
|
ctx.lineWidth = 2; ctx.beginPath();
|
||||||
|
m.corners.forEach((p,i)=>{const x=p.x*sx(),y=p.y*sy();i?ctx.lineTo(x,y):ctx.moveTo(x,y);});
|
||||||
|
ctx.closePath(); ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = raw.map(m => m.id);
|
||||||
|
let rec = null;
|
||||||
|
if (main) {
|
||||||
|
const known = KNOWN_IDS.includes(main.id);
|
||||||
|
const wind = signedArea(main.corners) > 0 ? 'CW' : 'CCW';
|
||||||
|
const c = main.corners.map((p)=>({x:p.x-grab.width/2, y:grab.height/2-p.y}));
|
||||||
|
const pose = posit.pose(c);
|
||||||
|
let bestErr=null, ratio=null, ang=null;
|
||||||
|
if (pose) {
|
||||||
|
bestErr = pose.bestError;
|
||||||
|
ratio = pose.bestError>0 ? pose.alternativeError/pose.bestError : 99;
|
||||||
|
ang = eulerish(pose.bestRotation);
|
||||||
|
}
|
||||||
|
rec = { id: main.id, known, ham: main.hammingDistance, wind, area: Math.round(area),
|
||||||
|
bestErr, ratio, ang, ids: ids.slice() };
|
||||||
|
|
||||||
|
// live readout
|
||||||
|
$('lIds').innerHTML = ids.map(i => KNOWN_IDS.includes(i) ? i : `<span class="bad">${i}</span>`).join(', ') || '—';
|
||||||
|
$('lMain').innerHTML = known ? `<span class="ok">${main.id}</span>` : `<span class="bad">${main.id} (phantom!)</span>`;
|
||||||
|
$('lHam').innerHTML = main.hammingDistance===0 ? '<span class="ok">0</span>' : `<span class="warn">${main.hammingDistance}</span>`;
|
||||||
|
if (pose) {
|
||||||
|
$('lBest').textContent = bestErr.toFixed(1);
|
||||||
|
const amb = ratio < 1.5;
|
||||||
|
$('lRatio').textContent = ratio.toFixed(2);
|
||||||
|
$('lFlag').innerHTML = amb ? '<span class="bad">FLIP</span>' : '<span class="ok">ok</span>';
|
||||||
|
$('lYaw').textContent = ang.yaw.toFixed(0); $('lPitch').textContent = ang.pitch.toFixed(0); $('lRoll').textContent = ang.roll.toFixed(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('lIds').textContent = '—'; $('lMain').textContent = 'none'; $('lHam').textContent='—';
|
||||||
|
$('lBest').textContent='—'; $('lRatio').textContent='—'; $('lFlag').textContent='';
|
||||||
|
}
|
||||||
|
|
||||||
|
// record
|
||||||
|
if (recording) {
|
||||||
|
frameN++;
|
||||||
|
if (rec && frameN % sampleEvery === 0) {
|
||||||
|
log.push({ t: Math.round(now), tag: $('tag').value, ...rec });
|
||||||
|
accumulate(rec);
|
||||||
|
$('count').textContent = `● REC · ${log.length} samples · ${$('tag').value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// running aggregates so the pasted log has a clear summary
|
||||||
|
function accumulate(r) {
|
||||||
|
if (!stats) stats = { total:0, phantom:0, ham0:0, hamHi:0, flips:0, idCounts:{}, ratios:[], byTag:{} };
|
||||||
|
stats.total++;
|
||||||
|
if (!r.known) stats.phantom++;
|
||||||
|
if (r.ham === 0) stats.ham0++; else stats.hamHi++;
|
||||||
|
if (r.ratio != null && r.ratio < 1.5) stats.flips++;
|
||||||
|
stats.idCounts[r.id] = (stats.idCounts[r.id]||0) + 1;
|
||||||
|
if (r.ratio != null && r.ratio < 90) stats.ratios.push(r.ratio);
|
||||||
|
const tg = r.tag || 'na'; stats.byTag[tg] = stats.byTag[tg] || { n:0, phantom:0, flips:0 };
|
||||||
|
stats.byTag[tg].n++; if(!r.known) stats.byTag[tg].phantom++; if(r.ratio<1.5) stats.byTag[tg].flips++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLogText() {
|
||||||
|
const lines = [];
|
||||||
|
lines.push('=== NEWBURY POSE LOG ===');
|
||||||
|
lines.push('marker size assumed: ' + MARKER_SIZE_MM + 'mm · known ids: ' + KNOWN_IDS.join(','));
|
||||||
|
lines.push('camera: ' + grab.width + 'x' + grab.height + ' · focalPx=' + Math.round(focalPx));
|
||||||
|
lines.push('samples: ' + log.length);
|
||||||
|
if (stats) {
|
||||||
|
const avgRatio = stats.ratios.length ? (stats.ratios.reduce((a,b)=>a+b,0)/stats.ratios.length) : 0;
|
||||||
|
lines.push('');
|
||||||
|
lines.push('--- SUMMARY ---');
|
||||||
|
lines.push('phantom-id frames: ' + stats.phantom + '/' + stats.total + ' (' + pct(stats.phantom,stats.total) + ')');
|
||||||
|
lines.push('hamming==0: ' + stats.ham0 + ' · hamming>0: ' + stats.hamHi + ' (' + pct(stats.hamHi,stats.total) + ' corrected)');
|
||||||
|
lines.push('ambiguous(flip) frames: ' + stats.flips + '/' + stats.total + ' (' + pct(stats.flips,stats.total) + ')');
|
||||||
|
lines.push('avg alt/best ratio: ' + avgRatio.toFixed(2) + ' (near 1.0 = flips; want >1.5)');
|
||||||
|
lines.push('ids seen: ' + Object.entries(stats.idCounts).map(([k,v])=>`${k}:${v}${KNOWN_IDS.includes(+k)?'':'(PHANTOM)'}`).join(', '));
|
||||||
|
lines.push('');
|
||||||
|
lines.push('--- BY LABEL ---');
|
||||||
|
for (const [tg,s] of Object.entries(stats.byTag)) {
|
||||||
|
lines.push(`${tg}: ${s.n} frames · phantom ${pct(s.phantom,s.n)} · flip ${pct(s.flips,s.n)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
lines.push('--- RAW SAMPLES (t,tag,id,known,ham,wind,area,bestErr,ratio,yaw,pitch,roll,allIds) ---');
|
||||||
|
for (const r of log) {
|
||||||
|
lines.push([
|
||||||
|
r.t, r.tag, r.id, r.known?'Y':'N', r.ham, r.wind, r.area,
|
||||||
|
r.bestErr!=null?r.bestErr.toFixed(1):'-',
|
||||||
|
r.ratio!=null?r.ratio.toFixed(2):'-',
|
||||||
|
r.ang?r.ang.yaw.toFixed(0):'-', r.ang?r.ang.pitch.toFixed(0):'-', r.ang?r.ang.roll.toFixed(0):'-',
|
||||||
|
'['+r.ids.join(' ')+']'
|
||||||
|
].join(','));
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
function pct(a,b){ return b? Math.round(a/b*100)+'%' : '0%'; }
|
||||||
|
|
||||||
|
$('recBtn').addEventListener('click', () => {
|
||||||
|
recording = !recording;
|
||||||
|
$('recBtn').classList.toggle('rec', recording);
|
||||||
|
$('recBtn').textContent = recording ? '■ Stop' : '● Record';
|
||||||
|
$('count').textContent = recording ? '● REC · 0 samples' : `stopped · ${log.length} samples`;
|
||||||
|
});
|
||||||
|
$('markBtn').addEventListener('click', () => {
|
||||||
|
log.push({ t: Math.round(performance.now()), tag: $('tag').value, event: 'MARK', ids: [] });
|
||||||
|
$('count').textContent = `marked @ ${log.length}`;
|
||||||
|
});
|
||||||
|
$('viewBtn').addEventListener('click', () => {
|
||||||
|
$('logText').value = buildLogText();
|
||||||
|
$('logout').classList.add('show');
|
||||||
|
});
|
||||||
|
$('copyBtn').addEventListener('click', async () => {
|
||||||
|
try { await navigator.clipboard.writeText($('logText').value); $('copied').textContent = 'Copied! Paste it back in the chat.'; }
|
||||||
|
catch (_) { $('logText').select(); document.execCommand('copy'); $('copied').textContent = 'Selected — long-press Copy.'; }
|
||||||
|
});
|
||||||
|
$('closeBtn').addEventListener('click', () => $('logout').classList.remove('show'));
|
||||||
|
$('go').addEventListener('click', start);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user