fix(extract): Kartenbeschriftung nicht mehr als Geometrie ausgeben
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0bf2c29061
commit
f3abb5e803
92 files changed
+161
-96
No files matched your search
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user