--- title: SingleViewHeigthEstimation emoji: ๐Ÿข colorFrom: pink colorTo: pink sdk: gradio sdk_version: 6.27.0 python_version: '3.12' app_file: app.py pinned: false license: mit --- # Single View Height Estimation Per-pixel above-ground height from one optical aerial or satellite image, with an interactive 3D inspector for the result. ## Using the Space Upload an image, state its ground sampling distance in metres per pixel, and run. The GSD is not cosmetic โ€” every height, slope and distance the viewer reports is derived from it, so a wrong GSD gives a plausible-looking surface with wrong numbers. A georeferenced GeoTIFF carries its own resolution; leave the field at its default to use that, or set it to override. Optional **test-time augmentation** flip-averages the prediction. It is steadier on noisy input; all 8 views of each tile run in a single GPU batch, so it costs 8ร— the compute but far less than 8ร— the wall time on a large GPU. ## What comes back * **DSM viewer & 3D flythrough** โ€” the same standalone viewer that ships in `viewer/`, inlined into the page. Orbit, first-person (`F`) and cinematic drone cameras; an optical / height-colormap / hillshade drape; vertical exaggeration; a movable sun with cast shadows; a 2-D map pane with optional contours; and click-to-probe, which reports a height, and on a second click a ground distance, slope and elevation profile between the two points. * **Derived rasters** โ€” the turbo height colormap and a 315ยฐ/45ยฐ hillshade. Hillshade is the honest test of a DSM: flat ground has to read as flat. * **Height16** โ€” the height field as an 8-bit PNG carrying 16 bits of precision across its red and green channels: `height_m = min + (Rยท256 + G) / 65535 ยท (max โˆ’ min)`. This is what the viewer decodes, because a browser down-converts a true 16-bit PNG to 8 bits โ€” which would quantise a 0โ€“60 m scene into 0.23 m steps before anything is drawn. * **Artefacts** โ€” a zip plus the headline files: `ndsm_m.npy` (bit-exact float32 metres), `meta.json` (the full contract, including the encoding affine), `terrain.glb` (a self-contained binary glTF with the optical image as its base-colour texture โ€” opens in any glTF viewer), `terrain.obj` + `.mtl` + `texture.png` for MeshLab or Blender, and the two rasters. * **Object classes** โ€” `seg.png`, the segmentation head's class per pixel on the height grid (8-bit greyscale ids; the id โ†’ name legend is `meta.json` `classes`: other, ground, low_veg, building, water, road, tree), plus a colour preview in the gallery. * **3D objects** โ€” `objects.json` (`infer/objects.py`): trees as `{x, y, h, r}` (crown centre, top height, crown radius), buildings as footprint polygons with a median roof height, and water outlines. Positions are pixels on the height grid; heights and radii are metres. Trees are split into crowns by a watershed on the smoothed canopy seeded at its peaks, so touching crowns come out as separate trees. The gallery's "Detected trees, buildings and water" overlay is the check: every circle should sit on a tree. A failure in this step is reported in the status and never costs the height map. Heights are metres above local ground (nDSM). An input without georeferencing yields a relative surface (`rDSM`); `meta.json` says which you got, and does not publish an affine transform for a scene the demo resampled. ## Layout | path | what it is | |---|---| | `app.py` | the Space: inference, encodings, artefacts, and the viewer iframe | | `infer/`, `dwdata/`, `models/` | the model and the preprocessing contract stored in the checkpoint | | `infer/objects.py` | class map + height map โ†’ trees / buildings / water (`objects.json`) | | `tests/` | CPU-only unit tests for the object step (`pytest -q`; no torch needed) | | `viz/` | figure primitives, the mesh writers, and the standalone validation report | | `viewer/` | the dual-pane DSM inspector โ€” `index.html` + `app.js` + vendored three.js | | `predict_image.py` | single-image CLI, writing the older `phase0_viewer.html` | `viewer/index.html` has no build step and no network dependency: opened straight off disk it behaves exactly as it does in the Space, and takes a prediction three ways โ€” its file picker, a `?result=` URL, or an inlined payload, which is how `app.py` hands it a result. ## Running the model The Space reads its checkpoint from `/data/best.pt`, so a Hugging Face bucket containing `best.pt` must be mounted at `/data`, and `HF_TOKEN` must be set as a Space secret for the DINOv3 encoder download. ## Area mode (GeoLibre plugin) Two API-only endpoints serve the DepthWizard GeoLibre plugin: * `/catalog` โ€” the tile repository's footprints (GeoJSON). * `/predict_area(bbox_json, tta)` โ€” `"[west, south, east, north]"` in WGS84. The tiles under the box are stitched onto a 0.5 m UTM grid (`tiles/mosaic.py`), run through the same model, and returned georeferenced: `ndsm_m.tif`, `height_overlay.png` (map overlay, corners in the result's `corners_wgs84`), `objects.geojson` (WGS84), `ndsm_rgb.png` + `imagery.jpg` (Terrain-RGB heights and their texture on the same corners, for the GeoLibre plugin's 3D view), plus the usual artefacts. A partly covered box is processed over its covered part (`coverage` < 1); an uncovered one returns `status: "not_available"`. The repository is a folder at `DW_TILES_DIR` (default `/data/tiles`, i.e. the mounted bucket): `catalog.geojson` + one COG per tile, built from GeoTIFFs with `python -m tiles.build_catalog --out `. `pytest tests/test_tiles.py` covers it without torch.