Download viewer/phase0_viewer.html from akashch1512/SingleViewHeigthEstimation: direct link, hf CLI and curl.
- Browser
- Download file 8.21 kB
-
https://huggingface.co/spaces/akashch1512/SingleViewHeigthEstimation/resolve/main/viewer/phase0_viewer.html
- Command line
-
hf download hf://spaces/akashch1512/SingleViewHeigthEstimation/viewer/phase0_viewer.html
-
curl -L -o phase0_viewer.html https://huggingface.co/spaces/akashch1512/SingleViewHeigthEstimation/resolve/main/viewer/phase0_viewer.html
8.21 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>DepthWizard — Phase 0 Viewer</title> | |
| <style> | |
| :root { color-scheme: dark; } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; font: 13px/1.5 system-ui, sans-serif; background: #0d1013; color: #e6e6e6; overflow: hidden; } | |
| #ui { | |
| position: fixed; top: 12px; left: 12px; z-index: 10; width: 300px; | |
| background: rgba(18,22,26,.92); border: 1px solid #2b3138; border-radius: 8px; padding: 14px; | |
| } | |
| #ui h1 { margin: 0 0 4px; font-size: 14px; } | |
| #ui p.sub { margin: 0 0 12px; color: #8a949e; } | |
| #ui label { display: block; margin: 10px 0 3px; color: #b8c2cc; } | |
| #ui input[type=range] { width: 100%; } | |
| #ui .row { display: flex; gap: 8px; align-items: center; } | |
| #ui button { | |
| margin-top: 12px; width: 100%; padding: 8px; background: #2d6cdf; color: #fff; | |
| border: 0; border-radius: 6px; cursor: pointer; font-weight: 600; | |
| } | |
| #ui button.secondary { background: #333b45; } | |
| #meta { margin-top: 10px; padding-top: 10px; border-top: 1px solid #2b3138; color: #8a949e; white-space: pre-wrap; } | |
| #hint { position: fixed; bottom: 12px; left: 12px; color: #6b7580; z-index: 10; } | |
| canvas { display: block; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="ui"> | |
| <h1>DepthWizard · Phase 0</h1> | |
| <p class="sub">Predicted nDSM → 3D mesh, RGB draped.</p> | |
| <label>Load export from <code>outputs/v1/viewer_sample/</code></label> | |
| <input type="file" id="files" multiple accept=".png,.json" /> | |
| <label>Vertical exaggeration <span id="vexLabel">2.0×</span></label> | |
| <input type="range" id="vex" min="0.2" max="8" step="0.1" value="2" /> | |
| <label>Wireframe</label> | |
| <div class="row"><input type="checkbox" id="wire" /> <span>show mesh grid</span></div> | |
| <button id="fp">Toggle first-person (WASD + drag)</button> | |
| <button id="reset" class="secondary">Reset camera</button> | |
| <div id="meta">Pick rgb.png, height16.png and meta.json…</div> | |
| </div> | |
| <div id="hint">drag = orbit · scroll = zoom · first-person: WASD + mouse-drag</div> | |
| <script src="vendor/three.min.js"></script> | |
| <script src="vendor/OrbitControls.js"></script> | |
| <script src="vendor/PointerLockControls.js"></script> | |
| <script> | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0d1013); | |
| scene.fog = new THREE.Fog(0x0d1013, 400, 1600); | |
| const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 6000); | |
| const HOME = new THREE.Vector3(0, 260, 340); | |
| camera.position.copy(HOME); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); | |
| renderer.setSize(innerWidth, innerHeight); | |
| if ('outputEncoding' in renderer) renderer.outputEncoding = THREE.sRGBEncoding; | |
| document.body.appendChild(renderer.domElement); | |
| const orbit = new THREE.OrbitControls(camera, renderer.domElement); | |
| orbit.target.set(0, 0, 0); | |
| orbit.maxPolarAngle = Math.PI * 0.495; | |
| const fpc = new THREE.PointerLockControls(camera, renderer.domElement); | |
| const keys = {}; | |
| addEventListener('keydown', e => keys[e.code] = true); | |
| addEventListener('keyup', e => keys[e.code] = false); | |
| scene.add(new THREE.HemisphereLight(0xbfd4ff, 0x202020, 1.1)); | |
| const sun = new THREE.DirectionalLight(0xffffff, 1.6); | |
| sun.position.set(-300, 500, 200); | |
| scene.add(sun); | |
| let mesh = null; | |
| let meta = { height_min_m: 0, height_max_m: 1, gsd_m: 0.33, size_px: 512 }; | |
| const loader = new THREE.TextureLoader(); | |
| const store = {}; | |
| document.getElementById('files').addEventListener('change', async e => { | |
| for (const f of e.target.files) { | |
| const url = URL.createObjectURL(f); | |
| if (f.name.endsWith('.json')) meta = JSON.parse(await f.text()); | |
| else if (f.name.toLowerCase().includes('rgb')) store.rgb = url; | |
| else if (f.name.toLowerCase().includes('height')) store.height = url; | |
| } | |
| if (store.rgb && store.height) buildMesh(); | |
| }); | |
| function buildMesh() { | |
| loader.load(store.height, hTex => { | |
| // read the 16-bit-ish PNG back to a Float32 displacement grid | |
| const img = hTex.image; | |
| // 1 world unit == 1 pixel of the prediction grid, which is what the | |
| // metres -> pixel conversion below assumes, so take the extents straight | |
| // from the height PNG. meta.size_px is only a sanity check (and its [w, h] | |
| // vs [h, w] order differs between v2 and v3 exports). | |
| const worldX = img.width, worldZ = img.height; | |
| const c = document.createElement('canvas'); | |
| c.width = img.width; c.height = img.height; | |
| const ctx = c.getContext('2d'); | |
| ctx.drawImage(img, 0, 0); | |
| const data = ctx.getImageData(0, 0, img.width, img.height).data; | |
| const span = (meta.height_max_m - meta.height_min_m) || 1; | |
| const hmin = isFinite(meta.height_min_m) ? meta.height_min_m : 0; | |
| const gsd = (typeof meta.gsd_m === 'number' && meta.gsd_m > 0) ? meta.gsd_m : 0.33; | |
| // Decimate the grid so the index count stays under the WebGL | |
| // max-vert-ids-per-draw limit (~30M indices => ~2200 segments/side). | |
| const MAX_SEG = 2000; | |
| const segX = Math.min(img.width - 1, MAX_SEG); | |
| const segY = Math.min(img.height - 1, MAX_SEG); | |
| const geo = new THREE.PlaneGeometry(worldX, worldZ, segX, segY); | |
| geo.rotateX(-Math.PI / 2); | |
| const pos = geo.attributes.position; | |
| const vertsX = segX + 1; | |
| for (let i = 0; i < pos.count; i++) { | |
| const gx = i % vertsX, gy = Math.floor(i / vertsX); | |
| const px = Math.min(img.width - 1, Math.round(gx / segX * (img.width - 1))); | |
| // PlaneGeometry row gy carries uv.v = 1 - gy/segY, and textures upload with | |
| // flipY, so gy == 0 drapes image row 0. Sample the height grid in the same | |
| // top-down order or the relief ends up mirrored against the RGB. | |
| const py = Math.min(img.height - 1, Math.round(gy / segY * (img.height - 1))); | |
| const g = data[(py * img.width + px) * 4] / 255; // 8-bit read is enough for a spike | |
| const h_m = hmin + g * span; | |
| pos.setY(i, h_m / gsd); // metres -> pixel units, matches XZ scale | |
| } | |
| geo.computeVertexNormals(); | |
| loader.load(store.rgb, rgbTex => { | |
| if ('colorSpace' in rgbTex) rgbTex.colorSpace = 'srgb'; | |
| else rgbTex.encoding = THREE.sRGBEncoding; | |
| const mat = new THREE.MeshStandardMaterial({ map: rgbTex, roughness: 0.95, metalness: 0.0 }); | |
| if (mesh) { scene.remove(mesh); mesh.geometry.dispose(); } | |
| mesh = new THREE.Mesh(geo, mat); | |
| mesh.userData.baseScaleY = 1; | |
| scene.add(mesh); | |
| applyVex(); | |
| document.getElementById('meta').textContent = | |
| `tile: ${meta.stem || '?'}\n` + | |
| `height range: ${meta.height_min_m?.toFixed(1)} – ${meta.height_max_m?.toFixed(1)} m\n` + | |
| `GSD: ${meta.gsd_m} m/px · ${img.width}×${img.height}px`; | |
| }); | |
| }); | |
| } | |
| const vex = document.getElementById('vex'); | |
| function applyVex() { | |
| if (mesh) mesh.scale.y = parseFloat(vex.value); | |
| document.getElementById('vexLabel').textContent = parseFloat(vex.value).toFixed(1) + '×'; | |
| } | |
| vex.addEventListener('input', applyVex); | |
| document.getElementById('wire').addEventListener('change', e => { | |
| if (mesh) mesh.material.wireframe = e.target.checked; | |
| }); | |
| document.getElementById('reset').addEventListener('click', () => { | |
| camera.position.copy(HOME); orbit.target.set(0, 0, 0); orbit.update(); | |
| }); | |
| document.getElementById('fp').addEventListener('click', () => { | |
| if (fpc.isLocked) fpc.unlock(); else fpc.lock(); | |
| }); | |
| fpc.addEventListener('lock', () => orbit.enabled = false); | |
| fpc.addEventListener('unlock', () => orbit.enabled = true); | |
| const clock = new THREE.Clock(); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const dt = clock.getDelta(); | |
| if (fpc.isLocked) { | |
| const v = 160 * dt; | |
| if (keys.KeyW) fpc.moveForward(v); | |
| if (keys.KeyS) fpc.moveForward(-v); | |
| if (keys.KeyA) fpc.moveRight(-v); | |
| if (keys.KeyD) fpc.moveRight(v); | |
| if (keys.Space) camera.position.y += v; | |
| if (keys.ShiftLeft) camera.position.y -= v; | |
| } else { | |
| orbit.update(); | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| addEventListener('resize', () => { | |
| camera.aspect = innerWidth / innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(innerWidth, innerHeight); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |