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.

🛰️ How it works

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:

  1. Watershed segmentation — Felzenszwalb over-segmentation + Region Adjacency Graph merging splits the landscape into homogeneous objects
  2. 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.

ℹ️ Base URL
All endpoints are relative to 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

bash
# 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]]]}'
response — 202
{
  "task_id": "a1b2c3d4-e5f6-...",
  "status": "running"
}

2. Poll progress

bash
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/segment/progress?task_id=a1b2c3d4-e5f6-...
response — 200
{
  "active": true,
  "step": "Classifying segments",
  "detail": "Random Forest prediction on 342 segments",
  "elapsed": 12.4,
  "done": false
}

3. Retrieve the result

bash
# Once "done": true, fetch the full FeatureCollection
curl https://srtm-lidar-at.exe.xyz:8000/api/v1/segment/result?task_id=a1b2c3d4-e5f6-...
💡 Tip
For small areas, omit ?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:

FormatContent-TypeDescription
GeoJSON Geometryapplication/jsonA bare GeoJSON geometry object (Point, Polygon, MultiPolygon, etc.)
GeoJSON + paramsapplication/json{"geometry": <GeoJSON>, "dataset": "...", ...}
KML stringapplication/jsonRaw KML markup as the request body
Coordinate stringtext/plainlon,lat (single point) or lon,lat;lon,lat;... (polygon ring)
File uploadmultipart/form-dataUpload a file: KML, GeoJSON, Shapefile ZIP, GPX, WKT, or GeoPackage
⚠️ Constraints

File upload example:

bash
curl -X POST https://srtm-lidar-at.exe.xyz:8000/api/v1/segment \
  -F "file=@area.kml" \
  -F "include_ortho=true"

Analysis Endpoints #

POST /api/v1/segment ← PRIMARY

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

ParameterTypeDescription
datasetstringALS date to use. Default: 20240915
min_object_sizenumberMinimum segment area in m². Default: 30
felz_scalenumberFelzenszwalb scale parameter. Default: 150
rag_thresholdnumberRAG merge threshold. Default: 0.12
include_orthobooleanInclude BEV RGBI orthophoto features (NDVI, NIR, brightness, GLCM texture). Default: false
include_temporalbooleanInclude 3-date DTM comparison (2022/2023/2024). Default: false
include_copernicusbooleanInclude Sentinel-2 NDVI + ESA WorldCover + SAR + NDVI harmonics. Default: false
include_cadastrebooleanInclude cadastre building footprint ground truth. Default: false
include_hansenbooleanInclude Hansen Global Forest Change calibration. Default: false
typesstringComma-separated type filter. Example: "roof,tree,road"
groupsstringComma-separated group filter. Example: "building,forest"
asyncbooleanIf true, returns 202 with a task_id for polling. Default: false
task_idstringClient-provided UUID for task tracking (optional).

Example

bash
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.

🌳 Random Forest classifier
Classification uses a continuously-trained RF model with 44 features per segment. The model learns from cadastre building footprints and OSM ground truth across thousands of Austrian municipalities. See Random Forest Training for details on the training pipeline, feature importances, and status endpoints.
POST /api/v1/elevation

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

bash
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]}'
POST /api/v1/terrain

Terrain characterisation: computes slope, aspect, Terrain Ruggedness Index (TRI), Topographic Position Index (TPI), and curvature for the input geometry.

Example

bash
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]]]}'
POST /api/v1/changes

Temporal change detection between two ALS dates. Identifies 20 event types including construction, earthworks, vegetation changes, and road modifications.

Parameters

ParameterTypeDescription
date_astringEarlier ALS date. Default: 20220915
date_bstringLater ALS date. Default: 20240915
min_changenumberMinimum 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

bash
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]]]}'
POST /api/v1/changes/trees

Per-tree growth and felling analysis between two ALS dates. Returns individual tree-level change features.

Example

bash
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]]]}'
POST /api/v1/changes/summary

