diff --git a/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md b/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md index 3421e75..3bb9fb4 100644 --- a/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md +++ b/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md @@ -701,6 +701,7 @@ from .classify import CLASSES from .coords import SLICE ROAD_CLASSES = ("strasse", "weg", "spekulativ") +BRIDGE_MARGIN_PX = 3 def _to_world(contour: np.ndarray) -> list[tuple[float, float]]: @@ -718,7 +719,7 @@ def mask_polygons(mask: np.ndarray, min_area: float, simplify: float) -> list[Po poly = Polygon(ring).buffer(0) if poly.is_empty or poly.area < min_area: continue - (holes if not _is_outer(mask, contour - 1) else outers).append(poly) + (outers if _signed_area(contour - 1) > 0 else holes).append(poly) result = [] for outer in outers: inner = [h.exterior.coords for h in holes if outer.contains(h)] @@ -726,12 +727,16 @@ def mask_polygons(mask: np.ndarray, min_area: float, simplify: float) -> list[Po return [p for p in result if p.geom_type == "Polygon" and p.area >= min_area] -def _is_outer(mask: np.ndarray, contour: np.ndarray) -> bool: - """Innen liegende Konturen umschließen Löcher.""" - r, c = contour.mean(axis=0) - r = int(np.clip(r, 0, mask.shape[0] - 1)) - c = int(np.clip(c, 0, mask.shape[1] - 1)) - return bool(mask[r, c]) +def _signed_area(contour: np.ndarray) -> float: + """Shoelace-Summe in Pixelkoordinaten (Zeile, Spalte). + + find_contours liefert Außenkonturen und Löcher mit entgegengesetztem Umlaufsinn; + das Vorzeichen trennt sie zuverlässig. Eine Stichprobe der Maske am Mittelpunkt der + Kontur versagt bei konzentrischen Formen: bei einer Insel mit Lagune liegt der + Mittelpunkt der Außenkontur selbst in der Lagune. + """ + row, col = contour[:, 0], contour[:, 1] + return float(0.5 * np.sum(col * np.roll(row, -1) - np.roll(col, -1) * row)) def land_polygons(labels: np.ndarray) -> list[Polygon]: @@ -754,6 +759,11 @@ def road_polygons(labels: np.ndarray) -> list[tuple[Polygon, bool]]: out = [] for poly in polys: r0, c0, r1, c1 = _bbox_px(poly) + # Ohne Rand enthält das Fenster nur Straßenpixel und damit nie Wasser. + r0 = max(r0 - BRIDGE_MARGIN_PX, 0) + c0 = max(c0 - BRIDGE_MARGIN_PX, 0) + r1 = min(r1 + BRIDGE_MARGIN_PX, water.shape[0] - 1) + c1 = min(c1 + BRIDGE_MARGIN_PX, water.shape[1] - 1) window = water[r0:r1 + 1, c0:c1 + 1] out.append((poly, bool(window.mean() > 0.3))) return out