Austrian Landscape Analysis API
Watershed segmentation + Random Forest classification of Austrian landscape from remote sensing data. Analyse any area in Austria up to 25 km² using 6 data sources, 25 object types, and 11 group types.
This API reads six remote sensing sources — 1m LiDAR elevation (DTM+DSM), 20cm aerial orthophotos, Sentinel-2 NDVI, ESA WorldCover, Sentinel-1 SAR, and Austrian cadastre footprints — then runs a two-stage pipeline:
- Watershed segmentation — Felzenszwalb over-segmentation + Region Adjacency Graph merging splits the landscape into homogeneous objects
- Random Forest classification — each segment is described by 44 features (height, shape, spectral, texture, SAR, phenology) and classified into one of 25 types
The RF model is continuously trained on cadastre + OSM ground truth across 4,000 Austrian municipalities (Katastralgemeinden). Adjacent compatible segments are grouped into 11 higher-level groups (forest, building, road network, etc.).
Overview #
The Austrian Landscape Analysis API performs automated landscape classification using watershed segmentation combined with a Random Forest classifier trained on six remote sensing data sources. It identifies 25 distinct object types (trees, buildings, roads, water, crops, excavations, etc.) and groups them into 11 higher-level group types (forest, building, road network, cropland, etc.).
The system is focused on distinguishing man-made vs. natural features and detecting terrain modification over time. Multi-temporal LiDAR data (2022, 2023, 2024) enables change detection for construction, earthworks, tree growth/felling, and more.
https://srtm-lidar-at.exe.xyz:8000.All coordinates use WGS84 (lon/lat). Processing is done in EPSG:3035. Maximum analysis area: 25 km². Austria only.
Quick Start #
Run an async landscape analysis, poll for progress, then retrieve the result:
1. Submit an async analysis
# Start an async segment analysis for a GeoJSON polygon
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/segment?async=true \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}'
{
"task_id": "a1b2c3d4-e5f6-...",
"status": "running"
}
2. Poll progress
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/segment/progress?task_id=a1b2c3d4-e5f6-...
{
"active": true,
"step": "Classifying segments",
"detail": "Random Forest prediction on 342 segments",
"elapsed": 12.4,
"done": false
}
3. Retrieve the result
# Once "done": true, fetch the full FeatureCollection
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/segment/result?task_id=a1b2c3d4-e5f6-...
?async=true and the result will be returned synchronously in a single request.
Geometry Input #
All POST analysis endpoints accept geometry in the request body in multiple formats:
| Format | Content-Type | Description |
|---|---|---|
| GeoJSON Geometry | application/json | A bare GeoJSON geometry object (Point, Polygon, MultiPolygon, etc.) |
| GeoJSON + params | application/json | {"geometry": <GeoJSON>, "dataset": "...", ...} |
| KML string | application/json | Raw KML markup as the request body |
| Coordinate string | text/plain | lon,lat (single point) or lon,lat;lon,lat;... (polygon ring) |
| File upload | multipart/form-data | Upload a file: KML, GeoJSON, Shapefile ZIP, GPX, WKT, or GeoPackage |
- All coordinates must be WGS84 (EPSG:4326) — longitude, latitude order.
- Maximum area: 25 km². Requests exceeding this will be rejected.
- Austria only — geometries outside Austrian borders will fail.
- Multi-feature inputs are unioned into a single analysis boundary.
- Point geometries are automatically buffered to a 100m radius.
File upload example:
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/segment \
-F "file=@area.kml" \
-F "include_ortho=true"
Analysis Endpoints #
The main analysis endpoint. Performs watershed segmentation on the input area and classifies every segment into one of 25 object types and 11 group types using a Random Forest classifier.
Parameters
| Parameter | Type | Description |
|---|---|---|
| dataset | string | ALS date to use. Default: 20240915 |
| min_object_size | number | Minimum segment area in m². Default: 30 |
| felz_scale | number | Felzenszwalb scale parameter. Default: 150 |
| rag_threshold | number | RAG merge threshold. Default: 0.12 |
| include_ortho | boolean | Include BEV RGBI orthophoto features (NDVI, NIR, brightness, GLCM texture). Default: false |
| include_temporal | boolean | Include 3-date DTM comparison (2022/2023/2024). Default: false |
| include_copernicus | boolean | Include Sentinel-2 NDVI + ESA WorldCover + SAR + NDVI harmonics. Default: false |
| include_cadastre | boolean | Include cadastre building footprint ground truth. Default: false |
| include_hansen | boolean | Include Hansen Global Forest Change calibration. Default: false |
| types | string | Comma-separated type filter. Example: "roof,tree,road" |
| groups | string | Comma-separated group filter. Example: "building,forest" |
| async | boolean | If true, returns 202 with a task_id for polling. Default: false |
| task_id | string | Client-provided UUID for task tracking (optional). |
Example
curl -X POST "https://srtm-lidar-at.exe.xyz:8000/api/v1/segment?include_ortho=true&include_temporal=true" \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}'
Returns a GeoJSON FeatureCollection — see Response Format for full property list.
Enrich features with DSM (Digital Surface Model) and DTM (Digital Terrain Model) elevation values. Send a GeoJSON geometry or feature collection and receive elevation-annotated features.
Example
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/elevation \
-H "Content-Type: application/json" \
-d '{"type":"Point","coordinates":[15.43,47.07]}'
Terrain characterisation: computes slope, aspect, Terrain Ruggedness Index (TRI), Topographic Position Index (TPI), and curvature for the input geometry.
Example
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/terrain \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}'
Temporal change detection between two ALS dates. Identifies 20 event types including construction, earthworks, vegetation changes, and road modifications.
Parameters
| Parameter | Type | Description |
|---|---|---|
| date_a | string | Earlier ALS date. Default: 20220915 |
| date_b | string | Later ALS date. Default: 20240915 |
| min_change | number | Minimum change threshold in metres. Default: 1.0 |
Event Types (20)
tree_growth, tree_felling, new_tree, forest_clearcut,
vegetation_growth, vegetation_loss, new_building, demolition,
construction, earthwork_fill, earthwork_cut, earthwork_grading,
earthwork_dam, earthwork_trench, earthwork_pond,
road_new, road_resurfaced, road_widened, surface_change,
unclassified_change
Example
curl -X POST "https://srtm-lidar-at.exe.xyz:8000/api/v1/changes?min_change=0.5" \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}'
Per-tree growth and felling analysis between two ALS dates. Returns individual tree-level change features.
Example
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/changes/trees \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}'
Multi-epoch change summary across all three LiDAR dates (2022 → 2023 → 2024). Provides an overview of landscape evolution over the full temporal range.
Example
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/changes/summary \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}'
Async Tasks #
Long-running analyses should be submitted with ?async=true. This returns a task_id
immediately (HTTP 202). Use the following endpoints to monitor progress, retrieve results, or abort.
Poll the progress of an async task.
| Parameter | Type | Description |
|---|---|---|
| task_id required | string | The task ID returned from the async submit. |
Response fields:
active, step, detail, elapsed,
done, error, auto_share_id
Retrieve the full result of a completed async task.
| Parameter | Type | Description |
|---|---|---|
| task_id required | string | The task ID of the completed analysis. |
Cancel a running async task.
| Parameter | Type | Description |
|---|---|---|
| task_id required | string | The task ID to cancel. |
Overlays #
Overlay endpoints return PNG images with geographic bounds in the X-Bounds
response header (south,west,north,east). Use these to render raster tiles on a map.
All endpoints accept geometry via POST.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/segment/overlay | Segment classification raster — each segment coloured by type |
| POST | /api/v1/dtm/overlay | DTM hillshade visualisation |
| POST | /api/v1/lidar/overlay | nDSM height map (viridis colour ramp) |
| POST | /api/v1/ortho/overlay | RGB orthophoto |
| POST | /api/v1/cir/overlay | CIR (Colour Infrared) false-colour composite |
| POST | /api/v1/hansen/overlay | Hansen Global Forest Change visualisation |
Example
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/ortho/overlay \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}' \
-o ortho.png -D -
X-Bounds: south,west,north,east so you can position the image accurately on a map.
Exports #
Export all layers into a single GeoPackage file. Supports async mode for large areas.
| Parameter | Type | Description |
|---|---|---|
| layers | string | all (default) or comma-separated layer IDs — see table below. |
| types | string | Segment type filter. Example: "tree,road" |
| height_min | number | Keep segments with height ≥ this value (metres) |
| height_max | number | Keep segments with height ≤ this value (metres) |
| height_op | string | gt, lt, or between. Auto-inferred from min/max if omitted. |
| color_mode | string | type (default) or height — segment colouring scheme. |
| async | boolean | If true, returns 202 with {task_id} for polling. Default: false |
Available Layers
| Layer ID | Contents | Format |
|---|---|---|
dtm | DTM + DSM + nDSM | Raw 1m float32 |
segments | Segment type + height rasters | Classified raster |
ortho-YYYY | Orthophoto RGBI for that year | RGBA |
cir-YYYY | CIR false-colour (NIR→R, R→G, G→B) | RGBA |
raster | Coloured segment overlay | RGBA |
hansen | Hansen forest change overlay | RGBA |
dtm-YYYY | DTM hillshade overlay | RGBA |
dsm-YYYY | nDSM height overlay | RGBA |
Replace YYYY with a year, e.g. ortho-2024, dtm-2023.
Use layers=all to include everything.
Async download
GET /api/v1/export/geopackage/download/<task_id> — fetch the completed file.
Share shortcut
GET /api/v1/share/<id>/download.gpkg — download directly from a saved share.
Accepts layers=all (default), active (from share's UI state), or comma-separated IDs.
Example
# Export all layers as GeoPackage (async)
curl -X POST "https://srtm-lidar-at.exe.xyz:8000/api/v1/export/geopackage?async=true&layers=dtm,segments,raster" \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}'
# Download from share (all layers)
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/share/Kohlschwarz80/download.gpkg -o analysis.gpkg
# Download from share (specific layers)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/share/Kohlschwarz80/download.gpkg?layers=dtm,ortho-2024" -o layers.gpkg
? for raster layer CRS (does not auto-detect it),
manually set the coordinate reference system to EPSG:3035 – ETRS89-extended / LAEA Europe.
Export a single raster layer as MBTiles for offline use in mapping applications.
| Parameter | Type | Description |
|---|---|---|
| layer | string | Required. Layer ID, e.g. dtm-2024, ortho-2024, raster, hansen. |
| min_zoom | integer | Minimum zoom level. |
| max_zoom | integer | Maximum zoom level. |
| async | boolean | If true, returns 202 with {task_id}. Default: false |
Async download: GET /api/v1/export/mbtiles/download/<task_id>
Raw GeoTIFF Downloads
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/lidar/geotiff | Download raw DTM, DSM, or nDSM as GeoTIFF |
| POST | /api/v1/ortho/geotiff | Download orthophoto RGBI as GeoTIFF |
Example
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/lidar/geotiff \
-H "Content-Type: application/json" \
-d '{"type":"Polygon","coordinates":[[[15.43,47.07],[15.44,47.07],[15.44,47.08],[15.43,47.08],[15.43,47.07]]]}' \
-o lidar.tif
One-Stop URL #
A single bookmarkable GET URL that triggers segmentation, auto-saves the result, and delivers a downloadable file. Designed for users on limited or mobile connections who want to run an analysis without the web UI.
- Start:
GET /api/v1/onestop?bbox=...&format=gpkg→ returns 202 withtask_idandpoll_url - Poll: Repeat the
poll_urlevery 5–10 seconds untilstatus=done - Download: The final poll automatically returns the file (GPKG, KML, or JSON)
The result is auto-saved as a share, so you can re-download later via the Load menu (⬇ icon) or the share URL.
Segment a bounding box and download the result in one step. All parameters are in the URL — no POST body needed.
URL Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | string | Required. Bounding box: lon_min,lat_min,lon_max,lat_max (WGS 84) |
| name | string | Save name / share ID for the result. 1–80 chars, [A-Za-z0-9_-]. If omitted, defaults to auto-<task_id>. |
| format | string | Output format: json (default), gpkg, or kml |
| min_object_size | integer | Min segment area in m². Default: 10 |
| include_ortho | boolean | Include 20 cm orthophoto features. Default: true |
| include_temporal | boolean | Include 3-date temporal comparison. Default: false |
| include_copernicus | boolean | Sentinel-2 NDVI + SAR + WorldCover. Default: false |
| include_cadastre | boolean | Cadastre building footprint ground truth. Default: false |
| include_hansen | boolean | Hansen Global Forest Change. Default: false |
| types | string | Object type filter. Example: tree,roof |
| height_min | number | Keep only objects with height ≥ this value (metres) |
| height_max | number | Keep only objects with height ≤ this value (metres) |
| height_op | string | gt, lt, or between. Auto-inferred from min/max if omitted. |
| layers | string | GPKG layers to include. Default: segments (raster + vector) |
| segment_geometry | string | Feature geometry: point (default for KML) or polygon (default for GPKG). Polygons are vectorised from the segment raster. |
| segment_geometry_style | string | Colour scheme: type (default) colours by object type, height colours by viridis height ramp. |
| group_by | string | KML folder grouping: type (default) or height_class |
| task_id | string | Poll a previously started task (omit bbox when using this) |
Building the URL
The URL is straightforward to construct — just append query parameters:
https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop
?bbox=lon_min,lat_min,lon_max,lat_max
&format=gpkg
&types=tree
&height_min=20
&include_ortho=true
Tip: Get a bounding box from boundingbox.klokantech.com (select CSV format) or from Google Maps by right-clicking two corners. Keep the area under 1 km² for fast results.
Height Filter
The height filter works like a WHERE clause on height_max_m. It applies to
GPKG vector segments, GPKG raster segments, KML placemarks, and JSON features.
| You want | URL parameters |
|---|---|
| Trees > 30 m | types=tree&height_min=30 |
| Objects < 5 m | height_max=5 |
| Between 10–25 m | height_min=10&height_max=25 |
| Roofs 5–15 m | types=roof&height_min=5&height_max=15 |
Timing Estimates (< 1 km²)
| Configuration | Est. time | Notes |
|---|---|---|
| Ortho only (default) | ~30–60 s | DTM + DSM + orthophoto |
| Ortho + temporal | ~60–90 s | + 3-date comparison |
| All sources | ~90–120 s | + Copernicus + cadastre + Hansen |
| GPKG / KML export | + 5–10 s | Added on top of analysis |
| Auto-save | + 1–2 s | Always happens automatically |
A 0.5 km² area with ortho takes about 40 s total. A 0.1 km² area about 25 s. On slow mobile connections, GPKG download (1–5 MB) may add 5–30 s.
Processing Queue
The server limits concurrent analyses to 2 at a time, with up to 4 in the queue.
If the queue is full, the API returns 503 Service Unavailable with retry_after_seconds.
Your task will show step: "queued" while waiting for a slot.
Examples
# Start analysis (returns task_id + poll_url)
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&height_min=20&format=gpkg'
# Response:
# {"task_id":"abc123...","status":"running","poll_url":"...?task_id=abc123...&format=gpkg","estimated_seconds":35}
# Poll until done (repeat every 5s)
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?task_id=abc123...&format=gpkg'
# When done: the response IS the .gpkg file — pipe to a file
curl -o trees.gpkg 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?task_id=abc123...&format=gpkg'
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.35,47.15,15.355,47.154&include_temporal=true&format=kml&group_by=height_class'
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=roof&height_min=5&height_max=15&include_cadastre=true&format=gpkg'
# The result will be saved as share "Graz-Trees" instead of auto-<task_id>
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&height_min=20&format=gpkg&name=Graz-Trees'
# Later, load or download via the share URL:
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/share/Graz-Trees'
curl -o trees.gpkg 'https://srtm-lidar-at.exe.xyz:8000/api/v1/share/Graz-Trees/download.gpkg'
# Trees as polygon outlines, coloured by height ramp (viridis), grouped by height class
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&types=tree&format=kml&segment_geometry=polygon&segment_geometry_style=height&group_by=height_class'
# Polygons coloured by type (default style)
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=kml&segment_geometry=polygon'
# Points only (default, fast — no raster vectorisation)
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=kml&segment_geometry=point'
# GPKG with height-coloured polygons (segment_geometry defaults to polygon for GPKG)
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=gpkg&segment_geometry_style=height'
# GPKG with points only (skip vectorisation, smaller file)
curl 'https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop?bbox=15.40,47.07,15.405,47.076&format=gpkg&segment_geometry=point'
#!/bin/bash
# Analyse a bounding box and download the GPKG when ready
BASE="https://srtm-lidar-at.exe.xyz:8000/api/v1/onestop"
BBOX="15.40,47.07,15.405,47.076"
# Start
RESP=$(curl -s "$BASE?bbox=$BBOX&types=tree&height_min=20&format=gpkg")
TASK=$(echo $RESP | python3 -c "import json,sys; print(json.load(sys.stdin)['task_id'])")
echo "Task: $TASK"
# Poll every 5 seconds
while true; do
HTTP_CODE=$(curl -s -o /tmp/result.gpkg -w '%{http_code}' "$BASE?task_id=$TASK&format=gpkg")
if [ "$HTTP_CODE" = "200" ]; then
echo "✅ Downloaded /tmp/result.gpkg"
break
fi
echo "⏳ Still processing (HTTP $HTTP_CODE)..."
sleep 5
done
Download from the Load menu
Results created via one-stop URLs appear in the 📂 Load dropdown in the web UI. Each one-stop entry shows a ⬇ icon for a quick GeoPackage download — no need to load the full analysis first.
Shares #
Save and retrieve analysis results for later use. Shares are automatically created for async tasks, or can be manually saved.
List saved shares.
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Maximum number of shares to return. Default: 20 |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/share | Save a new share (POST analysis result as body) |
| GET | /api/v1/share/<id> | Load a saved share by ID |
| POST | /api/v1/share/<old_id>/rename | Rename a share |
| GET | /api/v1/share/<id>/download.gpkg | Download share as GeoPackage file |
Example
# List recent shares
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/shares?limit=5
# Load a share
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/share/abc123
# Download as GeoPackage
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/share/abc123/download.gpkg -o result.gpkg
Utilities #
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/layers?bbox=... | List available data layers for a bounding box |
| GET | /api/v1/info | API version, status, and capabilities |
| POST | /api/v1/parse-geometry | Parse and validate geometry input (useful for debugging) |
| POST | /api/v1/classifier/train | Trigger classifier retraining |
| GET | /api/v1/classifier/status | Current classifier status and accuracy metrics |
| GET | /api/v1/training/status | Training data statistics and coverage |
Examples
# Check API info
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/info
# List layers for a bounding box
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/layers?bbox=15.4,47.0,15.5,47.1"
# Parse and validate geometry
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/parse-geometry \
-H "Content-Type: application/json" \
-d '{"type":"Point","coordinates":[15.43,47.07]}'
Search Index #
A pre-built SQLite FTS5 + R-tree index over all 8,440 Austrian Katastralgemeinden (KGs). Queries are fast (<25ms for index lookups) and support full-text search, spatial queries, admin hierarchy, object type rankings, and compound filters.
Pagination #
All list-returning query modes return a paginated envelope:
{
"total": 1234, // total matching records (before limit/offset)
"offset": 0, // current offset
"limit": 100, // page size
"results": [...] // array of KG records
}
Use limit= and offset= query parameters to paginate.
Default limit is 100, max is 1000.
Single-item lookups (kg=, parcel=) return the item directly without the envelope.
Aggregate queries (aggregate=true) return aggregate stats directly.
/api/v1/query
Unified query endpoint
Supports multiple query modes via query parameters. Pick one mode per request.
| Parameter | Type | Description |
|---|---|---|
| q | string | Full-text search (KG/gemeinde/district/state names) |
| kg | string | Exact KG code lookup |
| parcel | string | Parcel lookup (format: KGCODE-GNR) |
| bbox | string | Spatial R-tree query: w,s,e,n |
| point | string | Point proximity: lon,lat (sorted by distance) |
| state / district / gemeinde | string | Admin hierarchy filter (code or name) |
| type | string | Rank KGs by object type. Combine with metric= |
| hansen | boolean | Hansen forest loss query. Combine with year_from, year_to |
| new_buildings | boolean | KGs with new uncadastred buildings |
| segments | boolean | Segment-level power queries. See Segment Power Queries below. |
| divergence | boolean | KGs ranked by RF→final type divergence |
| parcels_by_type | string | Per-parcel filter by type + RF confidence (slow, async-capable) |
| top_features | string | Cross-KG features: trees|objects|new_buildings|infrastructure (slow, async-capable) |
| Modifiers (combine with any mode above) | ||
| limit | int | Max results per page. Default: 100, max: 1000 |
| offset | int | Pagination offset. Default: 0 |
| async | boolean | Run slow queries asynchronously (parcels_by_type, top_features). Returns task_id to poll. |
| task_id | string | Poll an async query task. Returns 202 while running, result when done. |
| aggregate | boolean | Return aggregate stats instead of KG list |
| processed_only | boolean | Only return processed KGs |
| min_confidence | float | Min RF confidence threshold |
| min_area_sqm | float | Min area in m² for type filter |
# Paginated text search
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/query?q=Wien&limit=10&offset=0
# → {"total": 23, "offset": 0, "limit": 10, "results": [...]}
# Page 2
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/query?q=Wien&limit=10&offset=10
# Spatial query with pagination
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/query?bbox=15.0,47.0,16.0,48.0&limit=50&offset=0
# Admin hierarchy
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/query?state=Vorarlberg&limit=25&offset=0Async Queries #
Slow queries that scan KG JSON files (parcels_by_type, top_features)
support asynchronous execution with async=true. This is recommended when querying
across many KGs to avoid HTTP timeouts.
# 1. Start async query
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=trees&min_confidence=0.9&async=true
# → 202 {"task_id": "abc-123", "status": "running", "poll": "/api/v1/query?task_id=abc-123"}
# 2. Poll until done
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/query?task_id=abc-123
# → 202 {"active": true, "step": "running", "elapsed": 3.2} (while running)
# → 200 {"total": 456, "offset": 0, "limit": 100, "results": [...]} (when done)
# Alternative: dedicated progress endpoint
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/query/progress?task_id=abc-123| Endpoint | Purpose |
|---|---|
GET /api/v1/query?task_id=X | Poll async query — returns 202 while running, result (200) when done, error (500) on failure |
GET /api/v1/query/progress?task_id=X | Dedicated progress endpoint — same behavior |
Async results are auto-cleaned after 4 hours.
Compound Filter #
/api/v1/query/compound
Filter KGs by any combination of attributes
GET with query params or POST a JSON body. All filters are pure SQL on indexed columns — very fast.
Returns paginated {total, offset, limit, results}.
Supports async=true; poll with task_id=<id>.
GET syntax: flat params (state=Vorarlberg&min_slope=15),
aspect=S,SW,W (comma-separated),
type_filter=tree:0.8:800 (repeatable, type:confidence:area),
landcover_filter=grass:1300:0.1 (repeatable, type:area:fraction).
| Filter key | Type | Description |
|---|---|---|
| bbox | [w,s,e,n] | Spatial bounding box |
| state / district / gemeinde | string | Admin filter |
| aspect | ["S","SW",...] | Dominant aspect direction |
| dominant_type / phenology / quality_grade | string | Exact match filters |
| min_/max_ + numeric field | float | Numeric ranges: slope, elevation, ndvi, tree_count, building_height, sar_vv, etc. |
| type_filters | array | [{"type": "tree", "min_confidence": 0.8, "min_area_sqm": 800}] |
| landcover_filters | array | [{"type": "crop", "min_fraction": 0.2, "max_height_mean": 1.0}] |
| sort / sort_dir | string | Sort column + direction (asc/desc) |
| limit / offset | int | Pagination. Default limit=50, max 1000 |
| async | boolean | Run asynchronously. Returns {task_id} |
# Trees + grass with high RF confidence, no buildings, rugged terrain
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?type_filter=tree:0.8:800&type_filter=grass:0.8:1300&max_buildings=0&aspect=S,SW,W&min_roughness=2.0"
# → {"total": 47, "offset": 0, "limit": 50, "results": [...]}
# High-elevation steep terrain with lots of trees, page 3
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?min_elevation=1500&min_slope=15&min_tree_count=100&sort=tree_canopy_sqm&sort_dir=desc&limit=20&offset=40"/api/v1/index/status
Index statistics
Returns kg_count, processed_count, total_area_km2, zenodo_kgs, and more.
/api/v1/index/rebuild
Rebuild search index
Rebuilds from scratch (~0.3s). Picks up new processor results and Zenodo manifest entries.
Processing Queue Management #
Control which KGs get processed next. The priority queue is a persistent ordered list; the processor pops from the front.
/api/v1/processing/queue
Read current priority queue
Returns the ordered list of KG numbers waiting to be processed.
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue/api/v1/processing/queue
Add KGs at a specific position
Insert one or more KGs into the queue at a given position. Use skip_processed: true to ignore already-processed KGs.
POST /api/v1/processing/queue
Content-Type: application/json
{
"kgs": ["49006", "49013"],
"position": 2,
"skip_processed": true
}Example — Prioritise Gesäuse + Kalkalpen national park KGs:
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue \
-H 'Content-Type: application/json' \
-d '{"kgs": ["49006","49013","49106","49311","49313","49321","49405","49406","49407","49409","49412","60102","60106","60107","67106","67107","67109","67111","67404","67405","67412","67501"],
"position": 2, "skip_processed": true}'Inserts Gesäuse & Kalkalpen KGs at position 2, right after whatever is currently processing.
/api/v1/processing/queue
Replace entire queue
Overwrites the full queue with a new ordered list.
PUT /api/v1/processing/queue
Content-Type: application/json
{
"queue": ["12345", "67890", "91109"]
}/api/v1/processing/queue?kg=X
Remove a KG from queue
Removes the specified KG number from the processing queue.
curl -X DELETE 'https://srtm-lidar-at.exe.xyz:8000/api/v1/processing/queue?kg=49006'Filter Logic: AND vs OR #
All filters within a single query combine with AND. For example:
segments=true & object_type=tree & min_rf_confidence=0.9 & min_height=20 & state=Vorarlbergmeans: type = tree AND rf_conf ≥ 0.9 AND height ≥ 20m AND state = Vorarlberg
Multiple types in object_type combine with OR (comma-separated):
object_type=tree,shrub,hedgemeans: (type = tree OR shrub OR hedge) AND all other filters.
Text search (q=) tokenises by whitespace; terms are AND’d:
q=Bregenz Gaissau matches KGs whose name/admin contains both terms.
district= and gemeinde= accept names (e.g. district=Bregenz) or numeric codes (district=802).
state= accepts names (e.g. state=Vorarlberg) or single-digit codes (state=8).
Segment Power Queries #
The search index stores the top 50 individual segments per object type per KG.
This enables cross-KG queries at segment level — find specific objects across
all of Austria without scanning full GeoPackage files.
Add segments=true to /api/v1/query.
| Parameter | Type | Description |
|---|---|---|
object_type | string | Single type or comma-separated OR list: tree, tree,shrub,hedge |
min_rf_confidence | float | Raw RF model score lower bound (0–1) |
max_rf_confidence | float | Raw RF model score upper bound |
min_confidence | float | Combined/calibrated score lower bound |
max_confidence | float | Combined/calibrated score upper bound |
min_area_sqm | float | Minimum segment area (m²) |
max_area_sqm | float | Maximum segment area |
min_height | float | Minimum height_max_m (metres) |
max_height | float | Maximum height_max_m |
min_volume | float | Minimum |volume_change_m³| (absolute) |
max_volume | float | Maximum |volume_change_m³| |
bbox | string | Spatial filter: w,s,e,n |
state | string | State name or code |
district | string | District name or code |
sort | string | height_max_m (default), height_mean_m, area_sqm, volume, rf_confidence, confidence |
sort_dir | string | desc (default) or asc |
percentile | float | 0–1: return only top N% (e.g. 0.01 = top 1%). Threshold computed from all matching segments, then applied as filter. |
limit, offset | int | Pagination (default limit=100, max 1000) |
Each result includes kg_code, object_type, lon/lat,
area_sqm, height_max_m, height_mean_m, volume_change_m3,
rf_confidence, confidence, rank, admin names,
and _links with a kg API URL and a map Google Maps link.
When percentile is used, the response also includes percentile_threshold
and percentile_count.
Example Queries
# Top 1% tallest trees with RF confidence > 90%
# Returns 1 result: 35.43m tree near Gaissau, rf_conf=0.937
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree&min_rf_confidence=0.9&percentile=0.01"
# Top 5% largest tree loss areas, RF confidence > 80%
# Returns segments ranked by area where forest has been lost
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree_loss&min_rf_confidence=0.8&percentile=0.05&sort=area_sqm"
# Top 10 excavations by volume, RF confidence > 70%
# Finds the largest ground disturbances measured in m³ of earth moved
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=excavation&min_rf_confidence=0.7&sort=volume&limit=10"
# Tallest 5 roofs with combined confidence > 70%
# Identifies the tallest buildings (by nDSM height at segment peak)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=roof&min_confidence=0.7&sort=height_max_m&limit=5"
# All tall vegetation: trees OR shrubs OR hedges above 15m, high confidence
# Multi-type OR — comma-separated types act as OR, other filters AND
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree,shrub,hedge&min_height=15&min_rf_confidence=0.7&sort=height_max_m"
# Largest building structures: roofs OR greenhouses, sorted by footprint
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=roof,greenhouse&sort=area_sqm&sort_dir=desc&min_confidence=0.7&limit=10"
# All disturbance events ranked by volume of earth moved
# Combines excavation, fill, construction, and tree_loss into one query
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=excavation,fill,construction,tree_loss&sort=volume&sort_dir=desc&limit=20"
# Trees in Bregenz district above 30m
# Combines type + spatial admin + height filter (all AND)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=tree&district=Bregenz&min_height=30&sort=height_max_m"
# Top 5% largest crop segments with high RF confidence
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?segments=true&object_type=crop&min_rf_confidence=0.8&percentile=0.05&sort=area_sqm"
More Query Examples (non-segment)
# KG lookup by code — returns full index record with landcover, hansen, links
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?kg=91109"
# Aggregate landscape stats for a state
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?state=Vorarlberg&aggregate=true"
# Aggregate stats for a district (by name)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?district=Bregenz&aggregate=true"
# Text search (terms are AND'd) — finds KGs matching BOTH terms
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?q=Bregenz+Gaissau"
# Point proximity — nearest KGs to a coordinate, sorted by distance
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?point=9.59,47.48"
# Spatial bbox, processed KGs only
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?bbox=9.55,47.46,9.62,47.50&processed_only=true"
# KGs ranked by tree coverage (by area)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?type=tree&metric=area"
# Hansen forest loss in a year range
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?hansen=true&year_from=2020&year_to=2024"
# KGs with new uncadastred buildings
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?new_buildings=true&processed_only=true"
# All KGs in Gaißau municipality
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?gemeinde=Gai%C3%9Fau"
# ── Terrain, slope, aspect, elevation filters ──
# NE-facing gentle slopes above 390m with buildings and trees
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?max_slope_mean=5&min_elevation_mean=390&min_building_count=100&min_tree_count=500&processed_only=true"
# Low slope + lush vegetation + high NDVI
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?min_vegetated_fraction=0.5&max_slope_mean=5&min_ndvi=0.3&processed_only=true"
# Dense settlements: 500+ buildings, multi-story, max height > 20m
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?min_building_count=500&min_building_stories=2&min_building_max_height=20&processed_only=true"
# High biodiversity: Shannon > 2.0, 1000+ trees, large canopy
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?min_shannon_diversity=2.0&min_tree_count=1000&min_tree_canopy_sqm=100000&processed_only=true"
# Active landscape change + high quality classification
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?min_changed_segments=100&min_quality_score=0.9&processed_only=true"
# ── Per-parcel terrain filter (GET /api/v1/parcels/batch) ──
# Compound filters as flat params, parcel filters with pf_ prefix
# All east-facing level parcels — returns parcel IDs with cadastre data
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/parcels/batch?state=Vorarlberg&pf_aspect=E&pf_terrain_class=level&limit=100"
# → 125 parcels with aspect_dominant=E and terrain_class=level
Slow Queries (async recommended)
These queries scan KG JSON files on disk and can take seconds to minutes depending on how many KGs
are processed. Use async=true to avoid HTTP timeouts — you get back a task_id
immediately and poll for the result.
# ── parcels_by_type: find individual parcels where a type has high RF confidence ──
# Scans KG JSONs → returns per-parcel results with centroid, elevation, areas
# Sync (fast if few KGs processed):
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?parcels_by_type=tree&min_confidence=0.8&min_area_sqm=1500"
# Async (recommended when many KGs are processed):
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?parcels_by_type=tree&min_confidence=0.8&min_area_sqm=1500&async=true"
# → 202 {"task_id": "abc-123", "status": "running", "poll": "/api/v1/query?task_id=abc-123"}
# Poll until done:
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?task_id=abc-123"
# → 202 while running, 200 with full result when complete
# ── top_features: cross-KG ranked features from JSON summaries ──
# Tallest trees across all processed KGs, RF confidence > 90%
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=trees&min_confidence=0.9&async=true"
# Tallest new (uncadastred) buildings, confidence > 75%
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=new_buildings&min_confidence=0.75&async=true"
# Infrastructure masts only, high confidence
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=infrastructure&type=mast&min_confidence=0.8&async=true"
# Top objects within a spatial bbox (restricts which KGs are scanned)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query?top_features=objects&bbox=9.5,47.4,9.7,47.6&min_confidence=0.7&async=true"
# ── compound filter (async): complex multi-attribute KG search ──
# GET with async=true — same poll mechanism via task_id
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/compound?type_filter=tree:0.8:800&min_elevation=1000&aspect=S,SW&min_ndvi=0.5&max_buildings=10&sort=tree_canopy_sqm&sort_dir=desc&async=true"
# → 202 {"task_id": "def-456", ...}
# Poll: /api/v1/query/compound?task_id=def-456
Per-Parcel Index Query #
GET /api/v1/query/parcels runs direct SQL against the kg_parcels index table.
Every processed parcel is indexed with terrain, vegetation, classification, Hansen, and building
attributes — all queries return in < 50 ms.
This is the endpoint for complex cross-KG parcel searches like
“south-facing parcels above 900 m with a single-storey pitched-roof building,
under 5 000 m², and little recent deforestation.”
| Parameter | Type | Description |
|---|---|---|
| Admin / spatial | ||
kg | string | Filter by KG code |
state | string | Bundesland name or code |
district | string | Bezirk name or code |
gemeinde | string | Gemeinde name or code |
bbox | string | Spatial bounding box: w,s,e,n |
| Area & elevation | ||
min_area / max_area | float | Parcel area in m² |
min_elevation / max_elevation | float | Mean elevation in metres |
| Terrain | ||
min_slope / max_slope | float | Mean slope in degrees |
min_tri / max_tri | float | Terrain Roughness Index |
terrain_class | string | level, nearly_level, slightly_rugged, rugged, … |
aspect | string | Comma-separated: N,NE,E,SE,S,SW,W,NW |
| Vegetation & land cover | ||
dominant_type | string | Dominant segment type: tree, grass, roof, … |
min_vegetated_fraction / max_vegetated_fraction | float | Vegetated fraction 0–1 |
min_forested_fraction / max_forested_fraction | float | Forested fraction 0–1 |
is_vegetated | bool | true or false |
| Height & classification | ||
min_ndsm_max / max_ndsm_max | float | Max nDSM height on parcel (m) |
min_confidence | float | Min mean classification confidence |
min_rf_confidence | float | Min RF model confidence |
| Hansen forest change | ||
min_hansen_recent_5yr / max_hansen_recent_5yr | int | Recent 5-year loss pixels |
min_hansen_total / max_hansen_total | int | Total loss pixels |
| Building join filters | ||
building_roof_type | string | pitched or flat — parcel must contain a matching building |
building_min_stories | int | Minimum estimated building stories |
building_max_stories | int | Maximum estimated building stories |
| Sorting & pagination | ||
sort | string | elevation_m (default), area_sqm, slope_mean_deg, tri_mean, vegetated_fraction, forested_fraction, ndsm_max_m, hansen_recent_5yr_pixels, hansen_total_pixels, mean_confidence, rf_mean_confidence |
sort_dir | string | desc (default) or asc |
limit | int | Max results (default 100, max 1000) |
offset | int | Pagination offset |
Each result includes all 27 parcel columns: kg_code, parcel_id,
area_sqm, centroid_lon/lat, elevation_m (+ min/max/range),
slope_mean_deg, tri_mean, tpi_mean, terrain_class,
aspect_mean_deg, aspect_dominant, vegetated_fraction,
forested_fraction, dominant_type, ndsm_max_m,
ndsm_mean_m, is_vegetated, classification counts + confidence,
and hansen_total_pixels / hansen_recent_5yr_pixels.
The response wraps results in {"total": N, "offset": 0, "limit": 100, "results": [...]}.
Example Queries
# ── “Zeig mir alle Parzellen auf denen ein Haus mit nur einem Stockwerk und
# einem Spitzdach steht, die eine Südhangneigung von über 30% haben und
# kleiner als 5000 Quadratmeter ab einer Seehöhe von 900 Metern, mit wenig
# Entwaldung in den letzten 5 Jahren.”
# (30% slope ≈ 17°)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?building_roof_type=pitched&building_max_stories=1&aspect=S,SE,SW&min_slope=17&max_area=5000&min_elevation=900&max_hansen_recent_5yr=5&sort=elevation_m&sort_dir=desc&limit=100"
# Highly forested parcels above 1500m with no recent deforestation
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?min_forested_fraction=0.7&min_elevation=1500&max_hansen_recent_5yr=0&sort=forested_fraction&sort_dir=desc"
# Large grassy parcels in Vorarlberg
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?state=Vorarlberg&dominant_type=grass&min_area=10000&sort=area_sqm&sort_dir=desc"
# Steep rocky terrain without vegetation (quarries, cliffs)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?dominant_type=rock&min_slope=25&max_vegetated_fraction=0.1&sort=slope_mean_deg&sort_dir=desc"
# Flat low-elevation parcels with high vegetation and buildings (suburban gardens)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?terrain_class=level&max_elevation=400&min_vegetated_fraction=0.6&building_min_stories=1&sort=vegetated_fraction&sort_dir=desc"
# Parcels with significant recent Hansen forest loss (monitoring deforestation)
curl "https://srtm-lidar-at.exe.xyz:8000/api/v1/query/parcels?min_hansen_recent_5yr=20&min_forested_fraction=0.3&sort=hansen_recent_5yr_pixels&sort_dir=desc"
/query/parcels, segments=true, q=, bbox=, point=, state=, district=, type=, hansen=, aggregate= — pure SQL index lookups.Medium (seconds):
compound with type_filters or landcover_filters — joins on indexed tables.Slow (seconds–minutes):
parcels_by_type, top_features — scan KG JSON files. Use async=true.
Query Explorer (UI) #
/query.html is a side-by-side
map + JSON playground for every query and feedback endpoint.
It is the fastest way to triage flags, drill into a candidate, eyeball it on satellite
imagery, and submit a confirm/reject/correction without leaving the page.
- Endpoint dropdown with one-click presets: critical flags, tallest “trees”, likely-mast misclassifications, ground-typed segments with non-zero height, RF high-confidence masts.
- Layer switcher on the map: ESRI satellite (default), basemap.at 30 cm orthophoto for Austria, OSM streets, plus a labels overlay you can toggle independently.
- List ↔ map sync: clicking a row pans & highlights the marker; clicking a marker scrolls the row into view. Severity colour-coded (critical=red, high=dark red, medium=amber, low=grey).
- Per-row “flag” link opens the same SrtmFlag popover used in
/process.html. The popover shows: matched candidate(s), aliases (when several refs collapse to the same physical object), aggregated rule weight, the action prediction (“will this flip community-effective type?”), and a notes field. - Three result tabs: Results (rendered list), Raw JSON, Summary (depth-limited tree view of the response shape).
- Permalinkable URL bar: the URL above the result list mirrors what is sent; copy it for share-able exact queries.
data-srtm-kg, data-srtm-lon,
data-srtm-lat scopes selection-based flagging. The Query Explorer sets these
on every result row so a click on “102.2m” in the row text already finds the right object.
Quality Flags & Feedback #
Every classified object goes through a distribution-aware rule pass
(quality_flags.py) immediately after the per-KG JSON is written
— and the same pass runs on app start over every JSON already in
data/austria_processor/json/, so the rules apply uniformly to
the entire processed corpus, not only newly produced KGs.
| Endpoint | Purpose |
|---|---|
GET /api/v1/flags |
List flags with filters: kg=, severity=, code=,
type=, kind=, bbox=w,s,e,n,
min_value=, order=severity|value|recent.
Each row carries an aggregate object with the
per-segment total_weight, n_flags, and
codes. |
GET /api/v1/flags/stats |
Counts by severity, code, object type, top-flagged KGs. |
GET /api/v1/flags/match?text=… |
Resolve a free-text snippet (e.g. 102.2m tree)
to one or more concrete object refs. Supports kg=,
lon=, lat=, radius_m=.
Same-segment duplicates (top_tree /
top_obj / top_by_type) collapse to a
single candidate with the others returned in aliases.
Includes per-action action_predictions. |
GET /api/v1/flags/predict |
Forecast the effect of kind=confirm|reject|correct_type
on a given obj_ref: how much weight your role adds,
whether the action would flip the community-effective
type or trigger resampling. |
GET /api/v1/flags/object/<obj_ref> |
Full record for one segment: object, flags, all feedback, override state, aggregate, audit history, and per-action predictions. |
GET /api/v1/flags/events |
Audit log of every created / changed /
removed rule flag. Filter by kg=,
obj_ref=, kind=, since=epoch.
Suitable for resampling the segments whose flag set has changed
across rule versions. |
POST /api/v1/feedback |
Submit one feedback row. Accepts {obj_ref, kind, corrected_type, notes}
or text/coord-only payloads that the server resolves through
flags/match. |
GET /api/v1/feedback /
/api/v1/feedback/events |
Active feedback rows / append-only event log of every submission, supersession or withdrawal (with role and weight). |
POST /api/v1/flags/rebuild |
Re-scan one KG (?kg=…) or all local JSONs.
Idempotent; rule changes that yield different flags emit
changed / removed events automatically. |
Severity weights
Each rule emits a flag with a severity-derived weight:
low=1, medium=2, high=4, critical=8.
When several independent rules fire on the same object, the weights
accumulate — the aggregate.total_weight in
/flags and /flags/match is the
de-duplicated sum (max-by-flag-code across top_*
aliases) so it captures “how many independent pieces of evidence point
the same way”. Use it to rank or to set a confidence threshold for
resampling.
Audit log & resampling
Two append-only event tables are persisted in data/feedback.sqlite:
flag_events— one row per creation, change, or removal of a rule flag, with fullrule_version+attrs_jsonat that moment.feedback_events— one row per user action, with theuser_role-weighted contribution and free-text notes.
Together these let you (a) reconstruct the exact flag state at any point in time, (b) rebuild a re-sampling pool of segments whose flag set or feedback changed since the last training cut, and (c) compute inter-annotator agreement when two students hit the same object.
Embeddable flag widget
/flag.js is a small
vanilla-JS module (zero deps). SrtmFlag.install() watches
text selections globally; SrtmFlag.openFor({obj_ref|text|point})
opens the popover programmatically. The popover now renders a
per-action prediction preview (“flips outcome”, “verify after
this confirm”, projected effective type) before the user submits.
Methods — Overview #
The analysis pipeline turns raw remote sensing data into a classified landscape map through four stages:
- Data acquisition — windowed reads from 8 independent data sources at 0.2–30 m resolution
- Segmentation — fused multi-layer gradient → Felzenszwalb over-segmentation → RAG boundary merge
- Feature extraction — 57 per-segment features spanning height, shape, spectral, texture, SAR, phenology, and temporal change
- Classification — Random Forest (trained on cadastre + OSM ground truth) with rule-based fallback and hierarchical grouping
Data Sources #
The system fuses eight independent remote sensing and reference data sources.
All raster data is accessed via HTTP range requests (GDAL /vsicurl/) or
the Copernicus openEO API — no bulk downloads required.
| # | Source | Resolution | Access | Role in pipeline |
|---|---|---|---|---|
| 1 | BEV ALS DTM + DSM | 1 m | HTTP range | Height model, terrain, temporal change |
| 2 | BEV DOP Orthophoto | 0.2 m | HTTP range | NDVI, spectral bands, texture (GLCM) |
| 3 | Sentinel-2 NDVI | 10 m | openEO | Growing-season NDVI, monthly time series, phenology |
| 4 | ESA WorldCover | 10 m | AWS COG | Land cover prior (built, tree, crop, grass, water) |
| 5 | Sentinel-1 SAR | 10 m | openEO | VV/VH backscatter (cloud-proof ground classification) |
| 6 | Hansen GFC | 30 m | HTTP range | Forest cover 2000, annual loss/gain 2001–2024 |
| 7 | Austrian Cadastre | mm-level | REST API | Building footprints, parcel boundaries (ground truth) |
| 8 | OpenStreetMap | varies | Overpass API | Roads, paths, waterways, landcover (ground truth) |
1. BEV Airborne Laser Scanning (DTM + DSM) #
BEV (Bundesamt für Eich- und Vermessungswesen) publishes Austria-wide Digital Terrain Models (DTM) and Digital Surface Models (DSM) derived from airborne LiDAR scanning (ALS) at 1 m resolution in EPSG:3035.
Three acquisition dates are available, enabling temporal change detection:
| Dataset | Date | Base URL |
|---|---|---|
| DTM / DSM | 2022-09-15 | data.bev.gv.at/download/ALS/DTM/20220915/ |
| DTM / DSM | 2023-09-15 | data.bev.gv.at/download/ALS/DTM/20230915/ |
| DTM / DSM | 2024-09-15 | data.bev.gv.at/download/ALS/DTM/20240915/ |
Tiles are Cloud-Optimised GeoTIFFs (COG) organised in a 55-tile grid.
Only the pixels needed for the analysis area are read via GDAL's /vsicurl/
virtual filesystem, avoiding downloading full 12 GB tiles.
The normalised Digital Surface Model (nDSM = DSM − DTM) gives object heights
above ground (tree canopy, building height).
Derived products:
- nDSM — object height above ground (canopy height, building height)
- Slope, aspect, TRI, TPI, curvature — terrain derivatives from DTM
- Temporal change — DTM/nDSM differences between dates detect excavation, fill, tree loss, and new construction
2. BEV DOP Orthophoto (RGBI) #
BEV publishes 4-band (Red, Green, Blue, Near-Infrared) orthophotos at 0.2 m resolution. Austria is divided into 47 Operates (regional survey units), each flown in different years. A fallback DOP (Digitale Orthophoto) mosaic covers areas where individual Operates aren't available.
| Release | Base URL |
|---|---|
| 2022-01-28 | data.bev.gv.at/download/DOP/20220128/ |
| 2022-10-27 | data.bev.gv.at/download/DOP/20221027/ |
| 2024-06-25 | data.bev.gv.at/download/DOP/20240625/ |
| 2025-04-15 | data.bev.gv.at/download/DOP/20250415/ |
Derived products:
- NDVI —
(NIR − Red) / (NIR + Red)at 1 m (resampled from 0.2 m) — best spatial resolution available - Fused NDVI — BEV 1 m spatial detail + Sentinel-2 seasonal correction
- Spectral bands — red, green, blue, NIR mean per segment, brightness, green ratio, NIR/red ratio
- GLCM texture — contrast, homogeneity, entropy, dissimilarity, energy from Grey-Level Co-occurrence Matrix on the greyscale ortho
3. Sentinel-2 NDVI #
Copernicus Data Space Ecosystem provides Sentinel-2 L2A imagery via the openEO API at 10 m resolution.
Two products are computed:
- NDVI composite — cloud-masked temporal median over the growing season (April–September), using the SCL (Scene Classification Layer) to exclude clouds, shadows, snow, and saturated pixels
- Monthly NDVI time series — 8 monthly medians (one per month, April–November) fitted with a harmonic function to extract phenology: mean, amplitude, phase, and RMSE
The monthly time series is the primary discriminator between crop (high amplitude, summer peak), pasture (moderate mean, low amplitude), and road (low mean and amplitude).
All Copernicus products are cached in a grid-snapped tile cache (0.1° tiles) so adjacent areas share cached results. Tiles are also persisted to Zenodo as ZIP archives for long-term storage.
4. ESA WorldCover #
ESA WorldCover v200 (2021) is a global 10 m land cover map derived from Sentinel-1 and Sentinel-2 data. It classifies each pixel into one of 11 classes (tree cover, shrubland, grassland, cropland, built-up, bare/sparse vegetation, snow/ice, permanent water, herbaceous wetland, mangroves, moss/lichen).
Data is read directly as Cloud-Optimised GeoTIFFs from AWS:
https://esa-worldcover.s3.eu-central-1.amazonaws.com/v200/2021/map/ESA_WorldCover_10m_2021_v200_N{lat}E{lon}_Map.tif
Austria requires 6 tiles (N45E009, N45E012, N45E015, N48E009, N48E012, N48E015).
Per-segment fractions are computed for each land cover class (e.g. esa_tree_frac,
esa_crop_frac) and used as spatial priors in both the RF model and the
rule-based classifier.
5. Sentinel-1 SAR #
Sentinel-1 C-band SAR backscatter is fetched via openEO at 10 m resolution. Both VV (co-polarisation) and VH (cross-polarisation) channels are used.
SAR provides cloud-proof ground classification:
- Low VH — specular surfaces (roads, parking, bare soil)
- High VH relative to VV — volume scattering (dense vegetation, forests)
- VV/VH ratio — separates built-up surfaces from natural ground
Especially valuable when NDVI harmonic data is unavailable — SAR distinguishes paved surfaces from vegetated ground without relying on optical data.
6. Hansen Global Forest Change #
Hansen GFC-2024-v1.12 (University of Maryland) provides global tree cover data at 30 m resolution. Tiles are WGS84 GeoTIFFs hosted on Google Cloud Storage:
https://storage.googleapis.com/earthenginepartners-hansen/GFC-2024-v1.12/
Layers used:
| Layer | Type | Description |
|---|---|---|
treecover2000 | uint8 (0–100) | Percent canopy cover in year 2000 |
lossyear | uint8 (0–24) | Year of forest loss (1 = 2001, …, 24 = 2024; 0 = no loss) |
gain | uint8 (0/1) | Forest gain between 2000 and 2012 |
datamask | uint8 | 0 = nodata, 1 = land, 2 = water |
Hansen features are used both for classification (current forest fraction, recent loss detection)
and as labelling criteria for tree_loss training samples.
Austria spans two tiles: 50N_000E and 50N_010E.
7. Austrian Cadastre #
The Austrian Cadastre API provides mm-precision vector data for all ~8,440 Katastralgemeinden (cadastral municipalities):
- Building footprints — used as ground truth labels for
roofclass during RF training - Parcel boundaries — land-use codes (Benützungsart) mapped to landscape types via a 50-code lookup table
- Parcel land-use codes — BEV BA codes (e.g. 42 = Gebäude →
roof, 51 = Acker →crop, 56 = Wald →tree)
https://cadastre-process-api.exe.xyz/api/v1
8. OpenStreetMap #
OpenStreetMap data is fetched via the Overpass API and rasterised onto the 1 m LiDAR grid for use as supplementary ground truth labels.
Feature categories:
- Roads & paths —
highway=motorway|trunk|primary|secondary|tertiary|residential|service|track→road/pathlabels, buffered to match road width - Waterways — rivers, streams, canals →
waterlabels - Land cover —
landuse=forest|farmland|meadow|orchard|vineyard|quarry+natural=water|wood|scrub|rock→ corresponding type labels
Queries use out geom; format (inline coordinates) to avoid expensive node recursion,
with 5 s pauses between sequential queries to respect rate limits.
https://z.overpass-api.de/api/interpreter (primary)
https://overpass-api.de/api/interpreter (fallback)
Segmentation Pipeline #
The segmentation pipeline converts raw raster layers into discrete landscape objects. It runs in three stages: gradient computation, over-segmentation, and region merging.
Step 1 — Fused Multi-Layer Gradient #
Object boundaries are detected as ridges in a weighted sum of Sobel edge gradients computed independently on each available data layer. Each layer is smoothed with a Gaussian filter (σ = 0.8), edge-detected with the Sobel operator, and normalised to [0, 1].
| Layer | Weight | Detects |
|---|---|---|
| CHM (nDSM) | 0.25 | Tree canopy edges, building outlines |
| DTM | 0.20 | Roads, embankments, ditches |
| DSM | 0.10 | Surface breaks (building roofs, canopy edges) |
| NDVI | 0.20 | Vegetation / non-vegetation boundaries |
| NIR | 0.15 | Material transitions (vegetation vs. artificial) |
| Green | 0.05 | Fine spectral detail |
| Red | 0.03 | Fine spectral detail |
| Blue | 0.02 | Fine spectral detail |
The fused gradient captures boundaries that are strong in any layer — a tree edge that's invisible in NDVI is still detected in the CHM, and a road that's hidden in the DSM is visible in NDVI and NIR.
Step 2 — Felzenszwalb Over-Segmentation + RAG Merge #
Segmentation uses a two-layer approach: ground (nDSM < 0.3 m) and elevated (nDSM ≥ 0.3 m) pixels are segmented separately to prevent tree crowns from merging with adjacent ground.
Felzenszwalb graph-based segmentation
Felzenszwalb & Huttenlocher (2004) over-segments the gradient image. Parameters are tuned separately per layer:
| Parameter | Elevated | Ground | Effect |
|---|---|---|---|
scale | 150 | 180 | Higher → fewer, larger segments |
sigma | 0.5 | 0.5 | Pre-smoothing of input |
min_size | 30 px (30 m²) | 60 px (60 m²) | Minimum segment area |
Ground gets coarser parameters (scale × 1.2, min_size × 2) because ground features (fields, roads) are spatially larger than elevated objects (individual trees, roofs).
RAG boundary merge
A Region Adjacency Graph (RAG) is built from the over-segmented result. Adjacent segments with similar mean gradient values are merged hierarchically using a threshold of 0.12. This reduces over-segmentation while preserving strong boundaries (gradient ridge > threshold).
Step 3 — Feature Extraction (57 Features) #
Each segment is described by 57 features spanning seven categories. All features are computed per-segment from the underlying raster data.
| Category | Count | Features |
|---|---|---|
| Height & terrain | 8 | h_mean, h_max, h_std, h_p90, slope_mean, slope_std, dsm_roughness, dtm_roughness |
| Shape | 5 | compactness, elongation, solidity, extent, area |
| DSM edges | 1 | dsm_edge_strength |
| Spectral (BEV ortho) | 11 | ndvi_mean, ndvi_std, brightness_mean, nir_mean, red_mean, green_mean, blue_mean, green_ratio, rg_index, nir_brightness_ratio, nir_red_ratio |
| Copernicus spectral | 3 | cop_ndvi_mean, fused_ndvi_mean, fused_ndvi_std |
| ESA WorldCover | 5 | esa_built_frac, esa_tree_frac, esa_crop_frac, esa_grass_frac, esa_water_frac |
| Temporal | 8 | h_change, dtm_change, dtm_change_abs, temporal_h_std, stability, volume_change_m3, volume_change_abs_m3, dtm_change_max, dtm_change_frac_03m |
| GLCM texture | 6 | glcm_contrast, glcm_homogeneity, glcm_entropy, glcm_dissimilarity, glcm_energy, texture_complexity |
| SAR | 3 | sar_vv, sar_vh, sar_ratio |
| NDVI harmonics | 4 | harm_mean, harm_amplitude, harm_phase, harm_rmse |
| Hansen forest | 6 | hansen_treecover2000, hansen_loss_frac, hansen_recent_loss_frac, hansen_loss_3yr_frac, hansen_gain_frac, hansen_current_forest_frac |
| Additional | 5 | ndvi_max, slope_max, h_p10, perimeter, esa_dominant_lc |
Note: the temporal category has 8 features listed but volume_change_m3 and volume_change_abs_m3 are counted separately, giving 57 total.
Classification #
Each segment is classified into one of 25 object types via a two-tier system: Random Forest as primary classifier with a hand-tuned rule-based decision tree as fallback.
Random Forest Classifier #
The primary classifier is a scikit-learn RandomForestClassifier trained on
cadastre + OSM ground truth (see Training Pipeline).
| Hyperparameter | Value | Rationale |
|---|---|---|
n_estimators | 200 | Enough trees for stable OOB estimate; diminishing returns beyond 200 |
max_depth | 20 | Deep enough to capture complex interactions without severe overfitting |
min_samples_leaf | 5 | Prevents leaves with single samples; smooths predictions |
class_weight | "balanced" | Inversely weights class frequencies — prevents dominant classes from drowning rare ones |
oob_score | True | Out-of-bag estimate used for model selection without held-out set |
n_jobs | 2 | Limited parallelism to cap memory usage |
random_state | 42 | Reproducibility |
Prediction workflow:
- Extract 57-feature vector for the segment
- RF predicts class and per-class probabilities
- If confidence ≥ threshold → accept RF prediction
- If confidence < threshold → fall back to rule-based classifier
- If
mark_uncertain=Trueand confidence < 0.15 → label asunclassified
The RF classifies 16 learnable types — three types (wind_turbine,
substation, solar_panel) are excluded from RF training because they require
spatial context from infrastructure databases rather than pixel-level features.
excavation and fill are merged into earthwork for training, then
split back at inference using DTM change direction.
Rule-Based Fallback Classifier #
When no RF model is loaded, or when RF confidence is below threshold, a hand-tuned decision tree classifies segments. The rules are ordered by discriminative power:
Priority 0 — Infrastructure spatial match
If an infrastructure database (austria-power API) identifies a known solar farm, wind turbine, or electrical substation near the segment, and the physical signature matches (height, area, compactness, NDVI), the segment is classified immediately.
Priority 1 — Temporal disturbance
Recent changes have highest priority — they override steady-state classification:
- DTM change > 0.8 m + non-vegetated →
excavation(lowered) orfill(raised) - nDSM dropped > 2 m + currently low →
tree_loss(terrain intact) orconstruction(terrain reshaped) - nDSM grew > 2 m + not vegetation →
construction - Volume change > 50 m³ + on ground → earthwork
- NDVI > 0.5 vetoes all disturbance labels (surface has regrown)
Priority 2 — Water
ESA WorldCover water fraction > 50% + NDVI < 0.1 + flat → water.
Also: very low NIR (< 30) + negative NDVI + flat → water.
Priority 3 — Elevated objects (nDSM > 0.5 m)
A building score is computed from DSM roughness, height variance, slope, compactness, NDVI, texture, and SAR. Calibrated thresholds from empirical distributions:
- Building score ≥ 5 →
roof(smooth DSM, uniform height, compact, low NDVI) - Further refined:
greenhouse(high NIR transmittance),solar_panel(very smooth + bright) - Building score < 0 → vegetation:
tree(> 4 m),shrub(0.5–4 m),hedge(elongation > 4) - Bridge: elevated road over gap (elongated, smooth, non-vegetation, above void)
Priority 4 — Ground objects (nDSM ≤ 0.5 m)
Uses NDVI, harmonics, SAR, texture, shape, and ESA priors:
- NDVI harmonics (strongest signal when available): high amplitude →
crop, low mean + low amplitude →road, moderate mean →grass - SAR fallback (when no harmonics): low VH + smooth →
road/parking, high VH + green →crop/grass - Shape: elongation > 3 →
road/path, compact + large →parking - Terrain: slope > 30° + rough →
rock, low NDVI + bright →bare_soil - ESA priors: crop fraction > 30% + NDVI evidence →
crop, grass fraction →grass - Context: moderate NDVI + small area + near buildings →
garden
NDVI selection hierarchy
The best available NDVI source is used in priority order: fused NDVI (1 m spatial + seasonal correction) > BEV ortho NDVI (1 m, possibly wrong season) > Copernicus 10 m NDVI (right season, but bleeds into adjacent land covers at boundaries). When relying on coarse 10 m NDVI, the classifier applies additional caution — e.g. moderate NDVI on smooth ground is more likely road with vegetation bleed than actual vegetation.
Hierarchical Grouping #
After classification, adjacent segments with compatible types are merged into 11 higher-level groups. Merge rules define which types can combine:
| Group | Constituent types |
|---|---|
| forest | tree + shrub + hedge |
| woodland | shrub + hedge |
| hedgerow | hedge |
| waterbody | water |
| building | roof + wall + solar_panel + greenhouse + substation |
| road_network | road + path + parking |
| cropland | crop + grass |
| pasture | grass + garden |
| orchard_grove | orchard + vineyard |
| quarry | excavation + fill |
| construction_site | construction + excavation + fill + tree_loss + bare_soil |
Grouping uses spatial adjacency: two segments merge only if they share a boundary
and their types appear together in the merge rules. The resulting group inherits
the dominant type name from the _GROUP_NAME_MAP lookup table.
RF Training Pipeline #
The Random Forest is trained on ground truth from the Austrian cadastre and OpenStreetMap, using a continuous background training process that iterates over hundreds of randomly selected Katastralgemeinden (KGs).
Ground Truth Labelling #
Training labels come from three independent sources, overlaid onto the 1 m segmentation grid:
Source A — Cadastre land-use codes
Each Austrian parcel has a Benützungsart (BA) code recording its official land use. These are mapped to landscape types via a 50-entry lookup table:
| BA Code | Austrian name | Mapped type |
|---|---|---|
| 42–47 | Gebäude (buildings) | roof |
| 41 | Baufläche (paved) | parking |
| 51, 62 | Acker (arable field) | crop |
| 52–55, 58, 61 | Wiese / Weide / Grünland | grass |
| 56 | Wald (forest) | tree |
| 57 | Krummholz / Strauchwald | shrub |
| 63 | Weingarten (vineyard) | vineyard |
| 64 | Hausgarten | garden |
| 65 | Obstgarten (orchard) | orchard |
| 48, 73 | Straße (road) | road |
| 74 | Weg (path) | path |
| 70, 71 | Gewässer (water) | water |
| 80, 81, 93 | Abbaufläche / Deponie | earthwork |
| 83, 84 | Felsen / Geröll | rock |
| 59, 90 | Ödland / sonstige | bare_soil |
Source B — OpenStreetMap
OSM road/path geometries are buffered to physical width and rasterised.
Land-use polygons (landuse=, natural=) provide supplementary labels
for forest, farmland, water, and quarries.
Source C — Hansen tree loss
The tree_loss class has no cadastre or OSM equivalent. Labels are created
synthetically in a post-processing step:
- Segment was originally labelled
tree,shrub,grass,bare_soil, orcrop - Hansen
recent_loss_frac ≥ 0.15andtreecover2000 ≥ 20% - Current height < 5 m or height change < −2 m (evidence of cleared canopy)
To prevent label circularity (the RF learning "hansen_recent_loss_frac > 0.15 → tree_loss"),
the two Hansen features used as labelling criteria (hansen_recent_loss_frac and
hansen_treecover2000) are zeroed out for all tree_loss training samples.
The model must learn from independent signals (height drop, NDVI change, spectral, texture).
Label filtering
excavation+fillare merged →earthwork(split back at inference by DTM change sign)wind_turbine,substation,solar_panelare excluded (require spatial context, not pixel features)- Dominant classes are downsampled to 5× the median class count to reduce imbalance
Training Process #
Training runs as a background systemd service (rf_train.service), iterating over
randomly-selected KGs:
- Select ~300 random KGs with > 5 buildings (seed 42 for reproducibility), plus additional KGs containing known infrastructure (solar, wind, substations)
- For each KG:
- Fetch cadastre building footprints + parcel boundaries with land-use codes
- Fetch OSM roads, waterways, and land cover polygons
- Read all raster data (LiDAR DTM/DSM, ortho RGBI, Sentinel-2 NDVI, WorldCover, SAR, Hansen)
- Run Felzenszwalb segmentation → extract 57 features per segment
- Match segments to ground truth polygons → assign labels
- Save checkpoint to
rf_training_data/checkpoints/kg_XXXXX.npz
- Every 10 successful KGs, retrain the RF on all accumulated samples and save to
/tmp/learned_classifier/ - The API automatically picks up the new model file
Each KG runs in a separate subprocess (via multiprocessing) with a
20-minute timeout to isolate memory and prevent stuck processes.
Checkpoints are saved before model training, so restarts skip already-processed KGs.
Model Selection #
Rather than simply using the latest checkpoint, the system evaluates the learning curve to find the optimal training data volume:
OOB curve evaluation
The evaluate_checkpoints.py script trains RF models at every 5-KG increment
and traces the Out-of-Bag (OOB) accuracy as a function of training data volume.
Multiple random seeds are tested at each point (5 seeds for ≤ 30 KGs, 3 for ≤ 60, 1 beyond)
to measure sensitivity to data composition.
Composite quality score
Models are ranked by a composite score that rewards balanced per-class accuracy:
composite = 0.40 × OOB + 0.35 × mean_per_class_accuracy + 0.25 × worst_class_accuracy
This penalises models that sacrifice rare classes (rock, earthwork) for overall accuracy. A model with 75% OOB but all classes ≥ 50% beats one with 77% OOB where rare classes score 0%.
Best model preservation
The checkpoint count + seed combination with the highest composite score is saved to
data/best_model/. The API prefers this over the live training model.
The Training Monitor dashboard visualises the learning curve,
per-class OOB scores, convergence detection, and best model history.
Deployed model
The current production model was selected from the learning curve at the point of peak composite score:
| Parameter | Value |
|---|---|
| Training KGs | 55 (seed 2) |
| Training samples | 87,496 |
| Classes | 16 |
| Trees / depth / min leaf | 200 / 20 / 5 |
| OOB accuracy | 69.9% |
| Composite score | 59.0% (0.4×OOB + 0.35×mean_cls + 0.25×worst_cls) |
| Mean per-class OOB | 65.0% |
| Worst class OOB | 33.0% (earthwork) |
Per-class OOB accuracy
| Category | Class | OOB | |
|---|---|---|---|
| Vegetation | tree | 86% | |
| crop | 83% | ||
| garden | 80% | ||
| shrub | 63% | ||
| grass | 59% | ||
| vineyard | 54% | ||
| orchard | 44% | ||
| Water | water | 52% | |
| Buildings | roof | 68% | |
| parking | 77% | ||
| Transport | road | 72% | |
| path | 35% | ||
| Terrain | bare_soil | 78% | |
| rock | 56% | ||
| earthwork | 33% | ||
| Change | tree_loss | 100% |
Greyed-out classes (hedge, fence, wall, mast,
wind_turbine, substation, solar_panel, greenhouse,
bridge, excavation, fill, construction)
are handled by the rule-based classifier, not the RF.
Top feature importances
| Rank | Feature | Importance | Category |
|---|---|---|---|
| 1 | hansen_loss_frac | 3.5% | Hansen forest |
| 2 | hansen_treecover2000 | 3.5% | Hansen forest |
| 3 | dtm_roughness | 3.2% | Terrain |
| 4 | fused_ndvi_mean | 3.2% | Spectral |
| 5 | slope_mean | 3.2% | Terrain |
| 6 | h_mean | 2.7% | Height |
| 7 | dtm_change_abs | 2.6% | Temporal |
| 8 | temporal_h_std | 2.3% | Temporal |
| 9 | h_p90 | 2.3% | Height |
| 10 | h_p10 | 2.2% | Height |
| 11 | nir_mean | 2.2% | Spectral |
| 12 | dtm_change_max | 2.2% | Temporal |
| 13 | harm_rmse | 2.1% | Phenology |
| 14 | stability | 2.1% | Temporal |
| 15 | ndvi_mean | 2.0% | Spectral |
Feature importances are broadly distributed (no single dominant feature), indicating the model
uses a diverse mix of terrain, spectral, temporal, and forest change signals. Hansen features
rank high because they are the primary discriminator for tree_loss vs tree (which are otherwise
spectrally similar). tree_loss achieves 100% OOB because the labelling criteria
(Hansen evidence) are highly predictive — but the circular features are zeroed at training time,
so the model learns from height-drop and NDVI-change signals instead.
OOB learning curve
The chart below shows how model quality evolves as more KGs are added to training. The composite score peaks at 55 KGs, then gradually declines — additional data introduces label noise from cadastre/OSM misalignment faster than it adds genuinely new examples.
Austria-Wide Processing #
Beyond interactive per-area analysis, the system includes a background processor that applies the full pipeline to every one of Austria's ~8,440 Katastralgemeinden (KGs), uploading the results to Zenodo as open-access persistent datasets.
Architecture #
The processor (austria_processor.py, ~5,100 lines) runs as a systemd service
(austria_processor.service). Each KG is processed in an isolated subprocess
via multiprocessing.Pool(1) so that memory is fully reclaimed between KGs.
Large KGs are divided into overlapping 1.5 km tiles (100 m overlap) with centroid-ownership
deduplication at boundaries, capping peak memory at ~500 MB regardless of KG size.
| Component | Role |
|---|---|
| Parent process | KG iteration, retry logic, Zenodo upload, progress tracking |
| Child process (per KG) | All data I/O, segmentation, GPKG/JSON building |
| Tile checkpoints | Completed tiles are pickled to disk; on crash/restart only the interrupted tile is re-processed |
| Tile cache | Copernicus (0.1°) and Hansen (0.5°) tiles are grid-snapped so adjacent KGs share cached data |
| Zenodo cache | Copernicus/Hansen tiles are also persisted to Zenodo as ZIP archives and restored via HTTP range reads on local cache miss |
Per-KG Outputs #
Three files are produced for each KG and uploaded to Zenodo:
| File | Contents | Typical size |
|---|---|---|
{kg}_full.gpkg |
All raster layers (DTM, DSM, nDSM, ortho RGBI, segment type) + vector segment polygons in a single GeoPackage | ~400 MB |
{kg}_light.gpkg |
Segment raster + vector, all parcels with DTM heights, all buildings with object heights, new buildings, infrastructure | ~100 MB |
{kg}.json |
JSON summary: area statistics, height distributions, landscape characterisation, top objects/trees, terrain, NDVI, Hansen loss, new buildings, infrastructure, data quality score | ~1 MB |
Zenodo Integration #
Each KG gets its own Zenodo deposit with the three output files. Deposits are published with DOIs for long-term citability. The search index stores download URLs for each KG:
zenodo_json_url— JSON summaryzenodo_light_gpkg_url— segments + enriched parcels/buildingszenodo_full_gpkg_url— all raster layers + vectors
The /api/v1/query and /api/v1/kg/<code>
endpoints include these URLs when available, so any consumer can download the full
analysis for a specific municipality directly from Zenodo.
Monitoring #
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/processing/status | Current state: KG being processed, step, tile progress, rates, ETA |
| POST | /api/v1/processing/start | Start processor (optional: state=, kg=) |
| POST | /api/v1/processing/pause | Pause (SIGSTOP) |
| POST | /api/v1/processing/resume | Resume (SIGCONT) |
| POST | /api/v1/processing/stop | Stop (SIGTERM) |
| POST | /api/v1/processing/single?kg=X | Process a single KG |
| GET | /api/v1/processing/log | Recent processor log lines |
| GET | /api/v1/processing/manifest | Zenodo manifest: upload status per KG |
Fault Tolerance #
The processor is designed for unattended multi-week runs:
- Tile checkpoints — each completed tile is pickled to disk; crashes only lose the in-progress tile
- Automatic retry — on timeout (90 min), the parent retries once with checkpoints restored; transient failures get deferred retry 5 KGs later (up to 2×)
- OOM recovery — systemd's
MemoryMax=4Gkills runaway processes; the service auto-restarts and resumes - Disk management — LRU cache eviction keeps disk under 5 GB; processing pauses automatically below 3 GB free
- Copernicus circuit breaker — when openEO returns 503s, Copernicus is skipped for a cooldown period instead of burning timeouts
- Permanent failure tracking — KGs that fail repeatedly are recorded in
failed_kgs.jsonand skipped on restart
Object Types (25) #
The classifier identifies 25 distinct landscape object types. Each segment in the result is assigned a
type (name) and type_code (integer). Types are detected based on combinations
of height, spectral, textural, and shape features.
| Code | Type | Detection Criteria |
|---|---|---|
1 | tree | nDSM > 4m, rough DSM surface, high NDVI |
2 | shrub | nDSM 0.5–4m, high NDVI |
3 | grass | Ground level, moderate+ NDVI, smooth DTM |
4 | hedge | Elongated shrub shape (length/width > 4) |
5 | water | ESA water class, very low NDVI + NIR, flat surface |
10 | roof | Compact elevated area, smooth DSM, low NDVI |
11 | greenhouse | Roof-like shape, high NIR transmittance |
12 | solar_panel | Very smooth, bright, low NDVI on roof surface |
15 | fence | Low height (0.5–2m), thin, elongated shape |
16 | wall | Narrow elevated feature, adjacent to roof |
17 | mast | Tiny footprint (< 10 m²), very tall (> 15m) |
20 | road | Smooth DTM (< 0.04m roughness), elongated, low NDVI |
21 | path | Narrower road (< 3m width) |
22 | parking | Smooth surface, large area, compact shape, low NDVI |
23 | bridge | Elevated road/path segment over a gap |
30 | crop | Flat surface, seasonal NDVI variation, ESA cropland class |
31 | orchard | Regular tree spacing pattern, < 10m height |
32 | vineyard | Low rows (< 3m), detectable row pattern |
33 | garden | Mixed vegetation in proximity to buildings |
40 | bare_soil | Low NDVI, flat to moderate slope |
41 | rock | Steep slope + very rough DTM + low NDVI |
50 | excavation | DTM lowered > 0.20m between dates |
51 | fill | DTM raised > 0.20m between dates |
52 | tree_loss | nDSM dropped > 2m, underlying terrain intact |
53 | construction | New structure or site clearing detected |
Group Types (11) #
Object types are aggregated into 11 group types for higher-level analysis. Each segment receives a
group_id and group_type property. Filtering by group provides a simplified
landscape view.
| Code | Group | Member Object Types |
|---|---|---|
101 | forest | tree, shrub, hedge |
102 | woodland | shrub, hedge (sparse cover) |
103 | hedgerow | hedge |
106 | waterbody | water |
110 | building | roof, wall, solar_panel, greenhouse |
115 | road_network | road, path, parking |
120 | cropland | crop, grass |
121 | pasture | grass, garden |
122 | orchard_grove | orchard, vineyard |
130 | quarry | excavation, fill |
131 | construction_site | construction, excavation, fill |
Data Sources #
The system fuses six independent remote sensing data sources at varying resolutions. Each contributes different spectral, structural, or temporal information to the classifier.
| # | Source | Resolution | Notes |
|---|---|---|---|
| 1 | BEV ALS DTM + DSM | 1 m | Airborne laser scanning — 3 dates: 2022, 2023, 2024 |
| 2 | BEV DOP RGBI Orthophoto | 0.2 m | 4-band (Red, Green, Blue, Near-Infrared) — 47 operates + DOP fallback |
| 3 | Sentinel-2 NDVI | 10 m | Growing-season NDVI composite via openEO |
| 4 | ESA WorldCover | 10 m | Global land cover classification |
| 5 | Sentinel-1 SAR | 10 m | VV + VH polarisation backscatter |
| 6 | Austrian Cadastre | mm-level | Building footprint ground truth |
Random Forest Training #
The landscape classifier is a scikit-learn Random Forest trained on ground truth from Austrian cadastre building footprints and OpenStreetMap features. Training runs continuously as a background service, iterating over 4,000 randomly-selected Katastralgemeinden (KGs — Austrian cadastral municipalities).
How training works
- For each KG, the system fetches cadastre building footprints + OSM road/landcover as ground truth labels
- All raster data is read for that area: LiDAR DTM/DSM, orthophoto RGBI, Sentinel-2 NDVI, ESA WorldCover, Sentinel-1 SAR
- Felzenszwalb segmentation splits the area into objects, and 44 features are extracted per segment
- Segments overlapping ground truth polygons are labelled → these become training samples
- Every 10 KGs, the RF model is retrained on all accumulated samples and saved
44 Features
Each segment is described by features spanning five categories:
| Category | Features |
|---|---|
| Height & shape | height_max, height_mean, height_p90, area, compactness, elongation, solidity, extent, dsm_edge_strength |
| Terrain | slope_mean, roughness |
| Spectral | ndvi_mean, ndvi_fused, nir_mean, brightness_mean |
| Texture | glcm_entropy, glcm_homogeneity, texture_complexity |
| SAR & phenology | sar_vv, sar_vh, harm_amplitude, harm_phase, phenology_class |
Plus temporal features (height_change, dtm_change, temporal_stability) when multi-date data is enabled, and additional derived features — 44 total.
Checkpointing & restarts
Each KG’s training data is checkpointed to rf_training_data/checkpoints/kg_XXXXX.npz.
If the service restarts, already-processed KGs are skipped. The trained model is saved to
/tmp/learned_classifier/ and automatically picked up by the API.
Monitoring
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/training/status | Background training progress: KGs completed, total samples, current KG, OOB score |
| GET | /api/v1/classifier/status | Current model status: trained (bool), n_samples, oob_score, feature importances |
| POST | /api/v1/classifier/train | Manually train on a specific bbox. Params: bbox, n_estimators, max_depth |
Example
# Check background training progress
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/training/status
# Check current model quality
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/classifier/status
# Manually train on a specific area
curl -X POST "https://srtm-lidar-at.exe.xyz:8000/api/v1/classifier/train?bbox=15.4,47.0,15.5,47.1"
rf_train.service — logs at /tmp/rf_train_4000kg.log.
Response Format #
The primary /api/v1/segment endpoint returns a standard GeoJSON FeatureCollection.
Each feature represents one classified landscape segment with a polygon geometry and rich properties.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [...] },
"properties": {
"type": "tree",
"type_code": 1,
"group_id": 101,
"group_type": "forest",
"is_manmade": false,
"confidence": 0.92,
// ... more properties below
}
}
]
}
Per-Feature Properties
| Property | Type | Description |
|---|---|---|
type | string | Object type name (e.g. "tree", "roof") |
type_code | integer | Numeric type code (see Object Types) |
group_id | integer | Group code (see Group Types) |
group_type | string | Group type name (e.g. "forest", "building") |
is_manmade | boolean | Whether the object is man-made |
confidence | float | Classifier confidence score (0–1) |
| Height Metrics | ||
height_max_m | float | Maximum nDSM height in metres |
height_mean_m | float | Mean nDSM height in metres |
height_p90_m | float | 90th percentile nDSM height |
| Geometry Metrics | ||
area_sqm | float | Segment area in square metres |
compactness | float | Shape compactness (Polsby-Popper) |
elongation | float | Length-to-width ratio |
solidity | float | Area / convex hull area ratio |
extent | float | Area / bounding box area ratio |
| Surface Features | ||
dsm_edge_strength | float | DSM edge detection strength |
slope_mean | float | Mean terrain slope (degrees) |
roughness | float | DTM surface roughness |
| Spectral Features | ||
ndvi_mean | float | Mean NDVI from orthophoto |
ndvi_fused | float | Fused NDVI (ortho + Sentinel-2) |
nir_mean | float | Mean Near-Infrared reflectance |
brightness_mean | float | Mean brightness from RGBI |
| Temporal Features | ||
height_change | float | nDSM change between dates (metres) |
dtm_change | float | DTM elevation change (metres) |
temporal_stability | float | Stability score across all dates |
| Texture Features | ||
glcm_entropy | float | Grey-Level Co-occurrence Matrix entropy |
glcm_homogeneity | float | GLCM homogeneity |
texture_complexity | float | Overall texture complexity score |
| Copernicus / SAR Features | ||
sar_vv | float | Sentinel-1 VV backscatter |
sar_vh | float | Sentinel-1 VH backscatter |
harm_amplitude | float | NDVI harmonic amplitude |
harm_phase | float | NDVI harmonic phase |
phenology_class | string | Phenological classification |