Multi-epoch change summary across all three LiDAR dates (2022 → 2023 → 2024). Provides an overview of landscape evolution over the full temporal range.

Example

bash
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.

GET /api/v1/segment/progress

Poll the progress of an async task.

ParameterTypeDescription
task_id requiredstringThe task ID returned from the async submit.

Response fields: active, step, detail, elapsed, done, error, auto_share_id

GET /api/v1/segment/result

Retrieve the full result of a completed async task.

ParameterTypeDescription
task_id requiredstringThe task ID of the completed analysis.
POST /api/v1/segment/abort

Cancel a running async task.

ParameterTypeDescription
task_id requiredstringThe 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.

MethodEndpointDescription
POST/api/v1/segment/overlaySegment classification raster — each segment coloured by type
POST/api/v1/dtm/overlayDTM hillshade visualisation
POST/api/v1/lidar/overlaynDSM height map (viridis colour ramp)
POST/api/v1/ortho/overlayRGB orthophoto
POST/api/v1/cir/overlayCIR (Colour Infrared) false-colour composite
POST/api/v1/hansen/overlayHansen Global Forest Change visualisation

Example

bash
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 -
ℹ️ Bounds Header
The response includes X-Bounds: south,west,north,east so you can position the image accurately on a map.

Exports #

POST /api/v1/export/geopackage

Export all layers into a single GeoPackage file. Supports async mode for large areas.

ParameterTypeDescription
layersstringall (default) or comma-separated layer IDs — see table below.
typesstringSegment type filter. Example: "tree,road"
height_minnumberKeep segments with height ≥ this value (metres)
height_maxnumberKeep segments with height ≤ this value (metres)
height_opstringgt, lt, or between. Auto-inferred from min/max if omitted.
color_modestringtype (default) or height — segment colouring scheme.
asyncbooleanIf true, returns 202 with {task_id} for polling. Default: false

Available Layers

Layer IDContentsFormat
dtmDTM + DSM + nDSMRaw 1m float32
segmentsSegment type + height rastersClassified raster
ortho-YYYYOrthophoto RGBI for that yearRGBA
cir-YYYYCIR false-colour (NIR→R, R→G, G→B)RGBA
rasterColoured segment overlayRGBA
hansenHansen forest change overlayRGBA
dtm-YYYYDTM hillshade overlayRGBA
dsm-YYYYnDSM height overlayRGBA

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

bash
# 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
QGIS tip: If QGIS shows a ? for raster layer CRS (does not auto-detect it), manually set the coordinate reference system to EPSG:3035 – ETRS89-extended / LAEA Europe.
POST /api/v1/export/mbtiles

Export a single raster layer as MBTiles for offline use in mapping applications.

ParameterTypeDescription
layerstringRequired. Layer ID, e.g. dtm-2024, ortho-2024, raster, hansen.
min_zoomintegerMinimum zoom level.
max_zoomintegerMaximum zoom level.
asyncbooleanIf true, returns 202 with {task_id}. Default: false

Async download: GET /api/v1/export/mbtiles/download/<task_id>

Raw GeoTIFF Downloads

MethodEndpointDescription
POST/api/v1/lidar/geotiffDownload raw DTM, DSM, or nDSM as GeoTIFF
POST/api/v1/ortho/geotiffDownload orthophoto RGBI as GeoTIFF

Example

bash
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.

How it works
  1. Start: GET /api/v1/onestop?bbox=...&format=gpkg → returns 202 with task_id and poll_url
  2. Poll: Repeat the poll_url every 5–10 seconds until status=done
  3. 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.

GET /api/v1/onestop

Segment a bounding box and download the result in one step. All parameters are in the URL — no POST body needed.

URL Parameters

