Zum Hauptinhalt springen

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

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 einer Segmentierungsqualität von ~85% IoU 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.

Links das Luftbild, rechts die U-Net Segmentierungsmaske: Weiss markiert versiegelte Flächen.

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 #

Kernbaustein ist die Inferenz pro Bildkachel; das Zusammenfügen der Masken zur nahtlosen Karte übernimmt die umgebende Pipeline.

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.
Finale CIR Heatmap: Rot zeigt hohe Versiegelung (Hitzeinsel-Risiko), Grün zeigt gesunden Baumbestand.

Ähnliche Fragestellung in Ihrem Betrieb? Ein erstes Gespräch kostet nichts: rund 30 Minuten.

Kostenloses Erstgespräch