ruedtim/luftbild_verortung

PoC: historische Luftbilder des Kantons Zürich mit lokalen Modellen verorten (DINOv2 + LightGlue, swisstopo-Daten)

HTML

0

6 commits

updated Oct 13, 2026

See the code

README

Luftbild-Verortung Zürich (PoC)

Historische Senkrecht-Luftbilder des Kantons Zürich werden allein aus dem Bildinhalt verortet: Bildumriss und Zentrum (LV95/WGS84), Konfidenz, Gemeinden und abgebildete Ortsnamen mit Koordinaten. Alle Modelle laufen lokal (Hugging Face; Apple-GPU/MPS oder CPU); Referenzdaten kommen aus offenen swisstopo-Diensten.

Online-Beispiel: Testbilder, Vorhersage und Überblendung «damals/heute» (die verorteten Bilder aus dem Lauf mit 30 StAZH-Luftbildern von 1981)

So funktioniert's

Die Frage

Wo wurde dieses alte Luftbild aufgenommen? Das Programm beantwortet sie, indem es nur das Bild selbst anschaut (ohne Dateiangaben und Archiv-Metadaten) und die passende Stelle im heutigen Luftbild des Kantons Zürich sucht. Am Ende stehen:

  • Lage und Umriss des Bildes auf der Karte
  • eine Einstufung, wie sicher das Ergebnis ist (Konfidenz)
  • die betroffenen Gemeinden
  • die Orte, Quartiere und Gewässer, die im Bild liegen

Zwei Beispiele aus dem Lauf mit 30 StAZH-Luftbildern von 1981 (Ausgabe von locate.py: das Luftbild liegt halbtransparent auf dem heutigen Orthofoto, rot der berechnete Umriss, gelb die Ortsnamen im Bild):