ParameterTypeDescription
bboxstringRequired. Bounding box: lon_min,lat_min,lon_max,lat_max (WGS 84)
namestringSave name / share ID for the result. 1–80 chars, [A-Za-z0-9_-]. If omitted, defaults to auto-<task_id>.
formatstringOutput format: json (default), gpkg, or kml
min_object_sizeintegerMin segment area in m². Default: 10
include_orthobooleanInclude 20 cm orthophoto features. Default: true
include_temporalbooleanInclude 3-date temporal comparison. Default: false
include_copernicusbooleanSentinel-2 NDVI + SAR + WorldCover. Default: false
include_cadastrebooleanCadastre building footprint ground truth. Default: false
include_hansenbooleanHansen Global Forest Change. Default: false
typesstringObject type filter. Example: tree,roof
height_minnumberKeep only objects with height ≥ this value (metres)
height_maxnumberKeep only objects with height ≤ this value (metres)
height_opstringgt, lt, or between. Auto-inferred from min/max if omitted.
layersstringGPKG layers to include. Default: segments (raster + vector)
segment_geometrystringFeature geometry: point (default for KML) or polygon (default for GPKG). Polygons are vectorised from the segment raster.
segment_geometry_stylestringColour scheme: type (default) colours by object type, height colours by viridis height ramp.
group_bystringKML folder grouping: type (default) or height_class
task_idstringPoll 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 wantURL parameters
Trees > 30 mtypes=tree&height_min=30
Objects < 5 mheight_max=5
Between 10–25 mheight_min=10&height_max=25
Roofs 5–15 mtypes=roof&height_min=5&height_max=15

Timing Estimates (< 1 km²)

ConfigurationEst. timeNotes
Ortho only (default)~30–60 sDTM + DSM + orthophoto
Ortho + temporal~60–90 s+ 3-date comparison
All sources~90–120 s+ Copernicus + cadastre + Hansen
GPKG / KML export+ 5–10 sAdded on top of analysis
Auto-save+ 1–2 sAlways 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

Example 1 — Trees > 20 m as GeoPackage
# 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'
Example 2 — All objects as KML, grouped by height
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'
Example 3 — Buildings 5–15 m, with ortho + cadastre
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'
Example 4 — Named save (custom share ID)
# 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'
Example 5 — Polygon KML coloured by height
# 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'
Example 6 — Full automation (shell script)
#!/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.

GET /api/v1/shares

List saved shares.

ParameterTypeDescription
limitintegerMaximum number of shares to return. Default: 20
MethodEndpointDescription
POST/api/v1/shareSave 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>/renameRename a share
GET/api/v1/share/<id>/download.gpkgDownload share as GeoPackage file

Example

bash
# 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 #

MethodEndpointDescription
GET/api/v1/layers?bbox=...List available data layers for a bounding box
GET/api/v1/infoAPI version, status, and capabilities
POST/api/v1/parse-geometryParse and validate geometry input (useful for debugging)
POST/api/v1/classifier/trainTrigger classifier retraining
GET/api/v1/classifier/statusCurrent classifier status and accuracy metrics
GET/api/v1/training/statusTraining data statistics and coverage

Examples

bash
# 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.

GET /api/v1/query Unified query endpoint

Supports multiple query modes via query parameters. Pick one mode per request.

ParameterTypeDescription
qstringFull-text search (KG/gemeinde/district/state names)
kgstringExact KG code lookup
parcelstringParcel lookup (format: KGCODE-GNR)
bboxstringSpatial R-tree query: w,s,e,n
pointstringPoint proximity: lon,lat (sorted by distance)
state / district / gemeindestringAdmin hierarchy filter (code or name)
typestringRank KGs by object type. Combine with metric=
hansenbooleanHansen forest loss query. Combine with year_from, year_to
new_buildingsbooleanKGs with new uncadastred buildings
segmentsbooleanSegment-level power queries. See Segment Power Queries below.
divergencebooleanKGs ranked by RF→final type divergence
parcels_by_typestringPer-parcel filter by type + RF confidence (slow, async-capable)
top_featuresstringCross-KG features: trees|objects|new_buildings|infrastructure (slow, async-capable)
Modifiers (combine with any mode above)
limitintMax results per page. Default: 100, max: 1000
offsetintPagination offset. Default: 0
asyncbooleanRun slow queries asynchronously (parcels_by_type, top_features). Returns task_id to poll.
task_idstringPoll an async query task. Returns 202 while running, result when done.
aggregatebooleanReturn aggregate stats instead of KG list
processed_onlybooleanOnly return processed KGs
min_confidencefloatMin RF confidence threshold
min_area_sqmfloatMin 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=0

