Files
gta6/tools/extract/leonida/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

77 lines
3.6 KiB
Python

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