KI in der Stadtplanung: Verhältnis von Baumkronen zu versiegelter Fläche

Executive Summary #
Auftraggeber:
Baum Schwyz (Hochschulprojekt)
Der Mehrwert: Entwicklung einer Full-Stack Webplattform, die 3D-LiDAR-Daten mit 2D-Luftbildern fusioniert, um städtische Hitzeinseln mit ~85% Segmentierungsgenauigkeit zu identifizieren. Nutzer können Regionen auf einer interaktiven Karte zeichnen und erhalten automatisierte CIR-Analysen.
Tech Stack: PyTorch (U-Net), LiDAR Point Clouds, PostGIS, FastAPI, React, MapLibre GL, Docker.
Die Herausforderung #
Kontext: Das Kollektiv
Baum Schwyz zielt darauf ab, städtische Hitzeinseln und den Verlust der Artenvielfalt durch die Erhöhung des Baumbestands im Kanton Schwyz zu mildern.
Das Problem: Für effektive Massnahmen muss man genau wissen, wo ein neuer Baum den grössten Nutzen bringt. Der bisherige Ansatz verliess sich auf “Bauchgefühl” oder manuelle Begehungen – beides nicht skalierbar und subjektiv. Hochauflösende Daten zu “versiegelten Flächen” (Beton/Asphalt) waren für die spezifischen Regionen schlicht nicht verfügbar.
Impact: Ohne präzise Daten riskieren wir eine ineffiziente Ressourcenallokation – Bäume werden dort gepflanzt, wo es bereits grün ist, während kritische Hotspots (hohe Versiegelung, kaum Schatten) übersehen werden.
Die Lösung #
Strategie: Ich habe eine multimodale Datenfusions-Pipeline entworfen. Anstatt ein Modell zu trainieren, um Bäume zu finden (was visuell komplex ist), habe ich vorhandene hochpräzise LiDAR-Daten für die Vegetation genutzt. Meine Machine Learning Bemühungen konzentrierten sich auf das fehlende Puzzleteil: die Erkennung versiegelter Oberflächen (Strassen, Dächer, Parkplätze) aus Luftbildern.
Kernfunktion: Die Canopy-to-Impervious Ratio (CIR). Diese Metrik quantifiziert das Verhältnis von Schatten zu Beton pro Rasterzelle und generiert eine visuelle “Heatmap” für Entscheidungsträger.
Entscheidung: Ich experimentierte zunächst mit YOLO (Objekterkennung), wechselte aber schnell den Ansatz. Eine Strasse ist kein diskretes Objekt wie ein “Auto” oder eine “Person” mit einer Bounding Box; sie ist eine kontinuierliche, unregelmässige Fläche. Ich entschied mich für U-Net (Semantische Segmentierung), welches jeden einzelnen Pixel klassifiziert und so die nötige Präzision für Flächenberechnungen bietet.

