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>
94 lines
5.0 KiB
Python
94 lines
5.0 KiB
Python
import numpy as np
|
|
import pytest
|
|
from shapely.geometry import Polygon
|
|
from leonida.buildings import building_polygons, regularize
|
|
from leonida.classify import CLASSES
|
|
|
|
def test_rough_rectangle_snaps_to_rectangle():
|
|
poly = Polygon([(0, 0), (10.4, 0.3), (10.1, 20.2), (-0.2, 19.8)])
|
|
out = regularize(poly)
|
|
assert len(out.exterior.coords) == 5 # geschlossenes Rechteck
|
|
assert abs(out.area - 205) < 25
|
|
|
|
@pytest.mark.filterwarnings("ignore:.*oriented_envelope.*:RuntimeWarning")
|
|
def test_l_shape_is_kept_not_boxed():
|
|
"""out.minimum_rotated_rectangle (im Assert unten) loest fuer dieses achsen-
|
|
parallele Ergebnispolygon eine harmlose Shapely/GEOS-RuntimeWarning aus
|
|
(Winkelterm wird 0/0, das Rechteck bleibt korrekt) - siehe Befund C, Ruling 25.
|
|
Nur dieser eine Test filtert sie; die Suite tut es nicht mehr global."""
|
|
poly = Polygon([(0, 0), (30, 0), (30, 10), (10, 10), (10, 30), (0, 30)])
|
|
out = regularize(poly)
|
|
assert out.area < 0.8 * out.minimum_rotated_rectangle.area
|
|
|
|
def test_tiny_polygon_is_dropped():
|
|
assert regularize(Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])) is None
|
|
|
|
def test_two_buildings_are_separated():
|
|
labels = np.full((60, 60), CLASSES["stadt"], dtype=np.uint8)
|
|
labels[10:25, 10:25] = CLASSES["gebaeude"]
|
|
labels[35:50, 35:50] = CLASSES["gebaeude"]
|
|
assert len(building_polygons(labels)) == 2
|
|
|
|
def test_small_staircase_building_is_not_dropped():
|
|
"""regularize() darf nicht doppelt vereinfachen (Ruling 28).
|
|
|
|
mask_polygons() liefert Kandidaten bereits mit 0,5 m simplify und mindestens
|
|
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 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((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():
|
|
"""building_polygons() markiert nur aus 'spekulativ' (rot) entstandene Grundrisse
|
|
als spekulativ; aus 'gebaeude' (grau) entstandene bleiben False."""
|
|
labels = np.full((60, 60), CLASSES["stadt"], dtype=np.uint8)
|
|
labels[10:25, 10:25] = CLASSES["gebaeude"] # grau, kleinere Spalten -> kleineres Welt-x
|
|
labels[35:50, 35:50] = CLASSES["spekulativ"] # rot, groessere Spalten -> groesseres Welt-x
|
|
result = building_polygons(labels)
|
|
assert len(result) == 2
|
|
result.sort(key=lambda item: item[0].centroid.x)
|
|
(_, spec_from_gebaeude), (_, spec_from_spekulativ) = result
|
|
assert spec_from_gebaeude is False
|
|
assert spec_from_spekulativ is True
|