Files
gta6/tools/extract/tests/test_buildings.py
T
knsoandClaude Sonnet 5 30cf64642b 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>
2026-09-13 05:14:42 +02:00

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