akashch1512's picture
Add tile repository and processing modules for area mode
1893d29
|
Raw History Blame Contribute Delete
5.66 kB
---
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.