Async 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
EndpointPurpose
GET /api/v1/query?task_id=XPoll async query — returns 202 while running, result (200) when done, error (500) on failure
GET /api/v1/query/progress?task_id=XDedicated progress endpoint — same behavior

Async results are auto-cleaned after 4 hours.

Compound Filter #

GETPOST /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 keyTypeDescription
bbox[w,s,e,n]Spatial bounding box
state / district / gemeindestringAdmin filter
aspect["S","SW",...]Dominant aspect direction
dominant_type / phenology / quality_gradestringExact match filters
min_/max_ + numeric fieldfloatNumeric ranges: slope, elevation, ndvi, tree_count, building_height, sar_vv, etc.
type_filtersarray[{"type": "tree", "min_confidence": 0.8, "min_area_sqm": 800}]
landcover_filtersarray[{"type": "crop", "min_fraction": 0.2, "max_height_mean": 1.0}]
sort / sort_dirstringSort column + direction (asc/desc)
limit / offsetintPagination. Default limit=50, max 1000
asyncbooleanRun asynchronously. Returns {task_id}
GET /api/v1/index/status Index statistics

Returns kg_count, processed_count, total_area_km2, zenodo_kgs, and more.

POST /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.

GET /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
POST /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.

PUT /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"]
}
DELETE /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=Vorarlberg

means: 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,hedge

