Dororo99's picture
Add files using upload-large-folder tool
cf374fc verified
Raw
History Blame Contribute Delete
9.25 kB
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>S3GS Waymo Eval Viewer</title>
<style>
:root { color-scheme: dark; --bg:#0b0f14; --panel:#121922; --text:#e7edf5; --muted:#9fb0c3; --accent:#6ee7ff; --line:#263342; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--text); font:14px/1.45 system-ui, -apple-system, Segoe UI, sans-serif; }
header { position:sticky; top:0; z-index:10; background:rgba(18,25,34,.96); border-bottom:1px solid var(--line); padding:10px 14px; }
.row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
h1 { font-size:16px; margin:0 14px 0 0; color:var(--accent); white-space:nowrap; }
label { color:var(--muted); font-size:12px; }
select, input[type="number"], input[type="range"], button { background:#0f1620; color:var(--text); border:1px solid #344456; border-radius:8px; padding:7px 9px; }
button { cursor:pointer; font-weight:650; }
button:hover { border-color:var(--accent); }
input[type="range"] { width:min(520px, 42vw); padding:0; }
.hint { color:var(--muted); margin-left:auto; font-size:12px; }
main { height:calc(100vh - 78px); display:grid; grid-template-rows:1fr auto; padding:10px; }
#stage { width:100%; height:100%; overflow:hidden; background:#05080c; border:1px solid #1e2a36; border-radius:14px; padding:10px; display:flex; align-items:center; justify-content:center; }
.viewer { width:100%; height:100%; display:grid; grid-template-rows:auto 1fr 1fr; gap:8px; }
.cam-labels { display:grid; grid-template-columns:78px repeat(3, 1fr); gap:8px; color:#b9c7d7; font-weight:700; text-align:center; letter-spacing:.02em; }
.image-row { display:grid; grid-template-columns:78px repeat(3, 1fr); gap:8px; min-height:0; }
.row-label { display:flex; align-items:center; justify-content:center; writing-mode:vertical-rl; transform:rotate(180deg); color:var(--accent); font-weight:800; border:1px solid var(--line); border-radius:10px; background:#08101a; }
.cell { position:relative; min-width:0; min-height:0; display:flex; align-items:center; justify-content:center; background:#02050a; border:1px solid #172231; border-radius:10px; overflow:hidden; }
.cell img { max-width:100%; max-height:100%; width:100%; height:100%; object-fit:contain; }
.badge { position:absolute; left:8px; top:7px; padding:2px 6px; border-radius:999px; background:rgba(0,0,0,.58); color:#dce8f5; font:11px ui-monospace, SFMono-Regular, Menlo, monospace; }
#status { padding:8px 12px 0; color:var(--muted); font-family:ui-monospace, SFMono-Regular, Menlo, monospace; }
.pill { padding:4px 8px; border:1px solid #344456; border-radius:999px; color:#cbd7e5; background:#0f1620; }
a { color:var(--accent); text-decoration:none; }
</style>
</head>
<body>
<header>
<div class="row">
<h1>S3GS Waymo Eval Viewer</h1>
<label>Scene<br><select id="scene"></select></label>
<label>Split<br><select id="split"></select></label>
<label>Render Type<br><select id="kind"></select></label>
<button id="prev">◀ Prev</button>
<button id="next">Next ▶</button>
<label>Frame<br><input id="frame" type="number" min="0" value="0" style="width:88px"></label>
<input id="slider" type="range" min="0" value="0">
<span id="count" class="pill">-</span>
<span class="hint">Eval 기준은 test_videos 75 images. 한 프레임 = FRONT_LEFT / FRONT / FRONT_RIGHT. ←/→ or A/D, Home/End, Space autoplay</span>
</div>
</header>
<main>
<div id="stage">
<div class="viewer">
<div class="cam-labels"><div></div><div>FRONT_LEFT</div><div>FRONT</div><div>FRONT_RIGHT</div></div>
<div class="image-row" id="gtRow"><div class="row-label">GT</div></div>
<div class="image-row" id="predRow"><div class="row-label" id="predLabel">RENDER</div></div>
</div>
</div>
<div id="status"></div>
</main>
<script>
const camNames = ['FRONT_LEFT', 'FRONT', 'FRONT_RIGHT'];
const orderKinds = ['rgbs','depths','dynamic_rgbs','static_rgbs'];
const $ = id => document.getElementById(id);
let manifest, state = {scene:null, split:null, kind:null, idx:0, timer:null};
function frameName(i){ return String(i).padStart(3,'0') + '.png'; }
function fillSelect(sel, values, preferred){
sel.innerHTML = '';
values.forEach(v => { const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); });
if(values.includes(preferred)) sel.value = preferred;
}
function sceneData(){ return manifest.scenes[state.scene] || {}; }
function splitData(){ return sceneData()[state.split] || {}; }
function info(kind){ return splitData()[kind]; }
function timeCount(){
const gt = info('gt_rgbs');
const kd = info(state.kind);
const count = Math.min(gt?.count || 0, kd?.count || 0);
return Math.floor(count / 3);
}
function updateCombos(changed){
const scenes = Object.keys(manifest.scenes).sort((a,b)=>Number(a)-Number(b));
if(!state.scene) state.scene = scenes.includes('103') ? '103' : scenes[0];
fillSelect($('scene'), scenes, state.scene); state.scene = $('scene').value;
const splits = Object.keys(sceneData());
if(changed === 'scene' || !splits.includes(state.split)) state.split = splits.includes('test_videos') ? 'test_videos' : splits[0];
fillSelect($('split'), splits, state.split); state.split = $('split').value;
let kinds = Object.keys(splitData()).filter(k => k !== 'gt_rgbs');
kinds.sort((a,b)=>orderKinds.indexOf(a)-orderKinds.indexOf(b));
if(changed === 'scene' || changed === 'split' || !kinds.includes(state.kind)) state.kind = kinds.includes('rgbs') ? 'rgbs' : kinds[0];
fillSelect($('kind'), kinds, state.kind); state.kind = $('kind').value;
}
function clampIdx(){
const max = Math.max(0, timeCount() - 1);
state.idx = Math.max(0, Math.min(max, Number(state.idx)||0));
$('frame').max = max; $('slider').max = max;
$('frame').value = state.idx; $('slider').value = state.idx;
$('count').textContent = `${state.idx+1} / ${max+1}`;
}
function imgSrc(kind, camIdx){
const inf = info(kind);
if(!inf) return '';
return inf.prefix + frameName(state.idx * 3 + camIdx);
}
function ensureCells(rowId){
const row=$(rowId);
while(row.children.length < 4){
const cell=document.createElement('div'); cell.className='cell';
const badge=document.createElement('div'); badge.className='badge';
const img=document.createElement('img');
cell.appendChild(badge); cell.appendChild(img); row.appendChild(cell);
}
}
function renderRow(rowId, kind){
ensureCells(rowId);
const row=$(rowId);
for(let c=0;c<3;c++){
const cell=row.children[c+1];
cell.querySelector('.badge').textContent = camNames[c];
const img=cell.querySelector('img');
img.src = imgSrc(kind, c);
img.alt = `${state.scene} ${state.split} ${kind} ${camNames[c]} frame ${state.idx}`;
}
}
function render(){
updateCombos(); clampIdx();
renderRow('gtRow', 'gt_rgbs');
renderRow('predRow', state.kind);
$('predLabel').textContent = state.kind.toUpperCase();
const gt0 = imgSrc('gt_rgbs', 0), pred0 = imgSrc(state.kind, 0);
$('status').innerHTML = `${state.scene} / ${state.split} / frame ${state.idx} &nbsp; GT top, ${state.kind} bottom &nbsp; <a href="${pred0}" target="_blank">open current render LEFT</a> · <a href="${gt0}" target="_blank">open current GT LEFT</a>`;
const url = new URL(location.href); url.searchParams.set('scene', state.scene); url.searchParams.set('split', state.split); url.searchParams.set('kind', state.kind); url.searchParams.set('idx', state.idx); history.replaceState(null,'',url);
}
function step(d){ state.idx += d; clampIdx(); render(); }
function setFromControls(changed){
state.scene=$('scene').value; state.split=$('split').value; state.kind=$('kind').value; state.idx=Number($('frame').value)||0;
updateCombos(changed); clampIdx(); render();
}
function togglePlay(){
if(state.timer){ clearInterval(state.timer); state.timer=null; return; }
state.timer=setInterval(()=>{ const n=timeCount(); if(!n) return; state.idx=(state.idx+1)%n; render(); }, 350);
}
$('prev').onclick=()=>step(-1); $('next').onclick=()=>step(1);
$('scene').onchange=()=>setFromControls('scene'); $('split').onchange=()=>setFromControls('split'); $('kind').onchange=()=>setFromControls('kind');
$('frame').onchange=()=>setFromControls('frame'); $('slider').oninput=e=>{ state.idx=Number(e.target.value); render(); };
document.addEventListener('keydown', e=>{
if(['INPUT','SELECT'].includes(document.activeElement.tagName)) return;
if(e.key==='ArrowLeft'||e.key.toLowerCase()==='a') step(-1);
else if(e.key==='ArrowRight'||e.key.toLowerCase()==='d') step(1);
else if(e.key==='Home'){ state.idx=0; render(); }
else if(e.key==='End'){ state.idx=Math.max(0,timeCount()-1); render(); }
else if(e.key===' ') { e.preventDefault(); togglePlay(); }
});
fetch('manifest.json').then(r=>r.json()).then(m=>{
manifest=m;
const p=new URLSearchParams(location.search);
state.scene=p.get('scene'); state.split=p.get('split'); state.kind=p.get('kind'); state.idx=Number(p.get('idx')||0);
render();
});
</script>
</body>
</html>