Fix-Runde 1, vier Befunde aus dem Review (Rulings 25-28): Befund A (Critical): der Versatz nach außen konnte eine Palme über die Uferlinie ins Wasser schieben (218 von 2.433 auf dem echten Mosaik). palm_points() bekommt jetzt land_polys (vor seed), _verge_point() verwirft einen Kandidaten, der nicht auf Land liegt. Befund B (Important): regularize() vereinfachte im Rückfallzweig ein zweites Mal und gröber (1,0 m statt der 0,5 m, die mask_polygons() schon angewandt hatte) und prüfte erneut gegen MIN_AREA - das drückte kleine, treppenrandige Grundrisse unter die Schwelle (202 von 2.085 gebaeude-, 9 von 267 spekulativ-Kandidaten). Der Rückfallzweig vereinfacht jetzt nur noch einmal, mit 0,5 m, ohne zweite Flächenprüfung. Befund C: der globale filterwarnings-Eintrag in pyproject.toml (Fix für die oriented_envelope-RuntimeWarning aus der letzten Runde) war zu weit gefasst und hätte auch eine echte künftige Division durch Null versteckt. Er gehört nur auf den einen Test, der minimum_rotated_rectangle selbst aufruft - pyproject.toml zurück auf den Stand davor, @pytest.mark.filterwarnings auf test_l_shape_is_kept_not_boxed. Befund D (Minor): vier doppelte Palmenpunkte entfernt (dict.fromkeys vor dem return), zwei bisher ungetestete Zweige abgedeckt (spekulativ=True/False, Park-Streuung mit Determinismus-Check). Test zuerst für A und B, beide vor dem jeweiligen Fix rot gesehen (echte Assertion-Fehler, keine Signatur-Fehler). 31/31 Tests grün, weiterhin ohne Warnungen. Auf dem echten Mosaik: 2.352 Gebäude (vorher 2.141, +211 - exakt die Zahl, die Befund B vorher fallen ließ; 0 von regularize verworfen, siehe Bericht für die Erklärung), 267 spekulativ. 2.133 Palmenpunkte (Referenz des Koordinators von 2.176 war mit dem alten, kleineren Gebäudebestand gemessen - mit dem isoliert nachgebauten alten Bestand komme ich auf 2.175, die verbleibende Differenz von 1 ist im Bericht dokumentiert statt verschwiegen). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
64 lines
2.9 KiB
Python
64 lines
2.9 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. Vier Gebäudepixel an zwei gegenüberliegenden Ecken eines
|
|
4x4-Blocks fehlen, wie bei einer gefasten Ecke auf dem echten Mosaik.
|
|
"""
|
|
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"]
|
|
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
|