125 lines
5.4 KiB
Python
125 lines
5.4 KiB
Python
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 테스트 통과")
|