Zum Hauptinhalt springen

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

·5 min
Heatmap von städtischem Baumbestand vs. versiegelten Flächen

Gitlab Projekt   Live Demo

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.

Semantic Segmentation: Aerial image and U-Net mask side by side

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:

  1. Input: User gibt eine Bounding Box an (Schweizer LV95 Koordinaten).
  2. Datenbeschaffung: Download von RGB-Luftbildkacheln (Swisstopo) und Abruf der 3D-LiDAR-Punktwolken.
  3. Verarbeitung:
    • LiDAR Pfad: Filterung der Punktwolken nach class=vegetation und height > 3m.
    • Vision Pfad: Kacheln der Bilder (512x512), Inferenz via U-Net und Zusammenfügen der Masken (Stitching).
  4. Fusion: Überlagerung der Datensätze und Anwendung von Gaussian Smoothing.
  5. 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.

graph TD User[React + MapLibre GL] -->|Bbox zeichnen, Analyse anfordern| API[FastAPI Gateway] API -->|Job einfügen| DB[(PostgreSQL + PostGIS)] Worker[Inference Worker] -->|Jobs abfragen & beanspruchen| DB Worker -->|Abruf| Swisstopo[Swisstopo API - Luftbilder & LiDAR] Worker -->|Ergebnisse speichern| DB DB -->|Vector Tiles MVT| API API -->|Ergebnisse rendern| User

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_Subdivide beim 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.