akashch1512's picture
Add tile repository and processing modules for area mode
1893d29
|
Raw History Blame Contribute Delete
5.66 kB

A newer version of the Gradio SDK is available: 6.31.0

Upgrade
metadata
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=<directory> 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 <tifs> --out <dir>. pytest tests/test_tiles.py covers it without torch.