means: (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.

ParameterTypeDescription
object_typestringSingle type or comma-separated OR list: tree, tree,shrub,hedge
min_rf_confidencefloatRaw RF model score lower bound (0–1)
max_rf_confidencefloatRaw RF model score upper bound
min_confidencefloatCombined/calibrated score lower bound
max_confidencefloatCombined/calibrated score upper bound
min_area_sqmfloatMinimum segment area (m²)
max_area_sqmfloatMaximum segment area
min_heightfloatMinimum height_max_m (metres)
max_heightfloatMaximum height_max_m
min_volumefloatMinimum |volume_change_m³| (absolute)
max_volumefloatMaximum |volume_change_m³|
bboxstringSpatial filter: w,s,e,n
statestringState name or code
districtstringDistrict name or code
sortstringheight_max_m (default), height_mean_m, area_sqm, volume, rf_confidence, confidence
sort_dirstringdesc (default) or asc
percentilefloat0–1: return only top N% (e.g. 0.01 = top 1%). Threshold computed from all matching segments, then applied as filter.
limit, offsetintPagination (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.”

ParameterTypeDescription
Admin / spatial
kgstringFilter by KG code
statestringBundesland name or code
districtstringBezirk name or code
gemeindestringGemeinde name or code
bboxstringSpatial bounding box: w,s,e,n
Area & elevation
min_area / max_areafloatParcel area in m²
min_elevation / max_elevationfloatMean elevation in metres
Terrain
min_slope / max_slopefloatMean slope in degrees
min_tri / max_trifloatTerrain Roughness Index
terrain_classstringlevel, nearly_level, slightly_rugged, rugged, …
aspectstringComma-separated: N,NE,E,SE,S,SW,W,NW
Vegetation & land cover
dominant_typestringDominant segment type: tree, grass, roof, …
min_vegetated_fraction / max_vegetated_fractionfloatVegetated fraction 0–1
min_forested_fraction / max_forested_fractionfloatForested fraction 0–1
is_vegetatedbooltrue or false
Height & classification
min_ndsm_max / max_ndsm_maxfloatMax nDSM height on parcel (m)
min_confidencefloatMin mean classification confidence
min_rf_confidencefloatMin RF model confidence
Hansen forest change
min_hansen_recent_5yr / max_hansen_recent_5yrintRecent 5-year loss pixels
min_hansen_total / max_hansen_totalintTotal loss pixels
Building join filters
building_roof_typestringpitched or flat — parcel must contain a matching building
building_min_storiesintMinimum estimated building stories
building_max_storiesintMaximum estimated building stories
Sorting & pagination
sortstringelevation_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_dirstringdesc (default) or asc
limitintMax results (default 100, max 1000)
offsetintPagination 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"
Speed guide: Fast (<50ms): /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.

Tip. Any DOM ancestor with 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.

EndpointPurpose
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:

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:

  1. Data acquisition — windowed reads from 8 independent data sources at 0.2–30 m resolution
  2. Segmentation — fused multi-layer gradient → Felzenszwalb over-segmentation → RAG boundary merge
  3. Feature extraction — 57 per-segment features spanning height, shape, spectral, texture, SAR, phenology, and temporal change
  4. Classification — Random Forest (trained on cadastre + OSM ground truth) with rule-based fallback and hierarchical grouping
Processing model: Areas are tiled into overlapping 1.5 km strips (100 m overlap). Each tile is processed independently, then results are merged with centroid-ownership deduplication at boundaries. This caps memory at ~90 MB per tile regardless of total area size.

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.

#SourceResolutionAccessRole in pipeline
1BEV ALS DTM + DSM1 mHTTP rangeHeight model, terrain, temporal change
2BEV DOP Orthophoto0.2 mHTTP rangeNDVI, spectral bands, texture (GLCM)
3Sentinel-2 NDVI10 mopenEOGrowing-season NDVI, monthly time series, phenology
4ESA WorldCover10 mAWS COGLand cover prior (built, tree, crop, grass, water)
5Sentinel-1 SAR10 mopenEOVV/VH backscatter (cloud-proof ground classification)
6Hansen GFC30 mHTTP rangeForest cover 2000, annual loss/gain 2001–2024
7Austrian Cadastremm-levelREST APIBuilding footprints, parcel boundaries (ground truth)
8OpenStreetMapvariesOverpass APIRoads, 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:

DatasetDateBase URL
DTM / DSM2022-09-15data.bev.gv.at/download/ALS/DTM/20220915/
DTM / DSM2023-09-15data.bev.gv.at/download/ALS/DTM/20230915/
DTM / DSM2024-09-15data.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:

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.

ReleaseBase URL
2022-01-28data.bev.gv.at/download/DOP/20220128/
2022-10-27data.bev.gv.at/download/DOP/20221027/
2024-06-25data.bev.gv.at/download/DOP/20240625/
2025-04-15data.bev.gv.at/download/DOP/20250415/

Derived products:

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:

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:

URL pattern
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:

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:

Base URL
https://storage.googleapis.com/earthenginepartners-hansen/GFC-2024-v1.12/

Layers used:

LayerTypeDescription
treecover2000uint8 (0–100)Percent canopy cover in year 2000
lossyearuint8 (0–24)Year of forest loss (1 = 2001, …, 24 = 2024; 0 = no loss)
gainuint8 (0/1)Forest gain between 2000 and 2012
datamaskuint80 = 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):

Base URL
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:

Queries use out geom; format (inline coordinates) to avoid expensive node recursion, with 5 s pauses between sequential queries to respect rate limits.

Endpoints
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].

LayerWeightDetects
CHM (nDSM)0.25Tree canopy edges, building outlines
DTM0.20Roads, embankments, ditches
DSM0.10Surface breaks (building roofs, canopy edges)
NDVI0.20Vegetation / non-vegetation boundaries
NIR0.15Material transitions (vegetation vs. artificial)
Green0.05Fine spectral detail
Red0.03Fine spectral detail
Blue0.02Fine 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:

