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