fix(extract): Kartenbeschriftung nicht mehr als Geometrie ausgeben

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-12 17:59:54 +02:00
1 parent 0bf2c29061
commit f3abb5e803
92 files changed
+161 -96

No files matched your search

+40
View File
@@ -41,3 +41,43 @@ def class_shares(labels: np.ndarray) -> dict[str, float]:
total = labels.size
counts = np.bincount(labels.ravel(), minlength=len(PALETTE))
return {name: float(counts[i]) / total for name, i in CLASSES.items()}
NATURAL_CLASSES = ("gruen", "sand", "land")
LABEL_MAX_NATURAL = 0.02 # Anteil natuerlicher Merkmale, ab dem eine Flaeche als echt gilt
# Groesser als jeder gemessene Buchstabe (4.876-14.760 px am realen Mosaik) und weit kleiner
# als die Hauptlandmasse (5.852.263 px): ohne diese Untergrenze waere in einer Szene ohne
# echte Hauptlandmasse die groesste - und einzige - Komponente automatisch "Festland" und
# damit nie loeschbar, auch wenn sie selbst ein Schriftzug ist (siehe
# test_letter_shaped_island_becomes_water, wo genau das die einzige Komponente ist).
MAINLAND_MIN_SIZE = 15_000
def drop_map_labels(labels: np.ndarray) -> np.ndarray:
"""Beschriftungen der Originalkarte auf Wasser zuruecksetzen.
Die Schriftzuege der Karte werden pixelweise als stadt/strasse/gebaeude klassifiziert und
sind farblich von echten Flaechen nicht zu unterscheiden. Trennbar sind sie erst als
Region: ein Schriftzug im Wasser bildet eine eigene, vom Festland getrennte Nicht-Wasser-
Komponente ohne jedes natuerliche Merkmal, waehrend eine echte Insel Gruen oder Sand
traegt. Die groesste Komponente ist das Festland und bleibt unangetastet, sofern sie
ueberhaupt gross genug ist, um ueberhaupt als Festland infrage zu kommen (MAINLAND_MIN_SIZE);
sonst wird auch sie nur ueber den Anteil natuerlicher Merkmale beurteilt.
"""
solid = labels != CLASSES["wasser"]
if not solid.any():
return labels
components, count = ndi.label(solid, structure=np.ones((3, 3)))
sizes = ndi.sum(solid, components, range(1, count + 1))
largest = int(np.argmax(sizes)) + 1
mainland = largest if sizes[largest - 1] >= MAINLAND_MIN_SIZE else 0
natural = np.zeros(labels.shape, dtype=bool)
for name in NATURAL_CLASSES:
natural |= labels == CLASSES[name]
shares = ndi.mean(natural.astype(np.float32), components, range(1, count + 1))
drop = np.zeros(count + 1, dtype=bool)
for index in range(1, count + 1):
drop[index] = index != mainland and shares[index - 1] <= LABEL_MAX_NATURAL
out = labels.copy()
out[drop[components]] = CLASSES["wasser"]
return out
+2 -1
View File
@@ -3,7 +3,7 @@ import json
from pathlib import Path
from leonida.buildings import building_polygons
from leonida.classify import CLASSES, class_shares, classify
from leonida.classify import CLASSES, class_shares, classify, drop_map_labels
from leonida.emit import (chunk_box_of, chunk_payload, in_chunk, split_by_chunk_all,
write_chunks, write_debug_png)
from leonida.heights import assign_heights, palm_points
@@ -20,6 +20,7 @@ def main() -> None:
download_tiles(cache)
mosaic = load_mosaic(cache)
labels, unknown_share = classify(mosaic, report_unknown=True)
labels = drop_map_labels(labels)
if unknown_share > 0.05:
raise SystemExit(f"{unknown_share:.1%} der Pixel unbekannt. Palette kalibrieren, nicht die Grenze anheben.")
shares = class_shares(labels)
+25 -1
View File
@@ -1,5 +1,5 @@
import numpy as np
from leonida.classify import CLASSES, PALETTE, class_shares, classify
from leonida.classify import CLASSES, PALETTE, class_shares, classify, drop_map_labels
def block(color, size=8):
return np.full((size, size, 3), color, dtype=np.uint8)
@@ -34,3 +34,27 @@ def test_shares_sum_to_one():
shares = class_shares(classify(img))
assert abs(sum(shares.values()) - 1.0) < 1e-9
assert abs(shares["wasser"] - 0.5) < 1e-9
def test_letter_shaped_island_becomes_water():
"""Ein Buchstabe im Wasser ist Beschriftung, keine Stadt: keine natuerlichen Merkmale."""
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
labels[40:60, 40:120] = CLASSES["stadt"] # Querbalken
labels[40:120, 40:60] = CLASSES["strasse"] # Laengsbalken -> zusammen ein "L"
out = drop_map_labels(labels)
assert (out == CLASSES["wasser"]).all()
def test_real_island_survives():
"""Eine Insel mit Gruen und Sand bleibt, auch wenn sie klein und vom Festland getrennt ist."""
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
labels[40:120, 40:120] = CLASSES["stadt"]
labels[50:70, 50:70] = CLASSES["gruen"]
labels[100:115, 45:115] = CLASSES["sand"]
out = drop_map_labels(labels)
assert (out[40:120, 40:120] != CLASSES["wasser"]).all()
def test_mainland_is_never_dropped():
"""Die groesste Nicht-Wasser-Komponente bleibt immer, auch voellig ohne Gruen und Sand."""
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
labels[10:190, 10:100] = CLASSES["stadt"]
out = drop_map_labels(labels)
assert (out[10:190, 10:100] == CLASSES["stadt"]).all()