ParameterElevatedGroundEffect
scale150180Higher → fewer, larger segments
sigma0.50.5Pre-smoothing of input
min_size30 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.

CategoryCountFeatures
Height & terrain8 h_mean, h_max, h_std, h_p90, slope_mean, slope_std, dsm_roughness, dtm_roughness
Shape5 compactness, elongation, solidity, extent, area
DSM edges1 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 spectral3 cop_ndvi_mean, fused_ndvi_mean, fused_ndvi_std
ESA WorldCover5 esa_built_frac, esa_tree_frac, esa_crop_frac, esa_grass_frac, esa_water_frac
Temporal8 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 texture6 glcm_contrast, glcm_homogeneity, glcm_entropy, glcm_dissimilarity, glcm_energy, texture_complexity
SAR3 sar_vv, sar_vh, sar_ratio
NDVI harmonics4 harm_mean, harm_amplitude, harm_phase, harm_rmse
Hansen forest6 hansen_treecover2000, hansen_loss_frac, hansen_recent_loss_frac, hansen_loss_3yr_frac, hansen_gain_frac, hansen_current_forest_frac
Additional5 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).

HyperparameterValueRationale
n_estimators200Enough trees for stable OOB estimate; diminishing returns beyond 200
max_depth20Deep enough to capture complex interactions without severe overfitting
min_samples_leaf5Prevents leaves with single samples; smooths predictions
class_weight"balanced"Inversely weights class frequencies — prevents dominant classes from drowning rare ones
oob_scoreTrueOut-of-bag estimate used for model selection without held-out set
n_jobs2Limited parallelism to cap memory usage
random_state42Reproducibility

Prediction workflow:

  1. Extract 57-feature vector for the segment
  2. RF predicts class and per-class probabilities
  3. If confidence ≥ threshold → accept RF prediction
  4. If confidence < threshold → fall back to rule-based classifier
  5. If mark_uncertain=True and confidence < 0.15 → label as unclassified

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:

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:

Priority 4 — Ground objects (nDSM ≤ 0.5 m)

Uses NDVI, harmonics, SAR, texture, shape, and ESA priors:

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:

GroupConstituent types
foresttree + shrub + hedge
woodlandshrub + hedge
hedgerowhedge
waterbodywater
buildingroof + wall + solar_panel + greenhouse + substation
road_networkroad + path + parking
croplandcrop + grass
pasturegrass + garden
orchard_groveorchard + vineyard
quarryexcavation + fill
construction_siteconstruction + 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 CodeAustrian nameMapped type
42–47Gebäude (buildings)roof
41Baufläche (paved)parking
51, 62Acker (arable field)crop
52–55, 58, 61Wiese / Weide / Grünlandgrass
56Wald (forest)tree
57Krummholz / Strauchwaldshrub
63Weingarten (vineyard)vineyard
64Hausgartengarden
65Obstgarten (orchard)orchard
48, 73Straße (road)road
74Weg (path)path
70, 71Gewässer (water)water
80, 81, 93Abbaufläche / Deponieearthwork
83, 84Felsen / Geröllrock
59, 90Ödland / sonstigebare_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:

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

Training Process #

Training runs as a background systemd service (rf_train.service), iterating over randomly-selected KGs:

  1. Select ~300 random KGs with > 5 buildings (seed 42 for reproducibility), plus additional KGs containing known infrastructure (solar, wind, substations)
  2. 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
  3. Every 10 successful KGs, retrain the RF on all accumulated samples and save to /tmp/learned_classifier/
  4. 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:

formula
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:

ParameterValue
Training KGs55 (seed 2)
Training samples87,496
Classes16
Trees / depth / min leaf200 / 20 / 5
OOB accuracy69.9%
Composite score59.0% (0.4×OOB + 0.35×mean_cls + 0.25×worst_cls)
Mean per-class OOB65.0%
Worst class OOB33.0% (earthwork)

Per-class OOB accuracy

