perf(B04): 표고 보간 삼각망을 물어볼 자리 둘레로 좁힘
노선 [확인] 이 293초였고 그중 285초가 배수유역 분석, 다시 그중 256초가 1차 영역 만들기였음. 범인은 ElevationSampler 하나임 — LinearNDInterpolator 가 들로네 삼각망을 첫 호출 때 만드는데 등고선 구름을 통째로(용화 317,348점) 넘겨 그 한 번이 256~1,481초로 뛰었음(편차 큼). 표고를 묻는 자리는 도로 교차 노드에 닿은 세류 조각 몇 개뿐임. 그 조각들만 먼저 골라 그 둘레 100m 안 등고선만 남기고 삼각망을 만듦. 여유 100m 는 등고선 재샘플 간격(5m)의 스무 배라 물어볼 점은 언제나 볼록껍질 안쪽이고 보간값이 그대로임. 실측(용화 5601e828) — 보간 구름 317,348 → 4,367점, 1차 영역 256.6s → 43.8s(측정 중 기기 부하가 9배였음, 준비 5.1s → 45.7s). 결과 완전 동일 — 1차 영역 266,697㎡ · 격자 545×835 · 268,456셀 · 노선 이탈 0m/1079m · 상류망 1 / 하류망 109 / 미연결 1153. pytest 409 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -90,14 +90,25 @@ def split_streams_at_road(
|
|||||||
node_edges.setdefault(head, []).append(index)
|
node_edges.setdefault(head, []).append(index)
|
||||||
node_edges.setdefault(tail, []).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]] = {}
|
upper_seeds: dict[int, tuple[float, float]] = {}
|
||||||
lower_seeds: dict[int, tuple[float, float]] = {}
|
lower_seeds: dict[int, tuple[float, float]] = {}
|
||||||
for index, piece in enumerate(pieces):
|
for index, piece, touching in seed_pieces:
|
||||||
touching = [node for node in ends[index] if node in crossing_nodes]
|
|
||||||
if not touching:
|
|
||||||
continue
|
|
||||||
heights = sampler.at(np.array(touching, dtype=np.float64))
|
heights = sampler.at(np.array(touching, dtype=np.float64))
|
||||||
crossing_node = touching[int(np.argmin(heights))]
|
crossing_node = touching[int(np.argmin(heights))]
|
||||||
if _mean_elevation(piece, sampler) > float(np.min(heights)):
|
if _mean_elevation(piece, sampler) > float(np.min(heights)):
|
||||||
@@ -210,16 +221,42 @@ class ElevationSampler:
|
|||||||
최근접 등고선 정점만 쓰면 오차가 등고선 간격(주곡선 5m)만큼 나서, 계곡 교차점 표고가
|
최근접 등고선 정점만 쓰면 오차가 등고선 간격(주곡선 5m)만큼 나서, 계곡 교차점 표고가
|
||||||
실제보다 한 등고선 위로 잡히고 상류 조각이 통째로 하류로 오판된다. TIN 선형보간을
|
실제보다 한 등고선 위로 잡히고 상류 조각이 통째로 하류로 오판된다. TIN 선형보간을
|
||||||
1차로 쓰고, 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
|
self._z = cloud.z
|
||||||
if cloud.is_empty:
|
if cloud.is_empty:
|
||||||
self._interpolator = None
|
self._interpolator = None
|
||||||
self._tree = None
|
self._tree = None
|
||||||
return
|
return
|
||||||
self._interpolator = LinearNDInterpolator(cloud.xy, cloud.z)
|
xy = np.asarray(cloud.xy, dtype=np.float64)
|
||||||
self._tree = cKDTree(cloud.xy)
|
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:
|
def at(self, xy: np.ndarray) -> np.ndarray:
|
||||||
"""(N, 2) 좌표의 표고 (N,)."""
|
"""(N, 2) 좌표의 표고 (N,)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user