fix(extract): Mindestbreite gegen papierdünne und wandartige Gebäudegrundrisse
727 von 2.286 rohen Grundrissen (31,8 %, aber nur 1,8 % der bebauten Fläche) sind unter 4 m an der kurzen Seite ihres gedrehten Hüllrechtecks — extrudiert werden daraus Wände, keine Häuser. Eine Mindestbreite allein reicht nicht: einzelne Formen bleiben bei >= 4 m Breite trotzdem extrem lang und dünn (z.B. 4,27 m x 153 m, Verhältnis 35,9). MAX_ASPECT_RATIO=12 ergänzt die Mindestbreite MIN_BUILDING_WIDTH=4, an einer echten Lücke in den gemessenen Verhältnissen festgemacht (größte plausible große Grundfläche: Verhältnis 10,8 bei 7.196 m²; nächste eindeutig wandartige Form: Verhältnis 12,0 bei 60,5 m²). Zusammen fallen 766 Grundrisse (33,5 %, 36.986 m² = 3,3 % der Fläche) weg. Bestehende Fixture in test_small_staircase_building_is_not_dropped vergrößert (4x4- auf 6x6-Block): ihre kurze Seite lag zufällig bei ~3,7 m, unter der neuen Schwelle, und hätte den Test sonst für den falschen Grund bestehen lassen (Ruling 74 statt Ruling 28). Pipeline neu durchgelaufen: eindeutige Gebäude 2.282 -> 1.520 (-33,4 %), Dreiecke über alle 144 Chunks 327.181 -> 307.678 (-6,0 %, vollständig aus der Gebäude-Ebene), public/data/vice-a 2,73 M -> 2,50 M. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad9bf63b36
commit
30cf64642b
129 files changed
+205
-138
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
@@ -9,6 +10,21 @@ from .vectors import mask_polygons
|
||||
MIN_AREA = 12.0
|
||||
RECT_COVERAGE = 0.85
|
||||
|
||||
# Befund C (Ruling 74): an den 2.286 rohen Grundrissen des echten Mosaiks gemessen (kurze
|
||||
# Seite des gedrehten Huellrechtecks) - p5 1,0 m, p25 3,5 m, p50 5,0 m, die fuenf duennsten
|
||||
# 0,50 m breit bei bis zu 164 m Laenge. Unter MIN_BUILDING_WIDTH liegen 727 Grundrisse
|
||||
# (31,8 % der Anzahl, aber nur 20.575 m² = 1,8 % der bebauten Flaeche) - extrudiert werden
|
||||
# daraus Waende, keine Haeuser. 4 m reicht als alleinige Schwelle nicht: einige Grundrisse
|
||||
# bleiben bei >= 4 m Breite trotzdem sehr lang und duenn (z.B. 4,27 m x 153 m, Verhaeltnis
|
||||
# 35,9) - MAX_ASPECT_RATIO faengt genau diese "4x100-m-Wand"-Faelle zusaetzlich ab. Schwelle
|
||||
# aus einer Luecke in den gemessenen Verhaeltnissen hergeleitet: die groesste noch plausible
|
||||
# grosse Grundflaeche (30,1 m x 324 m, 7.196 m², Verhaeltnis 10,8) bleibt darunter, die naechst-
|
||||
# hoehere Form im Datensatz (Verhaeltnis 12,0, nur 60,5 m²) darueber - 12 liegt in dieser Luecke.
|
||||
# Zusammen fallen 766 Grundrisse (33,5 %, 36.986 m² = 3,3 % der Flaeche) weg, s. Bericht fuer
|
||||
# die vollstaendige Herleitung und die Verteilung.
|
||||
MIN_BUILDING_WIDTH = 4.0
|
||||
MAX_ASPECT_RATIO = 12.0
|
||||
|
||||
|
||||
def regularize(poly: Polygon) -> Polygon | None:
|
||||
"""Rechteck, wenn das Polygon sein gedrehtes Hüllrechteck gut ausfüllt; sonst vereinfacht."""
|
||||
@@ -28,12 +44,33 @@ def regularize(poly: Polygon) -> Polygon | None:
|
||||
return poly.simplify(0.5)
|
||||
|
||||
|
||||
def _rect_sides(poly: Polygon) -> tuple[float, float]:
|
||||
"""Kurze und lange Seite von poly.minimum_rotated_rectangle, in Metern."""
|
||||
with warnings.catch_warnings():
|
||||
# Siehe Kommentar in regularize(): dieselbe harmlose GEOS-RuntimeWarning bei bereits
|
||||
# achsenparallelen/rechtwinkligen Polygonen.
|
||||
warnings.filterwarnings("ignore", message=r".*oriented_envelope.*", category=RuntimeWarning)
|
||||
rect = poly.minimum_rotated_rectangle
|
||||
corners = list(rect.exterior.coords)[:4]
|
||||
edges = [math.dist(corners[i - 1], corners[i]) for i in range(4)]
|
||||
return min(edges), max(edges)
|
||||
|
||||
|
||||
def _is_wall_like(poly: Polygon) -> bool:
|
||||
"""Befund C (Ruling 74): papierduenne oder sehr lange schmale Grundrisse ergeben beim
|
||||
Extrudieren Waende statt Haeuser, s. Konstanten oben fuer die Herleitung der Schwellen."""
|
||||
short, long_ = _rect_sides(poly)
|
||||
if short < MIN_BUILDING_WIDTH:
|
||||
return True
|
||||
return (long_ / short) > MAX_ASPECT_RATIO
|
||||
|
||||
|
||||
def building_polygons(labels: np.ndarray) -> list[tuple[Polygon, bool]]:
|
||||
out = []
|
||||
for name, speculative in (("gebaeude", False), ("spekulativ", True)):
|
||||
mask = labels == CLASSES[name]
|
||||
for poly in mask_polygons(mask, min_area=MIN_AREA, simplify=0.5):
|
||||
reg = regularize(poly)
|
||||
if reg is not None:
|
||||
if reg is not None and not _is_wall_like(reg):
|
||||
out.append((reg, speculative))
|
||||
return out
|
||||
@@ -36,17 +36,47 @@ def test_small_staircase_building_is_not_dropped():
|
||||
MIN_AREA. Ein Grundriss mit gestuftem (Treppenrand-)Eck, der die Rechteckquote
|
||||
von 0,85 verfehlt, wurde bisher ein zweites Mal - und gröber, mit 1,0 m -
|
||||
vereinfacht und damit unter MIN_AREA gedrückt, obwohl er die 0,5-m-Vereinfachung
|
||||
klar übersteht. An zwei gegenüberliegenden Ecken eines 4x4-Blocks fehlt je ein
|
||||
Gebäudepixel, wie bei einer gefasten Ecke auf dem echten Mosaik.
|
||||
klar übersteht. An zwei gegenüberliegenden Ecken eines 6x6-Blocks fehlt je ein
|
||||
2x2-Gebäudepixelquadrat, wie bei einer gefasten Ecke auf dem echten Mosaik.
|
||||
|
||||
Fix-Runde 3 / Befund C: die Fixture war ursprünglich ein 4x4-Block mit je einem
|
||||
fehlenden Eckpixel (kurze Seite danach ca. 3,7 m) - das kollidierte zufällig mit
|
||||
der neuen Mindestbreite (Ruling 74, MIN_BUILDING_WIDTH=4.0) und hätte diesen Test
|
||||
in einen Test für den falschen Grund verwandelt (weggefallen wegen Breite statt
|
||||
weil die Rechteckquote verfehlt wird). Größer gewählt (kurze Seite ca. 5,8 m),
|
||||
damit dieser Test weiterhin ausschließlich Ruling 28 prüft, unabhängig von 74.
|
||||
"""
|
||||
labels = np.full((40, 40), CLASSES["stadt"], dtype=np.uint8)
|
||||
block = np.array([
|
||||
[1, 1, 1, 0],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[0, 1, 1, 1],
|
||||
], dtype=bool)
|
||||
labels[10:14, 10:14][block] = CLASSES["gebaeude"]
|
||||
labels = np.full((60, 60), CLASSES["stadt"], dtype=np.uint8)
|
||||
block = np.ones((6, 6), dtype=bool)
|
||||
block[0:2, 4:6] = False # obere rechte 2x2-Ecke fehlt
|
||||
block[4:6, 0:2] = False # untere linke 2x2-Ecke fehlt
|
||||
labels[10:16, 10:16][block] = CLASSES["gebaeude"]
|
||||
assert len(building_polygons(labels)) == 1
|
||||
|
||||
|
||||
# Befund C (Ruling 74, Fix-Runde 3): 727 von 2.286 rohen Grundrissen (31,8 %, aber nur 1,8 %
|
||||
# der bebauten Flaeche) sind unter 4 m an der kurzen Seite ihres gedrehten Huellrechtecks -
|
||||
# extrudiert werden daraus Waende, keine Haeuser. Zahlen an den echten Daten nachgemessen,
|
||||
# s. Bericht.
|
||||
def test_thin_sliver_building_is_dropped():
|
||||
"""Unter MIN_BUILDING_WIDTH (4 m) kurzer Seite faellt ein Grundriss weg, unabhaengig von
|
||||
der Flaeche (hier 60 m2, weit ueber MIN_AREA=12 - die Flaeche allein rettet es nicht)."""
|
||||
labels = np.full((60, 60), CLASSES["stadt"], dtype=np.uint8)
|
||||
labels[10:12, 10:40] = CLASSES["gebaeude"] # 2 m breit, 30 m lang
|
||||
assert building_polygons(labels) == []
|
||||
|
||||
def test_long_narrow_building_just_above_min_width_is_still_dropped():
|
||||
"""Ruling 74: 'eine 4x100-m-Form ist immer noch eine Wand' - Mindestbreite allein reicht
|
||||
nicht; ab MIN_BUILDING_WIDTH greift zusaetzlich eine Obergrenze fuer laenge/breite."""
|
||||
labels = np.full((150, 150), CLASSES["stadt"], dtype=np.uint8)
|
||||
labels[10:15, 10:110] = CLASSES["gebaeude"] # 5 m breit, 100 m lang -> Verhaeltnis 20
|
||||
assert building_polygons(labels) == []
|
||||
|
||||
def test_moderately_elongated_building_above_min_width_is_kept():
|
||||
"""Regression: die Verhaeltnis-Obergrenze darf normale, nur maessig lang gestreckte
|
||||
Gebaeude nicht treffen (10 m breit, 30 m lang -> Verhaeltnis 3, klar unter der Schwelle)."""
|
||||
labels = np.full((60, 60), CLASSES["stadt"], dtype=np.uint8)
|
||||
labels[10:20, 10:40] = CLASSES["gebaeude"]
|
||||
assert len(building_polygons(labels)) == 1
|
||||
|
||||
def test_speculative_flag_matches_pixel_class():
|
||||
|
||||
Reference in new issue
Block a user