CategoryClassOOB
Vegetationtree86%
crop83%
garden80%
shrub63%
grass59%
vineyard54%
orchard44%
Waterwater52%
Buildingsroof68%
parking77%
Transportroad72%
path35%
Terrainbare_soil78%
rock56%
earthwork33%
Changetree_loss100%

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

RankFeatureImportanceCategory
1hansen_loss_frac3.5%Hansen forest
2hansen_treecover20003.5%Hansen forest
3dtm_roughness3.2%Terrain
4fused_ndvi_mean3.2%Spectral
5slope_mean3.2%Terrain
6h_mean2.7%Height
7dtm_change_abs2.6%Temporal
8temporal_h_std2.3%Temporal
9h_p902.3%Height
10h_p102.2%Height
11nir_mean2.2%Spectral
12dtm_change_max2.2%Temporal
13harm_rmse2.1%Phenology
14stability2.1%Temporal
15ndvi_mean2.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.

📊 Open Training Monitor Dashboard — live OOB curve, per-class accuracy, convergence tracking, best model history

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.

ComponentRole
Parent processKG iteration, retry logic, Zenodo upload, progress tracking
Child process (per KG)All data I/O, segmentation, GPKG/JSON building
Tile checkpointsCompleted tiles are pickled to disk; on crash/restart only the interrupted tile is re-processed
Tile cacheCopernicus (0.1°) and Hansen (0.5°) tiles are grid-snapped so adjacent KGs share cached data
Zenodo cacheCopernicus/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:

FileContentsTypical 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:

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 #

📡 Open Processor Dashboard — live progress, map, tile status, log stream, Zenodo manifest
MethodEndpointDescription
GET/api/v1/processing/statusCurrent state: KG being processed, step, tile progress, rates, ETA
POST/api/v1/processing/startStart processor (optional: state=, kg=)
POST/api/v1/processing/pausePause (SIGSTOP)
POST/api/v1/processing/resumeResume (SIGCONT)
POST/api/v1/processing/stopStop (SIGTERM)
POST/api/v1/processing/single?kg=XProcess a single KG
GET/api/v1/processing/logRecent processor log lines
GET/api/v1/processing/manifestZenodo manifest: upload status per KG

Fault Tolerance #

The processor is designed for unattended multi-week runs:

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.

CodeTypeDetection Criteria
1treenDSM > 4m, rough DSM surface, high NDVI
2shrubnDSM 0.5–4m, high NDVI
3grassGround level, moderate+ NDVI, smooth DTM
4hedgeElongated shrub shape (length/width > 4)
5waterESA water class, very low NDVI + NIR, flat surface
10roofCompact elevated area, smooth DSM, low NDVI
11greenhouseRoof-like shape, high NIR transmittance
12solar_panelVery smooth, bright, low NDVI on roof surface
15fenceLow height (0.5–2m), thin, elongated shape
16wallNarrow elevated feature, adjacent to roof
17mastTiny footprint (< 10 m²), very tall (> 15m)
20roadSmooth DTM (< 0.04m roughness), elongated, low NDVI
21pathNarrower road (< 3m width)
22parkingSmooth surface, large area, compact shape, low NDVI
23bridgeElevated road/path segment over a gap
30cropFlat surface, seasonal NDVI variation, ESA cropland class
31orchardRegular tree spacing pattern, < 10m height
32vineyardLow rows (< 3m), detectable row pattern
33gardenMixed vegetation in proximity to buildings
40bare_soilLow NDVI, flat to moderate slope
41rockSteep slope + very rough DTM + low NDVI
50excavationDTM lowered > 0.20m between dates
51fillDTM raised > 0.20m between dates
52tree_lossnDSM dropped > 2m, underlying terrain intact
53constructionNew 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.

CodeGroupMember Object Types
101foresttree, shrub, hedge
102woodlandshrub, hedge (sparse cover)
103hedgerowhedge
106waterbodywater
110buildingroof, wall, solar_panel, greenhouse
115road_networkroad, path, parking
120croplandcrop, grass
121pasturegrass, garden
122orchard_groveorchard, vineyard
130quarryexcavation, fill
131construction_siteconstruction, 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.