Die Ergebnisse #
- Genauigkeit: Das angepasste U-Net Modell erreichte eine IoU (Intersection-over-Union) von ~85% auf dem Validierungsset.
- Output: Die Pipeline generiert automatisch:
- GIS Shapefiles (.shp): Kompatibel mit offiziellen Siedlungsdaten für die technische Planung.
- Visuelle Reports: Intuitive Heatmaps, die es nicht-technischen Stakeholdern ermöglichen, “rote Zonen” (Hitzeinseln) sofort zu erkennen.
Technische Architektur #
Aktueller Status: Die Inferenz-Pipeline #
Die Kernlogik läuft derzeit als Python-Skript (cir_report), das spezifische geografische Grenzen verarbeitet:
- Input: User gibt eine Bounding Box an (Schweizer LV95 Koordinaten).
- Datenbeschaffung: Download von RGB-Luftbildkacheln (Swisstopo) und Abruf der 3D-LiDAR-Punktwolken.
- Verarbeitung:
- LiDAR Pfad: Filterung der Punktwolken nach
class=vegetationundheight > 3m. - Vision Pfad: Kacheln der Bilder (512x512), Inferenz via U-Net und Zusammenfügen der Masken (Stitching).
- LiDAR Pfad: Filterung der Punktwolken nach
- Fusion: Überlagerung der Datensätze und Anwendung von Gaussian Smoothing.
- Output: Export von GeoTIFFs, Shapefiles und PNG-Reports.
Webplattform-Architektur #
Das Tool ist als gehostete Webanwendung deployed. Nutzer fordern Analysen über eine interaktive Kartenoberfläche an – keine lokalen Skripte erforderlich.
Kernfunktionen der Plattform:
- Interaktive Kartenoberfläche: Bounding Boxes auf Satelliten-/OSM-Basemaps zeichnen, um Analyseregionen zu definieren (max. 4 km²).
- Vector Tile Serving: Dynamische MVT-Tiles für Baumkronen-/Versiegelungs-Polygone und CIR-Heatmap-Raster via
ST_AsMVT. - Räumliche Optimierung: GIST-Indizes auf Geometrie-Spalten +
ST_Subdividebeim INSERT (teilt Polygone auf <255 Vertices) für schnelle Tile-Abfragen. - Tile-Wiederverwendung: Worker cached verarbeitete LiDAR-Tiles nach Modellversion. Überlappende Bereiche nutzen existierende Baumkronen-Ergebnisse wieder.
- Export-Optionen: Download der Ergebnisse als PNG-Heatmap (mit optionaler Swisstopo-Basemap) oder GeoPackage/Shapefile.
Technische Herausforderungen #
Challenge 1: “Ist das ein See oder ein Parkplatz?” Das Modell hatte anfangs Mühe, zwischen dunklen, glatten Gewässern und frischem Asphalt zu unterscheiden. Auch verdichtete Kieswege sahen Beton sehr ähnlich.
Die Lösung: Ich implementierte ein striktes Labeling-Protokoll (“Definitiv Versiegelt” vs. “Definitiv Nicht Versiegelt”) und führte gezielte “Active Learning”-Runden durch. Ich labelte spezifisch Edge Cases wie Seeufer und Waldwege, um das Modell zu zwingen, die kontextuellen Unterschiede zu lernen.
Challenge 2: Das Auflösungslimit. Ein 2000x2000px Luftbild in ein Standard-CNN zu speisen, zerstört die feinen Details, die nötig sind, um ein schmales Trottoir zu erkennen.
Die Lösung: Implementierung eines Tiling-Mechanismus. Die Pipeline zerlegt das geografische Gebiet in 512x512 Trainings-Kacheln (und 256x256 Inferenz-Kacheln). Dies erlaubt dem Modell, kleine Merkmale zu “sehen”, ohne den GPU-Speicher zu sprengen.
Implementierungsdetails #
Das Herzstück der Lösung ist der Inferenz-Loop, der die Tiling- und Stitching-Logik handhabt, um nahtlose Karten zu gewährleisten.
def process_tile(model, tile: np.ndarray, device: str) -> np.ndarray:
"""
Runs inference on a single image tile using the trained U-Net.
Args:
model: Loaded PyTorch U-Net model.
tile: Numpy array of shape (H, W, C), normalized.
device: 'cuda' or 'cpu'.
Returns:
Binary mask (H, W) where 1 indicates impervious surface.
"""
# 1. Preprocess: (H, W, C) -> (1, C, H, W)
input_tensor = torch.from_numpy(tile).permute(2, 0, 1).unsqueeze(0).float()
input_tensor = input_tensor.to(device)
# 2. Inference
model.eval()
with torch.no_grad():
output = model(input_tensor)
probs = torch.sigmoid(output)
# 3. Postprocess: Thresholding at 0.5 for binary classification
mask = (probs > 0.5).float().squeeze().cpu().numpy()
return mask
Logik: Die U-Net Architektur nutzt ein auf ImageNet vortrainiertes ResNet34 Backbone. Dadurch versteht das Modell grundlegende Merkmale (Kanten, Texturen) sofort, was die Trainingszeit reduziert. Ich habe das Problem als binäre Segmentierungsaufgabe behandelt: Klasse 0 (Durchlässig/Natur) vs. Klasse 1 (Versiegelt/Künstlich).
Infrastruktur & Deployment #
- Containerisierung: Multi-Stage Docker Builds für alle Services (React Frontend via nginx, FastAPI, Inference Worker).
- Job Queue: PostgreSQL-basierte Queue mit
SELECT FOR UPDATE SKIP LOCKED. Inklusive Recovery für veraltete Jobs (30-Min Timeout). - Hosting: Linux VPS mit Docker Compose Orchestrierung. Ressourcenlimits: Worker 3GB, API 1GB, PostgreSQL 2GB.
- CI/CD: GitLab CI Pipeline führt Tests aus und deployed. Modellartefakte werden in der GitLab Package Registry gespeichert.
