import sys from pathlib import Path import numpy as np # Add project root to sys.path root_dir = Path(__file__).resolve().parent.parent if str(root_dir) not in sys.path: sys.path.insert(0, str(root_dir)) from utils.terrain_skeleton import ( d8_flow_accumulation_numpy, extract_skeleton_from_grid, _trace_polylines, ) def _v_valley_grid(rows=80, cols=81, res=2.0): """V자 계곡 합성 지형: 중앙 열이 계곡선, 좌우 가장자리가 능선. y 가 작아질수록 낮아져 물이 중앙선을 따라 y=0 쪽으로 모인다.""" x = np.arange(cols, dtype=np.float64) * res y = np.arange(rows, dtype=np.float64) * res xc = x[cols // 2] xx, yy = np.meshgrid(x, y) z = 0.05 * np.abs(xx - xc) + 0.02 * yy valid = np.ones_like(z, dtype=bool) return x, y, z, valid, res, xc def test_d8_accumulation_on_tilted_plane(): """단순 경사면: 모든 물이 최저 셀 방향으로 모여 하류 누적값이 상류보다 크다.""" rows, cols = 20, 20 x = np.arange(cols, dtype=np.float64) z = np.tile(x * 1.0, (rows, 1)) # x 방향으로만 상승 → 물은 -x 방향으로 valid = np.ones_like(z, dtype=bool) acc = d8_flow_accumulation_numpy(z, valid) # 좌측(하류) 열의 평균 누적값이 우측(상류) 열보다 커야 한다. assert acc[:, 0].mean() > acc[:, -1].mean() # 모든 유효 셀은 최소 자기 자신(1) 이상. assert (acc >= 1.0).all() print("[ok] D8 accumulation flows downhill on a tilted plane") def test_d8_accumulation_v_valley_concentrates_center(): """V자 계곡: 중앙 열 누적값이 사면 셀보다 훨씬 커야 한다.""" x, y, z, valid, res, xc = _v_valley_grid() acc = d8_flow_accumulation_numpy(z, valid) center_c = len(x) // 2 hillside_c = len(x) // 4 # 하류(작은 y) 중앙 셀은 사면 중간 셀 대비 큰 유역을 가진다. assert acc[2, center_c] > acc[2, hillside_c] * 5 print("[ok] V-valley concentrates flow accumulation on the center line") def test_extract_skeleton_valley_on_center(): """스켈레톤 추출: 계곡 polyline 은 중앙선 부근, 능선 polyline 도 존재해야 한다.""" x, y, z, valid, res, xc = _v_valley_grid() thresholds = { "valley_acc": 40.0, "main_valley_acc": 800.0, "ridge_acc": 40.0, "main_ridge_acc": 800.0, } skel = extract_skeleton_from_grid( x, y, z, valid, res, thresholds=thresholds, use_whitebox=False ) valley_polys = skel["main_valley"] + skel["minor_valley"] ridge_polys = skel["main_ridge"] + skel["minor_ridge"] assert valley_polys, "계곡 polyline 이 추출되어야 한다" assert ridge_polys, "능선 polyline 이 추출되어야 한다" # 계곡 정점은 중앙선(xc)에서 10m 이내에 있어야 한다 (V자 지형의 유일한 집수선). for item in valley_polys: for px, py, pz in item["polyline"]: assert abs(px - xc) < 10.0, f"계곡 정점 x={px} 가 중앙선 {xc} 에서 벗어남" print("[ok] skeleton valley polylines follow the V-valley center line") def test_main_minor_split_by_threshold(): """주/지 분류: 임계값을 낮추면 주계곡이 생기고, 높이면 전부 지계곡이 된다.""" x, y, z, valid, res, xc = _v_valley_grid() low = extract_skeleton_from_grid( x, y, z, valid, res, use_whitebox=False, thresholds={"valley_acc": 40.0, "main_valley_acc": 200.0, "ridge_acc": 40.0, "main_ridge_acc": 1e9}, ) high = extract_skeleton_from_grid( x, y, z, valid, res, use_whitebox=False, thresholds={"valley_acc": 40.0, "main_valley_acc": 1e9, "ridge_acc": 40.0, "main_ridge_acc": 1e9}, ) assert low["main_valley"], "낮은 주계곡 임계값에서는 주계곡이 분류되어야 한다" assert not high["main_valley"], "임계값이 매우 높으면 주계곡이 없어야 한다" assert high["minor_valley"], "주계곡이 없으면 전부 지계곡으로 분류되어야 한다" print("[ok] main/minor split responds to the accumulation threshold") def test_trace_polylines_simple_line(): """픽셀 추적: 직선 마스크는 한 개의 polyline 으로 이어져야 한다.""" mask = np.zeros((10, 10), dtype=bool) mask[5, 1:9] = True polys = _trace_polylines(mask) assert len(polys) == 1 assert len(polys[0]) == 8 print("[ok] polyline tracing joins a straight pixel line into one path") def test_whitebox_path_or_fallback(): """whitebox 경로가 실패해도 numpy 폴백으로 결과가 나와야 한다 (통합 스모크). whitebox 미설치/실행불가 환경이면 폴백 로그와 함께 동일 결과가 나온다.""" x, y, z, valid, res, xc = _v_valley_grid(rows=40, cols=41) skel = extract_skeleton_from_grid( x, y, z, valid, res, use_whitebox=True, thresholds={"valley_acc": 40.0, "main_valley_acc": 1e9, "ridge_acc": 40.0, "main_ridge_acc": 1e9}, ) assert skel["minor_valley"], "whitebox 또는 폴백으로 계곡이 추출되어야 한다" print("[ok] whitebox path (or numpy fallback) produces a skeleton") if __name__ == "__main__": test_d8_accumulation_on_tilted_plane() test_d8_accumulation_v_valley_concentrates_center() test_extract_skeleton_valley_on_center() test_main_minor_split_by_threshold() test_trace_polylines_simple_line() test_whitebox_path_or_fallback() print("\n모든 terrain_skeleton 테스트 통과")