#SourceResolutionNotes
1BEV ALS DTM + DSM1 mAirborne laser scanning — 3 dates: 2022, 2023, 2024
2BEV DOP RGBI Orthophoto0.2 m4-band (Red, Green, Blue, Near-Infrared) — 47 operates + DOP fallback
3Sentinel-2 NDVI10 mGrowing-season NDVI composite via openEO
4ESA WorldCover10 mGlobal land cover classification
5Sentinel-1 SAR10 mVV + VH polarisation backscatter
6Austrian Cadastremm-levelBuilding 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

  1. For each KG, the system fetches cadastre building footprints + OSM road/landcover as ground truth labels
  2. All raster data is read for that area: LiDAR DTM/DSM, orthophoto RGBI, Sentinel-2 NDVI, ESA WorldCover, Sentinel-1 SAR
  3. Felzenszwalb segmentation splits the area into objects, and 44 features are extracted per segment
  4. Segments overlapping ground truth polygons are labelled → these become training samples
  5. 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:

CategoryFeatures
Height & shapeheight_max, height_mean, height_p90, area, compactness, elongation, solidity, extent, dsm_edge_strength
Terrainslope_mean, roughness
Spectralndvi_mean, ndvi_fused, nir_mean, brightness_mean
Textureglcm_entropy, glcm_homogeneity, texture_complexity
SAR & phenologysar_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

📊 Open Training Monitor Dashboard — live OOB curve, convergence tracking, best model history
MethodEndpointDescription
GET/api/v1/training/statusBackground training progress: KGs completed, total samples, current KG, OOB score
GET/api/v1/classifier/statusCurrent model status: trained (bool), n_samples, oob_score, feature importances
POST/api/v1/classifier/trainManually train on a specific bbox. Params: bbox, n_estimators, max_depth

Example

bash
# 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"
ℹ️ Live status
Training progress is also shown in the app’s status bar (bottom-left of the sidebar). The service runs as 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.

json
{
  "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

PropertyTypeDescription
typestringObject type name (e.g. "tree", "roof")
type_codeintegerNumeric type code (see Object Types)
group_idintegerGroup code (see Group Types)
group_typestringGroup type name (e.g. "forest", "building")
is_manmadebooleanWhether the object is man-made
confidencefloatClassifier confidence score (0–1)
Height Metrics
height_max_mfloatMaximum nDSM height in metres
height_mean_mfloatMean nDSM height in metres
height_p90_mfloat90th percentile nDSM height
Geometry Metrics
area_sqmfloatSegment area in square metres
compactnessfloatShape compactness (Polsby-Popper)
elongationfloatLength-to-width ratio
solidityfloatArea / convex hull area ratio
extentfloatArea / bounding box area ratio
Surface Features
dsm_edge_strengthfloatDSM edge detection strength
slope_meanfloatMean terrain slope (degrees)
roughnessfloatDTM surface roughness
Spectral Features
ndvi_meanfloatMean NDVI from orthophoto
ndvi_fusedfloatFused NDVI (ortho + Sentinel-2)
nir_meanfloatMean Near-Infrared reflectance
brightness_meanfloatMean brightness from RGBI
Temporal Features
height_changefloatnDSM change between dates (metres)
dtm_changefloatDTM elevation change (metres)
temporal_stabilityfloatStability score across all dates
Texture Features
glcm_entropyfloatGrey-Level Co-occurrence Matrix entropy
glcm_homogeneityfloatGLCM homogeneity
texture_complexityfloatOverall texture complexity score
Copernicus / SAR Features
sar_vvfloatSentinel-1 VV backscatter
sar_vhfloatSentinel-1 VH backscatter
harm_amplitudefloatNDVI harmonic amplitude
harm_phasefloatNDVI harmonic phase
phenology_classstringPhenological classification