import math import warnings import numpy as np from shapely.geometry import Polygon from .classify import CLASSES 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.""" poly = poly.buffer(0) if poly.geom_type != "Polygon" or poly.area < MIN_AREA: return None with warnings.catch_warnings(): # Shapelys oriented_envelope (minimum_rotated_rectangle) wertet einen Winkelterm # aus, der für bereits achsenparallele/rechtwinklige Polygone exakt 0/0 wird - # GEOS meldet das als RuntimeWarning, obwohl das zurückgegebene Rechteck korrekt # bleibt (geprüft: Fläche stimmt, kein NaN). Nur diese eine Property-Abfrage ist # eingeschränkt, damit ein echter Warnhinweis an anderer Stelle sichtbar bliebe. warnings.filterwarnings("ignore", message=r".*oriented_envelope.*", category=RuntimeWarning) rect = poly.minimum_rotated_rectangle if rect.area > 0 and poly.area / rect.area >= RECT_COVERAGE: return rect 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 and not _is_wall_like(reg): out.append((reg, speculative)) return out