docs: Plan auf Rulings 24-29 umstellen (Palmen, Gebäude)
palm_points bekommt die Landflächen und verwirft, was nicht darauf liegt: am echten Mosaik standen 218 von 2.433 Palmen im Wasser. regularize vereinfacht nicht mehr ein zweites Mal und gröber als mask_polygons, was 202 von 2.085 Gebäuden verworfen hatte. Dazu die vier neuen Tests, der Marker am einzelnen Test statt eines suiteweiten Filters, die Entdoppelung der Palmenpunkte und die korrigierte Signatur von assign_heights. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42f2a4415d
commit
2afae81bce
1 file changed
+74
-11
@@ -893,7 +893,7 @@ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>"
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `mask_polygons`, `centerlines` aus Task 4, `CLASSES` aus Task 3
|
||||
- Produces: `regularize(poly) -> Polygon | None`, `building_polygons(labels) -> list[tuple[Polygon, bool]]` (Polygon, `spekulativ`), `assign_heights(polys, landmarks, districts, seed) -> list[float]`, `height_sources(...) -> list[str]`, `palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed) -> list[tuple[float, float]]`
|
||||
- Produces: `regularize(poly) -> Polygon | None`, `building_polygons(labels) -> list[tuple[Polygon, bool]]` (Polygon, `spekulativ`), `assign_heights(polys, landmarks, districts, seed) -> list[dict]` (je Gebäude `height` und `source`), `palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed) -> list[tuple[float, float]]`
|
||||
|
||||
- [ ] **Step 1: Den fehlschlagenden Test für Gebäude schreiben**
|
||||
|
||||
@@ -901,6 +901,7 @@ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>"
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
from leonida.buildings import building_polygons, regularize
|
||||
from leonida.classify import CLASSES
|
||||
@@ -911,7 +912,12 @@ def test_rough_rectangle_snaps_to_rectangle():
|
||||
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():
|
||||
"""Das Assert unten ruft minimum_rotated_rectangle selbst auf und löst damit die
|
||||
harmlose Shapely-RuntimeWarning aus (Winkelterm 0/0, Rechteck korrekt). Nur dieser
|
||||
eine Test filtert sie — nicht die Suite, sonst wäre auch eine echte Division durch
|
||||
Null künftig unsichtbar."""
|
||||
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
|
||||
@@ -924,6 +930,36 @@ def test_two_buildings_are_separated():
|
||||
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.
|
||||
|
||||
mask_polygons liefert Kandidaten bereits mit 0,5 m simplify und mindestens MIN_AREA.
|
||||
Ein Grundriss mit gestuftem Eck, der die Rechteckquote verfehlt, wurde bisher ein
|
||||
zweites Mal und gröber (1,0 m) vereinfacht und damit unter MIN_AREA gedrückt. Am
|
||||
echten Mosaik betraf das 202 von 2.085 Gebäuden.
|
||||
"""
|
||||
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():
|
||||
"""Nur aus 'spekulativ' (rot) entstandene Grundrisse sind spekulativ."""
|
||||
labels = np.full((60, 60), CLASSES["stadt"], dtype=np.uint8)
|
||||
labels[10:25, 10:25] = CLASSES["gebaeude"] # grau -> kleineres Welt-x
|
||||
labels[35:50, 35:50] = CLASSES["spekulativ"] # rot -> größeres 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
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen, er muss scheitern**
|
||||
@@ -952,8 +988,9 @@ def regularize(poly: Polygon) -> Polygon | None:
|
||||
rect = poly.minimum_rotated_rectangle
|
||||
if rect.area > 0 and poly.area / rect.area >= RECT_COVERAGE:
|
||||
return rect
|
||||
simple = poly.simplify(1.0)
|
||||
return simple if simple.area >= MIN_AREA else None
|
||||
# Gleiche Toleranz wie mask_polygons: ein zweiter, gröberer Durchgang schrumpfte
|
||||
# kleine Grundrisse unter MIN_AREA und verwarf am echten Mosaik 202 von 2.085.
|
||||
return poly.simplify(0.5)
|
||||
|
||||
|
||||
def building_polygons(labels: np.ndarray) -> list[tuple[Polygon, bool]]:
|
||||
@@ -1087,7 +1124,7 @@ def test_no_palm_stands_on_the_carriageway():
|
||||
road = Polygon([(0, -9), (200, -9), (200, 9), (0, 9)]) # 18 m breite Fahrbahn
|
||||
line = LineString([(0, 0), (200, 0)])
|
||||
sand = Polygon([(0, 20), (200, 20), (200, 40), (0, 40)])
|
||||
pts = palm_points([sand], [], [line], [], [road], seed=1)
|
||||
pts = palm_points([sand], [], [line], [], [road], [], seed=1)
|
||||
assert pts
|
||||
assert all(road.distance(Point(x, y)) >= PALM_KERB for x, y in pts)
|
||||
|
||||
@@ -1096,9 +1133,29 @@ def test_palms_do_not_pile_up_where_centerlines_meet():
|
||||
auf zwei gegenüberliegenden Ästen zwei Palmen in genau denselben Punkt."""
|
||||
sand = Polygon([(-80, 20), (80, 20), (80, 40), (-80, 40)])
|
||||
arms = [LineString([(0, 0), (60, 0)]), LineString([(0, 0), (-60, 0)])]
|
||||
pts = palm_points([sand], [], arms, [], [], seed=1)
|
||||
pts = palm_points([sand], [], arms, [], [], [], seed=1)
|
||||
assert pts
|
||||
assert len(set(pts)) == len(pts)
|
||||
|
||||
def test_no_palm_stands_in_the_water():
|
||||
"""Der Versatz nach außen schiebt Palmen an Küstenstraßen über die Uferlinie.
|
||||
Am echten Mosaik standen so 218 von 2.433 Palmen im Wasser."""
|
||||
land = Polygon([(-100, -100), (100, -100), (100, 0), (-100, 0)]) # Ufer bei y = 0
|
||||
road = Polygon([(-100, -12), (100, -12), (100, 0), (-100, 0)])
|
||||
line = LineString([(-100, -6), (100, -6)])
|
||||
sand = Polygon([(-100, 0), (100, 0), (100, 10), (-100, 10)])
|
||||
pts = palm_points([sand], [], [line], [], [road], [land], seed=1)
|
||||
assert pts
|
||||
assert all(land.contains(Point(x, y)) for x, y in pts)
|
||||
|
||||
def test_park_scatter_is_deterministic_and_inside_the_park():
|
||||
"""Ohne Sperrflächen verstreut ein Park reproduzierbare Punkte."""
|
||||
park = Polygon([(0, 0), (50, 0), (50, 50), (0, 50)]) # 2500 m² -> 10 Punkte
|
||||
pts_a = palm_points([], [park], [], [], [], [], seed=7)
|
||||
pts_b = palm_points([], [park], [], [], [], [], seed=7)
|
||||
assert pts_a
|
||||
assert all(park.contains(Point(x, y)) for x, y in pts_a)
|
||||
assert pts_a == pts_b
|
||||
```
|
||||
|
||||
- [ ] **Step 10: Palmenpunkte ergänzen**
|
||||
@@ -1116,8 +1173,9 @@ PALM_KERB = 1.5 # Meter Abstand zum Fahrbahnrand
|
||||
PALM_VERGE = (6.0, 8.0, 10.0, 12.0, 14.0, 16.0)
|
||||
|
||||
|
||||
def _verge_point(line, p, side: str, roads, buildings):
|
||||
"""Erster Versatz von innen nach außen, der Fahrbahn und Gebäude freihält."""
|
||||
def _verge_point(line, p, side: str, roads, buildings, land):
|
||||
"""Erster Versatz von innen nach außen, der Fahrbahn und Gebäude freihält und
|
||||
auf Land bleibt (sonst schiebt der Versatz Küstenpalmen ins Wasser)."""
|
||||
for dist in PALM_VERGE:
|
||||
off = line.parallel_offset(dist, side)
|
||||
if off.is_empty:
|
||||
@@ -1127,16 +1185,20 @@ def _verge_point(line, p, side: str, roads, buildings):
|
||||
continue
|
||||
if buildings is not None and q.distance(buildings) < PALM_CLEARANCE:
|
||||
continue
|
||||
if land is not None and not land.contains(q):
|
||||
continue
|
||||
return [(round(q.x, 2), round(q.y, 2))]
|
||||
return []
|
||||
|
||||
|
||||
def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed: int):
|
||||
def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys,
|
||||
land_polys, seed: int):
|
||||
"""Palmen entlang strandnaher Mittellinien und verstreut in Parks."""
|
||||
from shapely.ops import unary_union
|
||||
sand = unary_union(sand_polys) if sand_polys else None
|
||||
roads = unary_union(list(road_polys)) if road_polys else None
|
||||
buildings = unary_union(list(building_polys)) if building_polys else None
|
||||
land = unary_union(land_polys) if land_polys else None
|
||||
blocked = unary_union([g for g in (roads, buildings) if g is not None]) \
|
||||
if (roads is not None or buildings is not None) else None
|
||||
points = []
|
||||
@@ -1149,7 +1211,7 @@ def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys,
|
||||
while pos <= line.length:
|
||||
p = line.interpolate(pos)
|
||||
for side in ("left", "right"):
|
||||
points.extend(_verge_point(line, p, side, roads, buildings))
|
||||
points.extend(_verge_point(line, p, side, roads, buildings, land))
|
||||
pos += PALM_SPACING
|
||||
for park in park_polys:
|
||||
minx, miny, maxx, maxy = park.bounds
|
||||
@@ -1160,7 +1222,7 @@ def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys,
|
||||
q = Point(minx + u * (maxx - minx), miny + v * (maxy - miny))
|
||||
if park.contains(q) and (blocked is None or q.distance(blocked) > PALM_CLEARANCE):
|
||||
points.append((round(q.x, 2), round(q.y, 2)))
|
||||
return points
|
||||
return list(dict.fromkeys(points))
|
||||
```
|
||||
|
||||
- [ ] **Step 11: Test laufen lassen, er muss bestehen**
|
||||
@@ -1394,7 +1456,8 @@ def main() -> None:
|
||||
landmarks = json.loads(Path("data/skyline.json").read_text())["landmarks"]
|
||||
districts = json.loads(Path("data/districts.json").read_text())
|
||||
heights = assign_heights([p for p, _ in buildings], landmarks, districts, SEED)
|
||||
palms = palm_points(sand, parks, lines, [p for p, _ in buildings], road_shapes, SEED)
|
||||
palms = palm_points(sand, parks, lines, [p for p, _ in buildings], road_shapes,
|
||||
land, SEED)
|
||||
|
||||
payloads = []
|
||||
for j in range(12):
|
||||
|
||||
Reference in new issue
Block a user