From f1834465c80d2f5d876fbf37c2fc9a70c84b4623 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 05:29:18 +0900 Subject: [PATCH] =?UTF-8?q?perf(B04):=20=EC=B0=BD=EC=9D=B4=20=EB=AA=A8?= =?UTF-8?q?=EC=9E=90=EB=9D=BC=EB=A9=B4=20=EA=B7=B8=20=ED=83=80=EC=9D=BC?= =?UTF-8?q?=EB=A7=8C=20=EB=84=93=ED=98=80=20=EB=8B=A4=EC=8B=9C=20=EC=9E=AC?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN 0-10 이어짐. 첫 판에서는 창이 모자란 단계를 **격자 전체**로 되돌렸는데, 격자가 커질수록 그 폴백이 25/91 까지 늘어 이득이 대부분 사라졌음(실측 확장 97.7s). - 모자란 타일만 창을 4배로 넓혀 다시 잼. 단계 전체를 되돌리지 않음. - 타일 256 → 128 (실측: 128/여유32 2.1s vs 256/여유32 2.7s). - 넓힘 횟수를 계측 줄에 남김(폴백 대신). 자체검증 — 전체격자 5.2s → 2.1s(넓힘 15/91), 거리·목표행·목표열 전부 동일. `build_contour_descent` 결과도 옛 코드와 완전 동일(8.0s → 5.1s). 시험 500 통과. Co-Authored-By: Claude Opus 5 (1M context) --- ...B04_PreProcess_Engine_Watershed_Descent.py | 93 +++++++++++-------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Descent.py b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Descent.py index b15c16ca..5f7a54e7 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Descent.py +++ b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Descent.py @@ -130,18 +130,17 @@ def rasterize_contours( # 타일만** 그 둘레 `margin` 까지 잘라 EDT 를 돌린다. # # 창 밖에 더 가까운 등고선이 있을 수 있으면(창 안 최대 거리가 여유에 닿거나 창에 낮은 -# 라인이 없으면) 그 단계를 전체 격자로 다시 돌린다 — **근사가 아니라 같은 값을 싸게 -# 구하는 것**. 값이 같은지는 `np.array_equal` 로 확인했다(거리·목표행·목표열 전부). +# 라인이 없으면) **그 타일만** 창을 4배로 넓혀 다시 잰다 — 근사가 아니라 같은 값을 싸게 +# 구하는 것. 단계 전체를 격자 전체로 되돌리면 이득이 사라진다(그 방식일 때 폴백 25/91). # -# 고른 근거(745×1035 격자·166단 중 EDT 도는 91단, 전체격자 방식 10.8s): -# 타일 128 여유 32 → 3.2s(폴백 9) 타일 256 여유 24 → 3.3s(폴백 27) -# **타일 256 여유 32 → 2.9s(폴백 9)** 타일 256 여유 48 → 3.2s(폴백 4) -# 타일 512 여유 32 → 4.4s(폴백 9) -# 여유는 타일 크기와 무관하게 **거리 실측**으로 정했다 — 한 단 아래 등고선은 주곡선 -# 5m·셀 1m 에서 대개 수십 칸 안이다. 지형이 완만해 폴백이 잦아지면(로그에 횟수가 -# 찍힌다) 여유를 늘릴 것. +# 고른 근거(745×1035 격자·166단 중 EDT 도는 91단, 전체격자 방식 5.2s): +# **타일 128 여유 32 → 2.1s(넓힘 15)** 타일 128 여유 24 → 1.9s(넓힘 52) +# 타일 128 여유 48 → 2.5s(넓힘 7) 타일 256 여유 32 → 2.7s(넓힘 15) +# 타일 512 여유 32 → 4.5s(넓힘 15) +# 여유 24 가 0.2s 빠르지만 넓힘이 52회로 지형에 예민해 32 로 뒀다. 한 단 아래 등고선은 +# 주곡선 5m·셀 1m 에서 대개 수십 칸 안이다. 넓힘 횟수는 계측 줄에 찍힌다. DESCENT_WINDOW_MARGIN_CELLS = 32 -DESCENT_TILE_CELLS = 256 +DESCENT_TILE_CELLS = 128 def _distance_to_lower( @@ -149,11 +148,11 @@ def _distance_to_lower( lower: np.ndarray, margin: int | None = None, tile: int | None = None, -) -> tuple[np.ndarray, np.ndarray, np.ndarray, bool]: +) -> tuple[np.ndarray, np.ndarray, np.ndarray, int]: """`members` 셀에서 한 단 낮은 등고 라인까지의 거리와 목표 셀(**전체 격자 좌표**). - 네 번째 값은 전체 격자로 되돌아갔는지(폴백) 여부다. 반환 순서는 `members` 의 - 행우선 순서 — 호출부가 `distance[members] = ...` 로 그대로 넣는다. + 네 번째 값은 창을 넓혀 다시 잰 타일 수다(0 = 전부 첫 창에서 끝남). 반환 순서는 + `members` 의 행우선 순서 — 호출부가 `distance[members] = ...` 로 그대로 넣는다. """ margin = DESCENT_WINDOW_MARGIN_CELLS if margin is None else margin tile = DESCENT_TILE_CELLS if tile is None else tile @@ -161,6 +160,7 @@ def _distance_to_lower( out_distance = np.zeros((rows, cols), dtype=np.float64) out_row = np.zeros((rows, cols), dtype=np.int32) out_col = np.zeros((rows, cols), dtype=np.int32) + widened = 0 for row_start in range(0, rows, tile): row_stop = min(rows, row_start + tile) @@ -169,32 +169,49 @@ def _distance_to_lower( tile_members = members[row_start:row_stop, col_start:col_stop] if not tile_members.any(): continue - win_row0 = max(0, row_start - margin) - win_row1 = min(rows, row_stop + margin) - win_col0 = max(0, col_start - margin) - win_col1 = min(cols, col_stop + margin) - window_lower = lower[win_row0:win_row1, win_col0:win_col1] - if not window_lower.any(): - return _full_distance_to_lower(members, lower) - step, (step_row, step_col) = distance_transform_edt(~window_lower, return_indices=True) - in_window = np.zeros(window_lower.shape, dtype=bool) - in_window[ - row_start - win_row0 : row_stop - win_row0, - col_start - win_col0 : col_stop - win_col0, - ] = tile_members - hit_rows, hit_cols = np.nonzero(in_window) - values = step[hit_rows, hit_cols] - # 타일 둘레로 `margin` 을 뒀으므로, 거리가 그보다 짧으면 창 밖에 더 가까운 - # 것은 있을 수 없다. 닿으면 이 단계는 통째로 전체 격자로 다시 잰다. - if values.size and float(values.max()) >= margin: - return _full_distance_to_lower(members, lower) + # 첫 창에서 안 닿으면 **그 타일만** 창을 넓혀 다시 잰다 — 단계 전체를 격자 + # 전체로 되돌리면 이득이 사라진다(실측 폴백 25/91). + reach = margin + attempt = 0 + while True: + win_row0 = max(0, row_start - reach) + win_row1 = min(rows, row_stop + reach) + win_col0 = max(0, col_start - reach) + win_col1 = min(cols, col_stop + reach) + whole = win_row0 == 0 and win_col0 == 0 and win_row1 == rows and win_col1 == cols + window_lower = lower[win_row0:win_row1, win_col0:win_col1] + if not window_lower.any(): + if whole: + return _full_distance_to_lower(members, lower) + reach *= 4 + attempt += 1 + widened += 1 + continue + step, (step_row, step_col) = distance_transform_edt( + ~window_lower, return_indices=True + ) + in_window = np.zeros(window_lower.shape, dtype=bool) + in_window[ + row_start - win_row0 : row_stop - win_row0, + col_start - win_col0 : col_stop - win_col0, + ] = tile_members + hit_rows, hit_cols = np.nonzero(in_window) + values = step[hit_rows, hit_cols] + # 타일 둘레로 `reach` 를 뒀으므로, 거리가 그보다 짧으면 창 밖에 더 + # 가까운 것은 있을 수 없다. 닿으면 창을 넓혀 다시 잰다. + if not whole and values.size and float(values.max()) >= reach: + reach *= 4 + attempt += 1 + widened += 1 + continue + break global_rows = hit_rows + win_row0 global_cols = hit_cols + win_col0 out_distance[global_rows, global_cols] = values out_row[global_rows, global_cols] = step_row[hit_rows, hit_cols] + win_row0 out_col[global_rows, global_cols] = step_col[hit_rows, hit_cols] + win_col0 - return out_distance[members], out_row[members], out_col[members], False + return out_distance[members], out_row[members], out_col[members], widened def _full_distance_to_lower( @@ -244,7 +261,8 @@ def build_contour_descent( target_row = np.zeros((rows, cols), dtype=np.int32) target_col = np.zeros((rows, cols), dtype=np.int32) band_rank = np.full((rows, cols), -1, dtype=np.int32) - window_fallbacks = 0 + window_widened = 0 + edt_steps = 0 for rank, elevation in enumerate(levels[:-1]): members = inside & (band_elevation == elevation) if not members.any(): @@ -252,8 +270,9 @@ def build_contour_descent( lower = on_contour & (burned < elevation) if not lower.any(): continue - step_distance, step_row, step_col, fell_back = _distance_to_lower(members, lower) - window_fallbacks += int(fell_back) + step_distance, step_row, step_col, widened = _distance_to_lower(members, lower) + window_widened += widened + edt_steps += 1 distance[members] = step_distance.astype(np.float32) target_row[members] = step_row target_col[members] = step_col @@ -263,7 +282,7 @@ def build_contour_descent( # 그래야 그 자리가 도로·세류선이면 적색으로 잡히고, 아니면 물이 고이는 지점으로 보인다. marks.append( ( - f"밴드별 하강거리(EDT {len(levels) - 1}회 · 전체격자 폴백 {window_fallbacks}회)", + f"밴드별 하강거리(EDT {edt_steps}단 · 창 넓힘 {window_widened}회)", time.perf_counter(), ) )