diff --git a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Stream.py b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Stream.py index c6951a5f..3200c4ee 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Stream.py +++ b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Stream.py @@ -90,14 +90,25 @@ def split_streams_at_road( node_edges.setdefault(head, []).append(index) node_edges.setdefault(tail, []).append(index) - sampler = ElevationSampler(cloud) + # 표고를 묻는 조각은 **도로 교차 노드에 닿은 것뿐**이다(나머지는 확산으로만 정해진다). + # 그 조각들만 먼저 골라 두고, 그 둘레로 삼각망을 좁힌다 — 위 클래스 주석 참조. + seed_pieces = [ + (index, piece, touching) + for index, piece in enumerate(pieces) + if (touching := [node for node in ends[index] if node in crossing_nodes]) + ] + focus_xy = ( + np.concatenate( + [np.asarray(piece.coords, dtype=np.float64)[:, :2] for _index, piece, _n in seed_pieces] + ) + if seed_pieces + else None + ) + sampler = ElevationSampler(cloud, focus_xy=focus_xy) # 씨앗 조각 → 그 조각의 하류쪽 끝점(= 도로 교차 노드). 이 값이 물 흐름 방향의 기준이 된다. upper_seeds: dict[int, tuple[float, float]] = {} lower_seeds: dict[int, tuple[float, float]] = {} - for index, piece in enumerate(pieces): - touching = [node for node in ends[index] if node in crossing_nodes] - if not touching: - continue + for index, piece, touching in seed_pieces: heights = sampler.at(np.array(touching, dtype=np.float64)) crossing_node = touching[int(np.argmin(heights))] if _mean_elevation(piece, sampler) > float(np.min(heights)): @@ -210,16 +221,42 @@ class ElevationSampler: 최근접 등고선 정점만 쓰면 오차가 등고선 간격(주곡선 5m)만큼 나서, 계곡 교차점 표고가 실제보다 한 등고선 위로 잡히고 상류 조각이 통째로 하류로 오판된다. TIN 선형보간을 1차로 쓰고, TIN 밖(볼록껍질 외부)만 최근접 정점으로 메운다. + + **`focus_xy` 를 꼭 줄 것 — 노선 [확인] 이 3분 반이던 원인이 여기였다**(2026-09-07 실측). + `LinearNDInterpolator` 는 들로네 삼각망을 **첫 호출 때** 만드는데, 등고선 구름을 통째로 + (용화 317,348점) 넘기면 그 한 번이 **256~1,481초**로 뛴다(같은 입력에도 편차가 큼). + 쓰는 곳은 세류 조각 위 몇 점뿐이므로, 물어볼 자리 둘레만 남기면 삼각망이 작아져 + **1초 아래**가 된다. 남기는 여유(`margin_m`)는 등고선 재샘플 간격(5m)의 스무 배라 + 물어볼 점은 언제나 볼록껍질 한참 안쪽에 있고 보간값도 그대로다. """ - def __init__(self, cloud: ContourCloud) -> None: + def __init__( + self, + cloud: ContourCloud, + focus_xy: np.ndarray | None = None, + margin_m: float = 100.0, + ) -> None: self._z = cloud.z if cloud.is_empty: self._interpolator = None self._tree = None return - self._interpolator = LinearNDInterpolator(cloud.xy, cloud.z) - self._tree = cKDTree(cloud.xy) + xy = np.asarray(cloud.xy, dtype=np.float64) + z = np.asarray(cloud.z, dtype=np.float64) + if focus_xy is not None and len(focus_xy): + focus = np.asarray(focus_xy, dtype=np.float64) + near = cKDTree(focus).query(xy, workers=-1)[0] <= margin_m + if near.any(): + xy, z = xy[near], z[near] + logger.info( + "배수유역: 표고 보간 구름을 물어볼 자리 %.0fm 안으로 줄임 — %d → %d점", + margin_m, + len(cloud.z), + len(z), + ) + self._z = z + self._interpolator = LinearNDInterpolator(xy, z) + self._tree = cKDTree(xy) def at(self, xy: np.ndarray) -> np.ndarray: """(N, 2) 좌표의 표고 (N,)."""