Horgen (Foto 1981_012_1813): Sicherheit «hoch» (1'303 übereinstimmende Punktpaare), Bildmitte LV95 2687238 / 1234239, Gemeinde Horgen, Orte u. a. Horgen, Horgenberg, Moorschwand und Oberhof. Das Bild ist gegenüber Norden um 90° gedreht; auch das findet das Programm selbst heraus.

Luftbild von 1981 über dem heutigen Orthofoto: Horgen am Zürichsee

Flugplatz Dübendorf (Foto 1981_014_2340): Sicherheit «hoch» (1'483 Punktpaare), Bildmitte LV95 2690248 / 1250735, Gemeinden Dietlikon, Dübendorf und Wangen-Brüttisellen, Orte u. a. Dübendorf (Flugplatzareal), Gfenn und Sonnenberg.

Luftbild von 1981 über dem heutigen Orthofoto: Flugplatz Dübendorf

Hintergrund: SWISSIMAGE © swisstopo; Luftbilder 1981 © GIS-ZH, Kanton Zürich.

Die Idee: grob suchen, dann genau prüfen

Eine ortskundige Person würde in zwei Stufen vorgehen. Zuerst ein grober Eindruck («Das sieht nach dem Stadtrand von Winterthur aus»), dann die Kontrolle am Detail («Diese Kreuzung und diese Bahnlinie finde ich auf der heutigen Karte genau so wieder»). Das Programm macht dasselbe automatisch, für den ganzen Kanton. Als Vergleich dient das heutige Luftbild von swisstopo (SWISSIMAGE).

 altes Luftbild  (nur die Bildpunkte)
        │
        ▼
 ① Grobsuche      rund 36'000 Ausschnitte des heutigen Kantons  →  die 50 ähnlichsten
        │
        ▼
 ② Feinprüfung    welcher dieser 50 passt im Detail wirklich?
        │
        ▼
 ③ Ergebnis       Lage · Umriss · Sicherheit · Gemeinden · Ortsnamen

① Grobsuche: «Was sieht ähnlich aus?» Der Kanton wird in rund 36'000 sich überlappende Ausschnitte zerlegt, kleine und grosse, denn niemand weiss, aus welcher Höhe das alte Bild aufgenommen wurde. Ein KI-Modell (DINOv2) rechnet jeden Ausschnitt in eine Zahlenreihe um, eine Art «Fingerabdruck» des Bildinhalts: Ähnliche Landschaften ergeben ähnliche Zahlen. Dasselbe passiert mit dem alten Bild, in allen vier Drehungen, denn auch die Ausrichtung ist unbekannt. Die 50 Ausschnitte mit den ähnlichsten Fingerabdrücken kommen in die engere Wahl. Das geht schnell, ist aber nur ein Eindruck: «könnte passen».

② Feinprüfung: «Passen die Einzelheiten?» Bei jedem Kandidaten sucht ein zweites KI-Modell (SuperPoint und LightGlue) markante Punkte in beiden Bildern, etwa Gebäudeecken, Kreuzungen oder Feldränder, und ordnet sie einander zu: «Diese Kreuzung links oben im alten Bild ist dieselbe wie die hier im heutigen.» Zeigen beide Bilder wirklich dieselbe Gegend, lassen sich fast alle Paare mit einer einzigen Verschiebung, Drehung und Skalierung ineinander überführen. Zufällige, falsche Paare passen dazu nicht und fallen heraus. Je mehr Paare zusammenpassen, desto sicherer ist die Stelle. Damit schwarz-weisse und farbige Bilder vergleichbar sind, werden beide vorher in Graustufen umgewandelt und im Kontrast angeglichen.

③ Ergebnis Aus den zusammenpassenden Paaren folgt, wie das alte Bild auf die Karte gelegt werden muss. Zur Kontrolle holt das Programm das heutige Luftbild noch einmal exakt für das gefundene Gebiet und gleicht erneut ab. Daraus entstehen Umriss und Mittelpunkt; in den Datenbanken von swisstopo werden die Gemeinden und Ortsnamen nachgeschlagen, die in diesem Gebiet liegen.

Wie sicher ist ein Ergebnis?

Das Mass ist die Zahl der zusammenpassenden Punktpaare:

Zusammenpassende PaareEinstufung
40 und mehrhoch
20 bis 39mittel
10 bis 19tief
unter 10nicht verortet: Das Programm meldet lieber nichts, als zu raten

Wie gut funktioniert es?

Jüngster Lauf: 30 Luftbilder der StAZH-Sammlung von 1981 (Swissair-Aufnahmen von 30 verschiedenen Fluglinien, zufällig gewählt). Das Programm kannte weder Ort noch Metadaten:

  • 29 von 30 Bildern wurden verortet, alle mit Konfidenz «hoch» und keines falsch.
  • Die Bildmitte lag im Median 234 m neben dem Archiv-Zentrum, bei 28 von 29 unter 500 m, höchstens 642 m. Die Archivangaben von 1981 sind Sollpositionen entlang der Fluglinien und selbst nur auf einige 100 m genau; die Abweichung enthält also auch deren Ungenauigkeit.
  • Der berechnete Umriss überlappt den Archiv-Umriss im Median zu 73 %.

Früherer Lauf mit Bildern aus swisstopo LUBIS und GIS-ZH (1930–2000): Die 140 verorteten Bilder lagen im Median 75 m neben der Lage laut Archiv, kein einziges mehr als 500 m.

Details siehe Ergebnisse.

Was läuft wo?

Alle KI-Modelle laufen auf dem eigenen Rechner. Das Luftbild wird nirgends hochgeladen; aus dem Internet werden nur Karten- und Ortsdaten von swisstopo abgerufen (siehe Daten: lokal oder live?).

Grenzen

  • Ausgelegt auf Senkrechtaufnahmen (Blick von oben) im Kanton Zürich (plus 500 m Puffer); Schrägaufnahmen und andere Regionen sind nicht vorgesehen.
  • Gefunden wird nur, was genug wiedererkennbare Strukturen zeigt (Siedlungen, Strassen, Waldränder, Gewässer).
  • Prototyp (PoC), kein fertiges Produkt.

Einzelnes Bild verorten

.venv/bin/python src/locate.py pfad/zum/bild.tif

Akzeptiert JPG/PNG/TIFF, ignoriert Metadaten, schneidet den Filmrand automatisch ab. Schreibt bild.json (Koordinaten, Umriss, Orte) und bild.html (Karte mit Overlay) neben das Bild (oder nach --out ordner/). Dauer: 30–60 s pro Bild.

Ganze Pipeline (Testdatensatz mit Auswertung)

uv venv -p 3.12 .venv && uv pip install -p .venv/bin/python -r requirements.txt
cd src
../.venv/bin/python fetch_images.py --n-lubis 200 --n-zh 40   # Bilder holen, Metadaten -> data/gt/
../.venv/bin/python reference.py                              # SWISSIMAGE-Mosaik 5 m/px
../.venv/bin/python retrieval.py                              # DINOv2-Suchindex (~15 min)
../.venv/bin/python localize.py                               # Verortung (~16 s/Bild)
../.venv/bin/python localize.py --retry 50                    # zweiter Durchgang für unsichere Bilder
../.venv/bin/python evaluate.py                               # output/results.csv, results.geojson
../.venv/bin/python make_map.py                               # output/map/index.html (vorschau.html lokal)

Zusatz-Testset mit den Luftbildern 1981 aus der StAZH-Liste (Excel-Datei mit Bildnummern und Koordinaten); Ablauf wie oben, mit --set stazh, Ergebnisse unter output/stazh/:

cd src
../.venv/bin/python fetch_stazh.py pfad/zur/liste.xlsx --n 30    # je Fluglinie ein Bild -> data/images_stazh/
../.venv/bin/python localize.py --set stazh
../.venv/bin/python evaluate.py --set stazh                      # output/stazh/results.csv, results.geojson
../.venv/bin/python make_map.py --set stazh --demo 40 --only-localized --out ../docs   # Online-Demo für GitHub Pages

swissNAMES3D muss einmalig nach data/cache/names/ entpackt werden: https://data.geo.admin.ch/ch.swisstopo.swissnames3d/swissnames3d_2025/swissnames3d_2025_2056.csv.zip

Technische Details

Ablauf im Detail

Vorverarbeitung. Abfragebild und Referenz werden in Graustufen umgewandelt und mit CLAHE (lokaler Kontrastausgleich) angeglichen; das neutralisiert Film-, Farb- und Epochenunterschiede. locate.py skaliert das Abfragebild auf höchstens 2048 px, schneidet einen dunklen Film-/Scanrand automatisch ab (auto_crop) und zusätzlich 5 % pro Seite.

Grobsuche (src/retrieval.py, src/reference.py). Referenz ist ein SWISSIMAGE-Mosaik des Kantons (plus 3 km Rand) mit 5 m/px. Es wird mit quadratischen Fenstern in 7 Grössen (0,8 / 1,2 / 1,8 / 2,7 / 4 / 6 / 9 km; Schrittweite 40 % der Fenstergrösse; nur Fenster im Kanton plus 500 m) abgedeckt: 35'860 Fenster. Jedes Fenster (auf 322 × 322 px skaliert) wird mit DINOv2 zu einem Vektor mit 1536 Zahlen (CLS-Token und GeM-Pooling der Patch-Tokens, normalisiert). Das Abfragebild wird in 4 Drehungen eingebettet; per Kosinus-Ähnlichkeit ergibt sich je Fenster die beste Drehung. Überlappende Fenster ähnlicher Grösse werden unterdrückt (Non-Maximum-Suppression), die besten K = 50 (locate.py: 80) sind die Kandidaten. Der Index entsteht einmalig (~15 min) und liegt in data/cache/index_dinov2.npz.

Feinabgleich (src/localize.py, src/matching.py). Pro Kandidat wird der SWISSIMAGE-Ausschnitt (Fenster × 1,6, damit Kontext dabei ist) mit 1024 px geholt. SuperPoint findet Keypoints, LightGlue ordnet sie zu, alle 4 Drehungen in einem Batch. OpenCV schätzt aus den Zuordnungen mit MAGSAC (Toleranz 8 px) eine Homographie; die Zahl der Inlier ist das Gütemass. Unplausible Lösungen (nicht konvex, Seitenverhältnis > 2, stark ungleiche Diagonalen) werden verworfen. Der Kandidat mit den meisten Inliern gewinnt; ab 80 Inliern wird die Suche früh abgebrochen, unter 10 gilt das Bild als nicht verortet. Findet diese Runde weniger als 20 Inlier, folgt eine feinere Runde über die besten 30 Kandidaten: Referenz mit 1536 px, das Abfragebild im Massstab des Kandidatenfensters (960 px). LightGlue verträgt nur etwa Faktor 1,5 Massstabsunterschied; bei 1024 px zeigt der Referenzausschnitt ein Bild von 2,4 km auf 4,3 km Kontext und passt nicht mehr.

Verfeinern. Zweimal: Referenz exakt um den vorhergesagten Umriss neu holen (1,2 × längste Ausdehnung, 1280 px) und erneut abgleichen. Das Ergebnis wird nur übernommen, wenn mindestens 80 % der bisherigen Inlier erreicht werden.

Ergebnis. Die 3×3-Matrix bildet Bildpixel auf LV95 (EPSG:2056) ab: Bildecken ergeben den Umriss, die Bildmitte das Zentrum (zusätzlich in WGS84). Konfidenz nach Inliern: ≥ 40 hoch, ≥ 20 mittel, darunter tief (unter 10: nicht verortet); die Schwellen wurden am Testlauf kalibriert (src/evaluate.py).

Ortsnamen (src/names.py). swissNAMES3D (Polygone, Punkte, Linien) mit ausgewählten Objektarten (Orte, Ortsteile, Quartiere, Gewässer, Erhebungen, Areale u. a.) im Kanton plus 5 km liegt als räumlicher Index im Speicher. names_in() liefert alle Punkte im Umriss, nach Bedeutung sortiert (Objektart, Einwohnerkategorie), jeden Namen einmal, höchstens 40. Die Gemeinden kommen aus der geo.admin-API (swissBOUNDARIES3D, Stand 2025).

Zweiter Durchgang. localize.py --retry N wiederholt Bilder ohne Ergebnis oder mit weniger als 40 Inliern mit N Kandidaten und übernimmt das neue Ergebnis nur, wenn es mehr Inlier hat.

Begriffe

BegriffBedeutung
Orthofoto, SWISSIMAGEentzerrtes, massstabsgetreues Luftbild von heute (swisstopo); dient als Referenz
LV95Schweizer Landeskoordinaten (EPSG:2056) in Metern: E (Ost) / N (Nord)
EmbeddingZahlenvektor, der den Inhalt eines Bildes zusammenfasst («Fingerabdruck»); ähnliche Bilder ergeben ähnliche Vektoren
Keypointmarkanter, wiedererkennbarer Punkt im Bild (Ecke, Kreuzung)
Homographie3×3-Matrix, die ein Bild auf ein anderes abbildet (Verschiebung, Drehung, Massstab, perspektivische Verzerrung); für Senkrechtaufnahmen über weitgehend ebenem Gelände eine gute Näherung
InlierZuordnung, die zur geschätzten Homographie passt (Gegenteil: Ausreisser); ihre Anzahl ist das Gütemass
MAGSACrobustes Schätzverfahren, das die Homographie auch findet, wenn viele Zuordnungen falsch sind
CLAHElokaler Kontrastausgleich
IoUÜberlappung zweier Flächen (Schnittfläche geteilt durch Vereinigungsfläche)
Ground TruthSoll-Wert: Lage laut Archiv-Metadaten, nur für die Auswertung genutzt

Modelle und Hardware

ModellHugging FaceGrösseAufgabe
DINOv2 (base)facebook/dinov2-baseca. 350 MBGrobsuche (Embeddings)
SuperPoint + LightGlueETH-CVG/lightglue_superpointca. 55 MBFeinabgleich

Beide werden beim ersten Start vom Hugging Face Hub geladen und danach lokal in ~/.cache/huggingface/ abgelegt. src/matching.py kennt zwei weitere Matcher (zju-community/matchanything_eloftr, zju-community/efficientloftr), die die Pipeline nicht verwendet.

Rechenintensiv sind der einmalige Index (35'860 Fenster durch DINOv2) und pro Bild bis zu 50 (bzw. 80) Kandidaten × 4 Drehungen im Matcher. Der Code nutzt die Apple-GPU (PyTorch mps) und fällt sonst auf die CPU zurück (langsamer; nicht getestet). CUDA (NVIDIA) ist nicht eingebaut. Richtwerte auf einem Mac mit Apple-GPU: Index ~15 min (einmalig), Pipeline ~15 s pro Bild (240 Bilder: ~1 h), locate.py 30–60 s pro Bild inklusive Laden der Modelle.

Daten: lokal oder live?

WasWoWie
SWISSIMAGE-Mosaik, 5 m/px (≈ 54 × 67 km)data/cache/mosaic_5m.png, ca. 280 MBeinmal per WMS geholt, dann lokal
DINOv2-Index (35'860 Fenster)data/cache/index_dinov2.npz, ca. 210 MBeinmal aus dem Mosaik berechnet, dann lokal
SWISSIMAGE-Ausschnitte für den Feinabgleichdata/cache/ref/live von wms.geo.admin.ch, danach gecacht
Ortsnamen (swissNAMES3D)data/cache/names/, ca. 115 MBeinmalig von Hand entpackt, lokal
Gemeindengeo.admin-APIlive, bei jedem Bild, nicht gecacht
Modellgewichte~/.cache/huggingface/beim ersten Start, danach lokal

Die Grobsuche läuft rein lokal. Der Feinabgleich holt pro neues Bild alle Kandidaten-Ausschnitte vorab und parallel (auch wenn die Suche danach früh abbricht): ca. 0,4 MB pro Ausschnitt, also rund 20 MB bei 50 und rund 30 MB bei 80 Kandidaten (locate.py). Bekannte Gegenden kommen aus dem Cache; nach den 240 Testbildern umfasst data/cache/ref/ rund 5'700 Ausschnitte (2,2 GB). data/cache/ ist nicht im Repo (.gitignore): Ein frischer Klon baut Mosaik (per WMS) und Index (~15 min) beim ersten Start selbst auf; nur swissNAMES3D muss von Hand entpackt werden (siehe oben).

Testaufbau und Auswertung

Die Verortung wird blind getestet: src/fetch_images.py lädt 240 Senkrechtbilder (200 aus swisstopo LUBIS, 1930er–2000er, je 25 pro Jahrzehnt; 40 von GIS-ZH, 1981–2000), verkleinert sie auf höchstens 2048 px, schneidet 5 % Rand ab und speichert sie als PNG mit Zufallsnamen und ohne Metadaten in data/images/. Lage, Umriss und Jahr laut Archiv liegen getrennt in data/gt/ground_truth.json; der Localizer liest sie nie. src/evaluate.py vergleicht danach Zentrumsfehler (m), Überlappung der Umrisse (IoU) und die Übereinstimmung der Siedlungs- und Quartiernamen (Jaccard) mit der Ground Truth.

Die Archiv-Metadaten sind selbst nicht exakt (bei GIS-ZH nur ein achsparalleles Rechteck um das Bild); die gemessenen Abweichungen sind deshalb als Grössenordnung zu lesen.

Zusätzlich gibt es ein Testset aus der StAZH-Liste der Luftbilder 1981 (1'292 Bilder auf 31 Fluglinien, src/fetch_stazh.py): Aus 30 der 31 Fluglinien wird je ein Bild zufällig gewählt (Seed 42; Bilder, die schon im ersten Testset sind, ausgenommen), vom GIS-ZH-Scan (rund 190 MB je TIFF) auf höchstens 2048 px verkleinert, 5 % Rand abgeschnitten und ohne Metadaten in data/images_stazh/ gespeichert. Lage und Umriss laut Liste liegen in data/gt/stazh_1981.json. Verortung und Auswertung laufen wie oben, mit --set stazh.

Ergebnisse

Jüngster Lauf: 30 StAZH-Bilder von 1981 (output/stazh/results.csv).

  • 29 von 30 verortet, alle mit Konfidenz «hoch» (274 bis 1'879 Inlier, Median 705).
  • Zentrumsfehler gegenüber dem Archiv-Zentrum: Median 234 m, Mittel 232 m, 90 % unter 384 m, maximal 642 m; 55 % unter 250 m, 28 von 29 unter 500 m, alle unter 1 km. Die Abweichung ist vor allem ost-westlich (quer zu den Fluglinien); die Archiv-Ostkoordinate schwankt innerhalb einer Fluglinie typischerweise nur um etwa 20 m (Standardabweichung): Es sind Sollwerte, und ihre Ungenauigkeit steckt in den Zahlen.
  • Überlappung der Umrisse (IoU): Median 0,73.
  • Ortsnamen: Siedlungs- und Quartiernamen von Vorhersage und Archiv-Umriss stimmen im Mittel zu 72 % überein (Jaccard).
  • Alle 29 Bilder sind gegenüber Norden um 90° oder 270° gedreht; die Drehung fand das Programm jeweils selbst.

Früherer Lauf mit Bildern aus swisstopo LUBIS und GIS-ZH, 1930–2000 (output/results.csv; der zweite Durchgang --retry wurde bei Bild 212 von 240 beendet).

  • 140 Bilder verortet, davon 138 mit Konfidenz «hoch» und 2 «mittel».
  • Zentrumsfehler: Median 75 m, Mittel 93 m, 90 % unter 185 m, maximal 400 m; 59 % unter 100 m, 97 % unter 250 m, alle unter 500 m.
  • Überlappung der Umrisse (IoU): Median 0,88.
  • Ortsnamen: Siedlungs- und Quartiernamen von Vorhersage und Archiv-Umriss stimmen im Mittel zu 83 % überein (Jaccard).
QuelleZeitraumverortete BilderMedian-Fehler
swisstopo (LUBIS)1930er661 m
swisstopo (LUBIS)1940er1179 m
swisstopo (LUBIS)1950er10122 m
swisstopo (LUBIS)1960er17159 m
swisstopo (LUBIS)1970er16106 m
swisstopo (LUBIS)1980er1960 m
swisstopo (LUBIS)1990er883 m
swisstopo (LUBIS)2000er1816 m
GIS-ZH1981–20003565 m

Karte

src/make_map.py entzerrt jedes verortete Bild mit seiner Homographie auf ein Nord-oben-Raster (Web Mercator, WebP) und rechnet denselben Ausschnitt aus dem heutigen SWISSIMAGE dazu; src/map_template.html (Leaflet) macht daraus eine eigenständige Seite:

  • Liste mit Filtern nach Jahrzehnt, Konfidenz und Fehler
  • Vorhersage in Rot, Umriss laut Archiv in Grün, Ortsnamen als Punkte
  • Regler zum Überblenden zwischen «damals» und «heute»

--demo N erzeugt eine kleine öffentliche Variante: eine zufällige, nach Quelle und Jahrzehnt geschichtete Auswahl von etwa N Bildern, die Kennzahlen im Kopf gelten weiterhin für alle. Mit --only-localized bleiben Bilder ohne Ergebnis weg. Das ist die Seite in docs/, die GitHub Pages ausliefert: die 29 verorteten Bilder des Laufs mit 30 StAZH-Bildern (~16 MB). Einzige externe Ressource ist die Kartenbibliothek Leaflet von cdnjs.cloudflare.com; Web-Fonts werden nicht geladen.

Dateien

Code:

DateiAufgabe
src/locate.pyEinzelbild verorten (Ende-zu-Ende), schreibt .json und .html
src/fetch_images.pyTestdatensatz holen (LUBIS, GIS-ZH), Metadaten abtrennen
src/fetch_stazh.pyZusatz-Testset aus der StAZH-Liste 1981 holen
src/reference.pySWISSIMAGE per WMS: Kantons-Mosaik und beliebige Ausschnitte (gecacht)
src/retrieval.pyDINOv2-Index und Grobsuche
src/matching.pySuperPoint/LightGlue-Abgleich, Homographie, Plausibilitätsprüfung
src/localize.pyVerortung des Testdatensatzes, schreibt output/predictions.json
src/names.pyOrtsnamen (swissNAMES3D) und Gemeinden
src/evaluate.pyVergleich mit der Ground Truth, results.csv und results.geojson
src/make_map.py, src/map_template.htmlinteraktive Karte, --demo für die Online-Demo
src/common.pyPfade, HTTP-Session, Kantonsgrenze

Daten und Ergebnisse:

PfadInhalt
data/images/Abfragebilder ohne Metadaten (Zufallsnamen); nicht im Repo
data/gt/ground_truth.jsonArchiv-Metadaten (nur für die Auswertung)
data/images_stazh/, data/gt/stazh_1981.jsonZusatz-Testset: 30 Bilder von 1981 aus der StAZH-Liste (src/fetch_stazh.py); Bilder nicht im Repo
data/cache/Mosaik, Index, Referenzausschnitte, Ortsnamen; nicht im Repo
output/predictions.jsonRohvorhersagen inkl. Homographie Bild→LV95
output/predictions_pass1.jsonStand nach dem ersten Durchgang (vor --retry)
output/results.csv, results.geojsonErgebnisse mit Fehlern, Gemeinden, Orten
output/summary.txtKennzahlen des Testlaufs (Ausgabe von evaluate.py)
output/stazh/dasselbe für den Lauf mit den 30 StAZH-Bildern (predictions.json, results.csv, results.geojson, summary.txt)
output/map/Vollkarte; lokal erzeugt, nicht im Repo
docs/Online-Demo für GitHub Pages
beispiele/Screenshots der Beispiele oben (stazh_*.jpg) sowie ältere Beispielbilder und -ergebnisse für locate.py

Quellen und Lizenzen

Luftbilder (LUBIS), SWISSIMAGE, swissNAMES3D, swissBOUNDARIES3D © swisstopo, freie Nutzung mit Quellenangabe (Nutzungsbedingungen). Historische Luftbilder 1981–2000 © Kanton Zürich, GIS-ZH (wms.zh.ch/HistLuftZHWMS): offene Daten mit der opendata.swiss-Nutzungsbedingung «Open use» (auch kommerziell erlaubt, Quellenangabe empfohlen; Stand 12.10.2026, Datensatz).

Modelle: DINOv2 steht unter Apache-2.0. SuperPoint (in ETH-CVG/lightglue_superpoint) ist nur für akademische oder gemeinnützige nichtkommerzielle Forschung lizenziert (Magic Leap; die LightGlue-Model-Card übernimmt das). Das Repo verteilt keine Gewichte, sie werden beim ersten Start von Hugging Face geladen. Für einen produktiven Betrieb ist das zu klären; auf Hugging Face als Apache-2.0 ausgewiesen sind zju-community/matchanything_eloftr, zju-community/efficientloftr (beide in src/matching.py vorgesehen) und ETH-CVG/lightglue_disk. Wie gut sie hier verorten, ist nicht dokumentiert.

ruedtim/luftbild_verortung

PoC: historische Luftbilder des Kantons Zürich mit lokalen Modellen verorten (DINOv2 + LightGlue, swisstopo-Daten)

HTML

0

6 commits

updated Oct 13, 2026

See the code

README

Luftbild-Verortung Zürich (PoC)

Historische Senkrecht-Luftbilder des Kantons Zürich werden allein aus dem Bildinhalt verortet: Bildumriss und Zentrum (LV95/WGS84), Konfidenz, Gemeinden und abgebildete Ortsnamen mit Koordinaten. Alle Modelle laufen lokal (Hugging Face; Apple-GPU/MPS oder CPU); Referenzdaten kommen aus offenen swisstopo-Diensten.

Online-Beispiel: Testbilder, Vorhersage und Überblendung «damals/heute» (die verorteten Bilder aus dem Lauf mit 30 StAZH-Luftbildern von 1981)

So funktioniert's

Die Frage

Wo wurde dieses alte Luftbild aufgenommen? Das Programm beantwortet sie, indem es nur das Bild selbst anschaut (ohne Dateiangaben und Archiv-Metadaten) und die passende Stelle im heutigen Luftbild des Kantons Zürich sucht. Am Ende stehen:

  • Lage und Umriss des Bildes auf der Karte
  • eine Einstufung, wie sicher das Ergebnis ist (Konfidenz)
  • die betroffenen Gemeinden
  • die Orte, Quartiere und Gewässer, die im Bild liegen

Zwei Beispiele aus dem Lauf mit 30 StAZH-Luftbildern von 1981 (Ausgabe von locate.py: das Luftbild liegt halbtransparent auf dem heutigen Orthofoto, rot der berechnete Umriss, gelb die Ortsnamen im Bild):

Horgen (Foto 1981_012_1813): Sicherheit «hoch» (1'303 übereinstimmende Punktpaare), Bildmitte LV95 2687238 / 1234239, Gemeinde Horgen, Orte u. a. Horgen, Horgenberg, Moorschwand und Oberhof. Das Bild ist gegenüber Norden um 90° gedreht; auch das findet das Programm selbst heraus.

Luftbild von 1981 über dem heutigen Orthofoto: Horgen am Zürichsee

Flugplatz Dübendorf (Foto 1981_014_2340): Sicherheit «hoch» (1'483 Punktpaare), Bildmitte LV95 2690248 / 1250735, Gemeinden Dietlikon, Dübendorf und Wangen-Brüttisellen, Orte u. a. Dübendorf (Flugplatzareal), Gfenn und Sonnenberg.

Luftbild von 1981 über dem heutigen Orthofoto: Flugplatz Dübendorf

Hintergrund: SWISSIMAGE © swisstopo; Luftbilder 1981 © GIS-ZH, Kanton Zürich.

Die Idee: grob suchen, dann genau prüfen

Eine ortskundige Person würde in zwei Stufen vorgehen. Zuerst ein grober Eindruck («Das sieht nach dem Stadtrand von Winterthur aus»), dann die Kontrolle am Detail («Diese Kreuzung und diese Bahnlinie finde ich auf der heutigen Karte genau so wieder»). Das Programm macht dasselbe automatisch, für den ganzen Kanton. Als Vergleich dient das heutige Luftbild von swisstopo (SWISSIMAGE).

 altes Luftbild  (nur die Bildpunkte)
        │
        ▼
 ① Grobsuche      rund 36'000 Ausschnitte des heutigen Kantons  →  die 50 ähnlichsten
        │
        ▼
 ② Feinprüfung    welcher dieser 50 passt im Detail wirklich?
        │
        ▼
 ③ Ergebnis       Lage · Umriss · Sicherheit · Gemeinden · Ortsnamen

① Grobsuche: «Was sieht ähnlich aus?» Der Kanton wird in rund 36'000 sich überlappende Ausschnitte zerlegt, kleine und grosse, denn niemand weiss, aus welcher Höhe das alte Bild aufgenommen wurde. Ein KI-Modell (DINOv2) rechnet jeden Ausschnitt in eine Zahlenreihe um, eine Art «Fingerabdruck» des Bildinhalts: Ähnliche Landschaften ergeben ähnliche Zahlen. Dasselbe passiert mit dem alten Bild, in allen vier Drehungen, denn auch die Ausrichtung ist unbekannt. Die 50 Ausschnitte mit den ähnlichsten Fingerabdrücken kommen in die engere Wahl. Das geht schnell, ist aber nur ein Eindruck: «könnte passen».

② Feinprüfung: «Passen die Einzelheiten?» Bei jedem Kandidaten sucht ein zweites KI-Modell (SuperPoint und LightGlue) markante Punkte in beiden Bildern, etwa Gebäudeecken, Kreuzungen oder Feldränder, und ordnet sie einander zu: «Diese Kreuzung links oben im alten Bild ist dieselbe wie die hier im heutigen.» Zeigen beide Bilder wirklich dieselbe Gegend, lassen sich fast alle Paare mit einer einzigen Verschiebung, Drehung und Skalierung ineinander überführen. Zufällige, falsche Paare passen dazu nicht und fallen heraus. Je mehr Paare zusammenpassen, desto sicherer ist die Stelle. Damit schwarz-weisse und farbige Bilder vergleichbar sind, werden beide vorher in Graustufen umgewandelt und im Kontrast angeglichen.

③ Ergebnis Aus den zusammenpassenden Paaren folgt, wie das alte Bild auf die Karte gelegt werden muss. Zur Kontrolle holt das Programm das heutige Luftbild noch einmal exakt für das gefundene Gebiet und gleicht erneut ab. Daraus entstehen Umriss und Mittelpunkt; in den Datenbanken von swisstopo werden die Gemeinden und Ortsnamen nachgeschlagen, die in diesem Gebiet liegen.

Wie sicher ist ein Ergebnis?

Das Mass ist die Zahl der zusammenpassenden Punktpaare:

Zusammenpassende PaareEinstufung
40 und mehrhoch
20 bis 39mittel
10 bis 19tief
unter 10nicht verortet: Das Programm meldet lieber nichts, als zu raten

Wie gut funktioniert es?

Jüngster Lauf: 30 Luftbilder der StAZH-Sammlung von 1981 (Swissair-Aufnahmen von 30 verschiedenen Fluglinien, zufällig gewählt). Das Programm kannte weder Ort noch Metadaten:

  • 29 von 30 Bildern wurden verortet, alle mit Konfidenz «hoch» und keines falsch.
  • Die Bildmitte lag im Median 234 m neben dem Archiv-Zentrum, bei 28 von 29 unter 500 m, höchstens 642 m. Die Archivangaben von 1981 sind Sollpositionen entlang der Fluglinien und selbst nur auf einige 100 m genau; die Abweichung enthält also auch deren Ungenauigkeit.
  • Der berechnete Umriss überlappt den Archiv-Umriss im Median zu 73 %.

Früherer Lauf mit Bildern aus swisstopo LUBIS und GIS-ZH (1930–2000): Die 140 verorteten Bilder lagen im Median 75 m neben der Lage laut Archiv, kein einziges mehr als 500 m.

Details siehe Ergebnisse.

Was läuft wo?

Alle KI-Modelle laufen auf dem eigenen Rechner. Das Luftbild wird nirgends hochgeladen; aus dem Internet werden nur Karten- und Ortsdaten von swisstopo abgerufen (siehe Daten: lokal oder live?).

Grenzen

  • Ausgelegt auf Senkrechtaufnahmen (Blick von oben) im Kanton Zürich (plus 500 m Puffer); Schrägaufnahmen und andere Regionen sind nicht vorgesehen.
  • Gefunden wird nur, was genug wiedererkennbare Strukturen zeigt (Siedlungen, Strassen, Waldränder, Gewässer).
  • Prototyp (PoC), kein fertiges Produkt.

Einzelnes Bild verorten

.venv/bin/python src/locate.py pfad/zum/bild.tif

Akzeptiert JPG/PNG/TIFF, ignoriert Metadaten, schneidet den Filmrand automatisch ab. Schreibt bild.json (Koordinaten, Umriss, Orte) und bild.html (Karte mit Overlay) neben das Bild (oder nach --out ordner/). Dauer: 30–60 s pro Bild.

Ganze Pipeline (Testdatensatz mit Auswertung)

uv venv -p 3.12 .venv && uv pip install -p .venv/bin/python -r requirements.txt
cd src
../.venv/bin/python fetch_images.py --n-lubis 200 --n-zh 40   # Bilder holen, Metadaten -> data/gt/
../.venv/bin/python reference.py                              # SWISSIMAGE-Mosaik 5 m/px
../.venv/bin/python retrieval.py                              # DINOv2-Suchindex (~15 min)
../.venv/bin/python localize.py                               # Verortung (~16 s/Bild)
../.venv/bin/python localize.py --retry 50                    # zweiter Durchgang für unsichere Bilder
../.venv/bin/python evaluate.py                               # output/results.csv, results.geojson
../.venv/bin/python make_map.py                               # output/map/index.html (vorschau.html lokal)

Zusatz-Testset mit den Luftbildern 1981 aus der StAZH-Liste (Excel-Datei mit Bildnummern und Koordinaten); Ablauf wie oben, mit --set stazh, Ergebnisse unter output/stazh/:

cd src
../.venv/bin/python fetch_stazh.py pfad/zur/liste.xlsx --n 30    # je Fluglinie ein Bild -> data/images_stazh/
../.venv/bin/python localize.py --set stazh
../.venv/bin/python evaluate.py --set stazh                      # output/stazh/results.csv, results.geojson
../.venv/bin/python make_map.py --set stazh --demo 40 --only-localized --out ../docs   # Online-Demo für GitHub Pages

swissNAMES3D muss einmalig nach data/cache/names/ entpackt werden: https://data.geo.admin.ch/ch.swisstopo.swissnames3d/swissnames3d_2025/swissnames3d_2025_2056.csv.zip

Technische Details

Ablauf im Detail

Vorverarbeitung. Abfragebild und Referenz werden in Graustufen umgewandelt und mit CLAHE (lokaler Kontrastausgleich) angeglichen; das neutralisiert Film-, Farb- und Epochenunterschiede. locate.py skaliert das Abfragebild auf höchstens 2048 px, schneidet einen dunklen Film-/Scanrand automatisch ab (auto_crop) und zusätzlich 5 % pro Seite.

Grobsuche (src/retrieval.py, src/reference.py). Referenz ist ein SWISSIMAGE-Mosaik des Kantons (plus 3 km Rand) mit 5 m/px. Es wird mit quadratischen Fenstern in 7 Grössen (0,8 / 1,2 / 1,8 / 2,7 / 4 / 6 / 9 km; Schrittweite 40 % der Fenstergrösse; nur Fenster im Kanton plus 500 m) abgedeckt: 35'860 Fenster. Jedes Fenster (auf 322 × 322 px skaliert) wird mit DINOv2 zu einem Vektor mit 1536 Zahlen (CLS-Token und GeM-Pooling der Patch-Tokens, normalisiert). Das Abfragebild wird in 4 Drehungen eingebettet; per Kosinus-Ähnlichkeit ergibt sich je Fenster die beste Drehung. Überlappende Fenster ähnlicher Grösse werden unterdrückt (Non-Maximum-Suppression), die besten K = 50 (locate.py: 80) sind die Kandidaten. Der Index entsteht einmalig (~15 min) und liegt in data/cache/index_dinov2.npz.

Feinabgleich (src/localize.py, src/matching.py). Pro Kandidat wird der SWISSIMAGE-Ausschnitt (Fenster × 1,6, damit Kontext dabei ist) mit 1024 px geholt. SuperPoint findet Keypoints, LightGlue ordnet sie zu, alle 4 Drehungen in einem Batch. OpenCV schätzt aus den Zuordnungen mit MAGSAC (Toleranz 8 px) eine Homographie; die Zahl der Inlier ist das Gütemass. Unplausible Lösungen (nicht konvex, Seitenverhältnis > 2, stark ungleiche Diagonalen) werden verworfen. Der Kandidat mit den meisten Inliern gewinnt; ab 80 Inliern wird die Suche früh abgebrochen, unter 10 gilt das Bild als nicht verortet. Findet diese Runde weniger als 20 Inlier, folgt eine feinere Runde über die besten 30 Kandidaten: Referenz mit 1536 px, das Abfragebild im Massstab des Kandidatenfensters (960 px). LightGlue verträgt nur etwa Faktor 1,5 Massstabsunterschied; bei 1024 px zeigt der Referenzausschnitt ein Bild von 2,4 km auf 4,3 km Kontext und passt nicht mehr.

Verfeinern. Zweimal: Referenz exakt um den vorhergesagten Umriss neu holen (1,2 × längste Ausdehnung, 1280 px) und erneut abgleichen. Das Ergebnis wird nur übernommen, wenn mindestens 80 % der bisherigen Inlier erreicht werden.

Ergebnis. Die 3×3-Matrix bildet Bildpixel auf LV95 (EPSG:2056) ab: Bildecken ergeben den Umriss, die Bildmitte das Zentrum (zusätzlich in WGS84). Konfidenz nach Inliern: ≥ 40 hoch, ≥ 20 mittel, darunter tief (unter 10: nicht verortet); die Schwellen wurden am Testlauf kalibriert (src/evaluate.py).

Ortsnamen (src/names.py). swissNAMES3D (Polygone, Punkte, Linien) mit ausgewählten Objektarten (Orte, Ortsteile, Quartiere, Gewässer, Erhebungen, Areale u. a.) im Kanton plus 5 km liegt als räumlicher Index im Speicher. names_in() liefert alle Punkte im Umriss, nach Bedeutung sortiert (Objektart, Einwohnerkategorie), jeden Namen einmal, höchstens 40. Die Gemeinden kommen aus der geo.admin-API (swissBOUNDARIES3D, Stand 2025).

Zweiter Durchgang. localize.py --retry N wiederholt Bilder ohne Ergebnis oder mit weniger als 40 Inliern mit N Kandidaten und übernimmt das neue Ergebnis nur, wenn es mehr Inlier hat.

Begriffe

BegriffBedeutung
Orthofoto, SWISSIMAGEentzerrtes, massstabsgetreues Luftbild von heute (swisstopo); dient als Referenz
LV95Schweizer Landeskoordinaten (EPSG:2056) in Metern: E (Ost) / N (Nord)
EmbeddingZahlenvektor, der den Inhalt eines Bildes zusammenfasst («Fingerabdruck»); ähnliche Bilder ergeben ähnliche Vektoren
Keypointmarkanter, wiedererkennbarer Punkt im Bild (Ecke, Kreuzung)
Homographie3×3-Matrix, die ein Bild auf ein anderes abbildet (Verschiebung, Drehung, Massstab, perspektivische Verzerrung); für Senkrechtaufnahmen über weitgehend ebenem Gelände eine gute Näherung
InlierZuordnung, die zur geschätzten Homographie passt (Gegenteil: Ausreisser); ihre Anzahl ist das Gütemass
MAGSACrobustes Schätzverfahren, das die Homographie auch findet, wenn viele Zuordnungen falsch sind
CLAHElokaler Kontrastausgleich
IoUÜberlappung zweier Flächen (Schnittfläche geteilt durch Vereinigungsfläche)
Ground TruthSoll-Wert: Lage laut Archiv-Metadaten, nur für die Auswertung genutzt

Modelle und Hardware

ModellHugging FaceGrösseAufgabe
DINOv2 (base)facebook/dinov2-baseca. 350 MBGrobsuche (Embeddings)
SuperPoint + LightGlueETH-CVG/lightglue_superpointca. 55 MBFeinabgleich

Beide werden beim ersten Start vom Hugging Face Hub geladen und danach lokal in ~/.cache/huggingface/ abgelegt. src/matching.py kennt zwei weitere Matcher (zju-community/matchanything_eloftr, zju-community/efficientloftr), die die Pipeline nicht verwendet.

Rechenintensiv sind der einmalige Index (35'860 Fenster durch DINOv2) und pro Bild bis zu 50 (bzw. 80) Kandidaten × 4 Drehungen im Matcher. Der Code nutzt die Apple-GPU (PyTorch mps) und fällt sonst auf die CPU zurück (langsamer; nicht getestet). CUDA (NVIDIA) ist nicht eingebaut. Richtwerte auf einem Mac mit Apple-GPU: Index ~15 min (einmalig), Pipeline ~15 s pro Bild (240 Bilder: ~1 h), locate.py 30–60 s pro Bild inklusive Laden der Modelle.

Daten: lokal oder live?

WasWoWie
SWISSIMAGE-Mosaik, 5 m/px (≈ 54 × 67 km)data/cache/mosaic_5m.png, ca. 280 MBeinmal per WMS geholt, dann lokal
DINOv2-Index (35'860 Fenster)data/cache/index_dinov2.npz, ca. 210 MBeinmal aus dem Mosaik berechnet, dann lokal
SWISSIMAGE-Ausschnitte für den Feinabgleichdata/cache/ref/live von wms.geo.admin.ch, danach gecacht
Ortsnamen (swissNAMES3D)data/cache/names/, ca. 115 MBeinmalig von Hand entpackt, lokal
Gemeindengeo.admin-APIlive, bei jedem Bild, nicht gecacht
Modellgewichte~/.cache/huggingface/beim ersten Start, danach lokal

Die Grobsuche läuft rein lokal. Der Feinabgleich holt pro neues Bild alle Kandidaten-Ausschnitte vorab und parallel (auch wenn die Suche danach früh abbricht): ca. 0,4 MB pro Ausschnitt, also rund 20 MB bei 50 und rund 30 MB bei 80 Kandidaten (locate.py). Bekannte Gegenden kommen aus dem Cache; nach den 240 Testbildern umfasst data/cache/ref/ rund 5'700 Ausschnitte (2,2 GB). data/cache/ ist nicht im Repo (.gitignore): Ein frischer Klon baut Mosaik (per WMS) und Index (~15 min) beim ersten Start selbst auf; nur swissNAMES3D muss von Hand entpackt werden (siehe oben).

Testaufbau und Auswertung

Die Verortung wird blind getestet: src/fetch_images.py lädt 240 Senkrechtbilder (200 aus swisstopo LUBIS, 1930er–2000er, je 25 pro Jahrzehnt; 40 von GIS-ZH, 1981–2000), verkleinert sie auf höchstens 2048 px, schneidet 5 % Rand ab und speichert sie als PNG mit Zufallsnamen und ohne Metadaten in data/images/. Lage, Umriss und Jahr laut Archiv liegen getrennt in data/gt/ground_truth.json; der Localizer liest sie nie. src/evaluate.py vergleicht danach Zentrumsfehler (m), Überlappung der Umrisse (IoU) und die Übereinstimmung der Siedlungs- und Quartiernamen (Jaccard) mit der Ground Truth.

Die Archiv-Metadaten sind selbst nicht exakt (bei GIS-ZH nur ein achsparalleles Rechteck um das Bild); die gemessenen Abweichungen sind deshalb als Grössenordnung zu lesen.

Zusätzlich gibt es ein Testset aus der StAZH-Liste der Luftbilder 1981 (1'292 Bilder auf 31 Fluglinien, src/fetch_stazh.py): Aus 30 der 31 Fluglinien wird je ein Bild zufällig gewählt (Seed 42; Bilder, die schon im ersten Testset sind, ausgenommen), vom GIS-ZH-Scan (rund 190 MB je TIFF) auf höchstens 2048 px verkleinert, 5 % Rand abgeschnitten und ohne Metadaten in data/images_stazh/ gespeichert. Lage und Umriss laut Liste liegen in data/gt/stazh_1981.json. Verortung und Auswertung laufen wie oben, mit --set stazh.

Ergebnisse

Jüngster Lauf: 30 StAZH-Bilder von 1981 (output/stazh/results.csv).

  • 29 von 30 verortet, alle mit Konfidenz «hoch» (274 bis 1'879 Inlier, Median 705).
  • Zentrumsfehler gegenüber dem Archiv-Zentrum: Median 234 m, Mittel 232 m, 90 % unter 384 m, maximal 642 m; 55 % unter 250 m, 28 von 29 unter 500 m, alle unter 1 km. Die Abweichung ist vor allem ost-westlich (quer zu den Fluglinien); die Archiv-Ostkoordinate schwankt innerhalb einer Fluglinie typischerweise nur um etwa 20 m (Standardabweichung): Es sind Sollwerte, und ihre Ungenauigkeit steckt in den Zahlen.
  • Überlappung der Umrisse (IoU): Median 0,73.
  • Ortsnamen: Siedlungs- und Quartiernamen von Vorhersage und Archiv-Umriss stimmen im Mittel zu 72 % überein (Jaccard).
  • Alle 29 Bilder sind gegenüber Norden um 90° oder 270° gedreht; die Drehung fand das Programm jeweils selbst.

Früherer Lauf mit Bildern aus swisstopo LUBIS und GIS-ZH, 1930–2000 (output/results.csv; der zweite Durchgang --retry wurde bei Bild 212 von 240 beendet).

  • 140 Bilder verortet, davon 138 mit Konfidenz «hoch» und 2 «mittel».
  • Zentrumsfehler: Median 75 m, Mittel 93 m, 90 % unter 185 m, maximal 400 m; 59 % unter 100 m, 97 % unter 250 m, alle unter 500 m.
  • Überlappung der Umrisse (IoU): Median 0,88.
  • Ortsnamen: Siedlungs- und Quartiernamen von Vorhersage und Archiv-Umriss stimmen im Mittel zu 83 % überein (Jaccard).
QuelleZeitraumverortete BilderMedian-Fehler
swisstopo (LUBIS)1930er661 m
swisstopo (LUBIS)1940er1179 m
swisstopo (LUBIS)1950er10122 m
swisstopo (LUBIS)1960er17159 m
swisstopo (LUBIS)1970er16106 m
swisstopo (LUBIS)1980er1960 m
swisstopo (LUBIS)1990er883 m
swisstopo (LUBIS)2000er1816 m
GIS-ZH1981–20003565 m

Karte

src/make_map.py entzerrt jedes verortete Bild mit seiner Homographie auf ein Nord-oben-Raster (Web Mercator, WebP) und rechnet denselben Ausschnitt aus dem heutigen SWISSIMAGE dazu; src/map_template.html (Leaflet) macht daraus eine eigenständige Seite:

  • Liste mit Filtern nach Jahrzehnt, Konfidenz und Fehler
  • Vorhersage in Rot, Umriss laut Archiv in Grün, Ortsnamen als Punkte
  • Regler zum Überblenden zwischen «damals» und «heute»

--demo N erzeugt eine kleine öffentliche Variante: eine zufällige, nach Quelle und Jahrzehnt geschichtete Auswahl von etwa N Bildern, die Kennzahlen im Kopf gelten weiterhin für alle. Mit --only-localized bleiben Bilder ohne Ergebnis weg. Das ist die Seite in docs/, die GitHub Pages ausliefert: die 29 verorteten Bilder des Laufs mit 30 StAZH-Bildern (~16 MB). Einzige externe Ressource ist die Kartenbibliothek Leaflet von cdnjs.cloudflare.com; Web-Fonts werden nicht geladen.

Dateien

Code:

DateiAufgabe
src/locate.pyEinzelbild verorten (Ende-zu-Ende), schreibt .json und .html
src/fetch_images.pyTestdatensatz holen (LUBIS, GIS-ZH), Metadaten abtrennen
src/fetch_stazh.pyZusatz-Testset aus der StAZH-Liste 1981 holen
src/reference.pySWISSIMAGE per WMS: Kantons-Mosaik und beliebige Ausschnitte (gecacht)
src/retrieval.pyDINOv2-Index und Grobsuche
src/matching.pySuperPoint/LightGlue-Abgleich, Homographie, Plausibilitätsprüfung
src/localize.pyVerortung des Testdatensatzes, schreibt output/predictions.json
src/names.pyOrtsnamen (swissNAMES3D) und Gemeinden
src/evaluate.pyVergleich mit der Ground Truth, results.csv und results.geojson
src/make_map.py, src/map_template.htmlinteraktive Karte, --demo für die Online-Demo
src/common.pyPfade, HTTP-Session, Kantonsgrenze

Daten und Ergebnisse:

PfadInhalt
data/images/Abfragebilder ohne Metadaten (Zufallsnamen); nicht im Repo
data/gt/ground_truth.jsonArchiv-Metadaten (nur für die Auswertung)
data/images_stazh/, data/gt/stazh_1981.jsonZusatz-Testset: 30 Bilder von 1981 aus der StAZH-Liste (src/fetch_stazh.py); Bilder nicht im Repo
data/cache/Mosaik, Index, Referenzausschnitte, Ortsnamen; nicht im Repo
output/predictions.jsonRohvorhersagen inkl. Homographie Bild→LV95
output/predictions_pass1.jsonStand nach dem ersten Durchgang (vor --retry)
output/results.csv, results.geojsonErgebnisse mit Fehlern, Gemeinden, Orten
output/summary.txtKennzahlen des Testlaufs (Ausgabe von evaluate.py)
output/stazh/dasselbe für den Lauf mit den 30 StAZH-Bildern (predictions.json, results.csv, results.geojson, summary.txt)
output/map/Vollkarte; lokal erzeugt, nicht im Repo
docs/Online-Demo für GitHub Pages
beispiele/Screenshots der Beispiele oben (stazh_*.jpg) sowie ältere Beispielbilder und -ergebnisse für locate.py

Quellen und Lizenzen

Luftbilder (LUBIS), SWISSIMAGE, swissNAMES3D, swissBOUNDARIES3D © swisstopo, freie Nutzung mit Quellenangabe (Nutzungsbedingungen). Historische Luftbilder 1981–2000 © Kanton Zürich, GIS-ZH (wms.zh.ch/HistLuftZHWMS): offene Daten mit der opendata.swiss-Nutzungsbedingung «Open use» (auch kommerziell erlaubt, Quellenangabe empfohlen; Stand 12.10.2026, Datensatz).

Modelle: DINOv2 steht unter Apache-2.0. SuperPoint (in ETH-CVG/lightglue_superpoint) ist nur für akademische oder gemeinnützige nichtkommerzielle Forschung lizenziert (Magic Leap; die LightGlue-Model-Card übernimmt das). Das Repo verteilt keine Gewichte, sie werden beim ersten Start von Hugging Face geladen. Für einen produktiven Betrieb ist das zu klären; auf Hugging Face als Apache-2.0 ausgewiesen sind zju-community/matchanything_eloftr, zju-community/efficientloftr (beide in src/matching.py vorgesehen) und ETH-CVG/lightglue_disk. Wie gut sie hier verorten, ist nicht dokumentiert.