feat(B04): 사이 등고선을 2D 가상 등고라인으로 뽑아 반복 이분한다
앞서는 등거리 '점'을 5셀 간격으로 흩뿌려 점 사이가 벌어진 곳에 평탄 삼각형이 남았고 한 단계(+2.5m)만 넣어 그 안에서 계단이 남았다. 사용자 지시(2D에서 사이 등고선을 먼저 만들고 3D화)대로 등거리선을 연속 셀 선으로 뽑아 새 레벨로 등록하고, 그 선들 사이를 다시 이분한다 (SHEET_SURFACE_MIDLINE_ROUNDS=2 → 5m 주곡선이 2.5m·1.25m 가상 등고로). 정점 간격도 5셀에서 2m로 좁혔다. 거리장은 라운드 간 재사용한다. 실측(c1bb453f): 평탄 셀 3.96%→2.48%(최초 9.35%), 경사 2% 미만 셀 27163→17145, 노선 |Δz| 2.56m 유지. 생성 16.4s→36.5s — 라운드 수는 config로 조절 가능. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ from config.config_system import (
|
||||
SHEET_SURFACE_GRID_M,
|
||||
SHEET_SURFACE_MARGIN_M,
|
||||
SHEET_SURFACE_MIDLINE_MAX_M,
|
||||
SHEET_SURFACE_MIDLINE_ROUNDS,
|
||||
SURFACE_MAX_PREVIEW_VERTICES,
|
||||
)
|
||||
|
||||
@@ -176,13 +177,17 @@ def _stream_breakline_vertices(
|
||||
def _densify_between_contours(
|
||||
spec: Any, burned: np.ndarray, present: list[float], cloud: Any
|
||||
) -> Any:
|
||||
"""인접 표고 등고선 쌍의 등거리 중간선을 중간 표고 정점으로 추가한다.
|
||||
"""등고선 사이에 2D 가상 등고라인을 반복 이분으로 만들어 정점으로 추가한다.
|
||||
|
||||
등고선 정점만으로 Delaunay TIN을 치면 같은 표고 정점 3개짜리 평탄 삼각형이
|
||||
굴곡부·능선에서 계단(terrace)을 만든다. 두 등고선의 "사이점"(등거리선)에
|
||||
평균 표고 정점을 넣어 삼각형이 반드시 서로 다른 표고를 잇게 한다
|
||||
(2026-08-30 사용자 지적). 최고 등고선 안쪽 봉우리·같은 표고 사이 골짜기
|
||||
바닥은 짝이 없어 평탄으로 남는다 — 원천 데이터 한계.
|
||||
등고선 정점만의 Delaunay TIN은 같은 표고 정점 3개짜리 평탄 삼각형이 굴곡부에
|
||||
계단(terrace)을 만든다. 인접 표고 라인 쌍의 등거리선(사이 등고선)을 **연속 셀
|
||||
선**으로 뽑아 새 레벨로 등록하고, 그 선들 사이를 다시 이분하는 식으로
|
||||
`SHEET_SURFACE_MIDLINE_ROUNDS`회 반복한다 — 5m 주곡선이면 2.5m, 1.25m 가상
|
||||
등고가 생겨 TIN이 어디서든 서로 다른 표고를 잇는다(2026-08-30 사용자 지시:
|
||||
2D에서 사이 등고선을 먼저 만들고 3D화).
|
||||
|
||||
등거리 조건은 멀리 있는 다른 표고 쌍에도 우연히 성립하므로(예: 525~530
|
||||
골짜기에 555/560 중간점), "그 지점의 최근접 라인이 바로 그 쌍"일 때만 인정한다.
|
||||
"""
|
||||
from scipy.ndimage import distance_transform_edt
|
||||
|
||||
@@ -191,43 +196,71 @@ def _densify_between_contours(
|
||||
if len(present) < 2:
|
||||
return cloud
|
||||
|
||||
def _distance(level: float) -> np.ndarray:
|
||||
return distance_transform_edt(burned != level, sampling=spec.cell_m).astype(np.float32)
|
||||
|
||||
# 1차: 셀별 최근접 등고선 레벨 — 등거리 조건은 멀리 있는 다른 표고 쌍에도 우연히
|
||||
# 성립하므로(예: 525~530 골짜기에 555/560 중간점이 박힘), "그 지점의 최근접
|
||||
# 등고선이 바로 그 쌍"일 때만 사이점으로 인정한다.
|
||||
best_distance = np.full(burned.shape, np.inf, dtype=np.float32)
|
||||
best_index = np.full(burned.shape, -1, dtype=np.int16)
|
||||
for index, level in enumerate(present):
|
||||
distance = _distance(level)
|
||||
take = distance < best_distance
|
||||
best_distance[take] = distance[take]
|
||||
best_index[take] = index
|
||||
# 라운드마다 거리장을 다시 구하되, 직전 라운드 것을 재사용한다 — 새 라인 사이의
|
||||
# 이분은 (직전 레벨 거리장, 새 중간선 거리장)만 있으면 되므로 전량 재계산이 필요없다.
|
||||
levels = list(present)
|
||||
masks = [burned == level for level in levels]
|
||||
distances = [
|
||||
distance_transform_edt(~mask, sampling=spec.cell_m).astype(np.float32) for mask in masks
|
||||
]
|
||||
virtual: dict[float, np.ndarray] = {}
|
||||
for _ in range(max(SHEET_SURFACE_MIDLINE_ROUNDS, 0)):
|
||||
if len(levels) < 2:
|
||||
break
|
||||
best_distance = np.full(burned.shape, np.inf, dtype=np.float32)
|
||||
best_index = np.full(burned.shape, -1, dtype=np.int16)
|
||||
for index, distance in enumerate(distances):
|
||||
take = distance < best_distance
|
||||
best_distance[take] = distance[take]
|
||||
best_index[take] = index
|
||||
next_levels: list[float] = []
|
||||
next_masks: list[np.ndarray] = []
|
||||
next_distances: list[np.ndarray] = []
|
||||
added_any = False
|
||||
for index in range(len(levels)):
|
||||
next_levels.append(levels[index])
|
||||
next_masks.append(masks[index])
|
||||
next_distances.append(distances[index])
|
||||
if index == 0:
|
||||
continue
|
||||
lower, upper = distances[index - 1], distances[index]
|
||||
near = (lower < SHEET_SURFACE_MIDLINE_MAX_M) & (upper < SHEET_SURFACE_MIDLINE_MAX_M)
|
||||
pair_is_nearest = (best_index == index - 1) | (best_index == index)
|
||||
midline = near & pair_is_nearest & (np.abs(lower - upper) <= spec.cell_m)
|
||||
midline &= ~masks[index - 1] & ~masks[index]
|
||||
if not midline.any():
|
||||
continue
|
||||
mid_level = (levels[index - 1] + levels[index]) / 2.0
|
||||
virtual[mid_level] = midline
|
||||
# 새 라인은 마지막(=자기 레벨) 자리 앞에 끼워 정렬을 유지한다.
|
||||
next_levels.insert(-1, mid_level)
|
||||
next_masks.insert(-1, midline)
|
||||
next_distances.insert(
|
||||
-1, distance_transform_edt(~midline, sampling=spec.cell_m).astype(np.float32)
|
||||
)
|
||||
added_any = True
|
||||
levels, masks, distances = next_levels, next_masks, next_distances
|
||||
if not added_any:
|
||||
break
|
||||
|
||||
if not virtual:
|
||||
return cloud
|
||||
xs = spec.cell_centers_x()
|
||||
ys = spec.cell_centers_y()
|
||||
extra_xy: list[np.ndarray] = []
|
||||
extra_z: list[np.ndarray] = []
|
||||
previous_distance = _distance(present[0])
|
||||
for index in range(1, len(present)):
|
||||
distance = _distance(present[index])
|
||||
near = (previous_distance < SHEET_SURFACE_MIDLINE_MAX_M) & (
|
||||
distance < SHEET_SURFACE_MIDLINE_MAX_M
|
||||
)
|
||||
pair_is_nearest = (best_index == index - 1) | (best_index == index)
|
||||
midline = near & pair_is_nearest & (np.abs(previous_distance - distance) <= spec.cell_m)
|
||||
rows, cols = np.nonzero(midline)
|
||||
if len(rows):
|
||||
# 등고선 정점 재샘플 밀도에 맞춰 성기게 담는다 — TIN 비용 절제.
|
||||
rows, cols = rows[::5], cols[::5]
|
||||
extra_xy.append(np.column_stack([xs[cols], ys[rows]]))
|
||||
extra_z.append(np.full(len(rows), (present[index - 1] + present[index]) / 2.0))
|
||||
previous_distance = distance
|
||||
if not extra_xy:
|
||||
return cloud
|
||||
for level, mask in virtual.items():
|
||||
rows, cols = np.nonzero(mask)
|
||||
rows, cols = rows[::2], cols[::2] # 1m 셀 → 2m 간격 정점 (TIN 비용 절제)
|
||||
extra_xy.append(np.column_stack([xs[cols], ys[rows]]))
|
||||
extra_z.append(np.full(len(rows), level))
|
||||
added = int(sum(len(a) for a in extra_z))
|
||||
logger.info("도엽 서피스: 중간 보간선 정점 %d개 추가 (등고 %d단)", added, len(present))
|
||||
logger.info(
|
||||
"도엽 서피스: 가상 등고라인 %d단, 정점 %d개 추가 (원 등고 %d단)",
|
||||
len(virtual),
|
||||
added,
|
||||
len(present),
|
||||
)
|
||||
return ContourCloud(
|
||||
xy=np.vstack([cloud.xy, *extra_xy]),
|
||||
z=np.concatenate([cloud.z, *extra_z]),
|
||||
|
||||
@@ -191,6 +191,9 @@ SHEET_SURFACE_GRID_M = float(os.getenv("SHEET_SURFACE_GRID_M", "1.0"))
|
||||
# 중간 보간선(인접 등고선 쌍의 등거리선) 탐색 최대 거리(m). 등고선 간격이 이보다 넓은
|
||||
# 완경사면은 보간선을 넣지 않는다. 5m 주곡선 기준 경사 2.5%까지 커버.
|
||||
SHEET_SURFACE_MIDLINE_MAX_M = float(os.getenv("SHEET_SURFACE_MIDLINE_MAX_M", "200.0"))
|
||||
# 가상 등고라인 반복 이분 횟수. 2회 = 5m 주곡선 → 2.5m → 1.25m 가상 등고 생성
|
||||
# (2026-08-30 사용자 지시 — 2D 사이 등고선을 먼저 만들고 3D화).
|
||||
SHEET_SURFACE_MIDLINE_ROUNDS = int(os.getenv("SHEET_SURFACE_MIDLINE_ROUNDS", "2"))
|
||||
|
||||
# 일반 사용자 WF1 자동 확정 기본값
|
||||
SURFACE_CONFIRM_DEFAULT_FILTER = os.getenv("SURFACE_CONFIRM_DEFAULT_FILTER", "csf")
|
||||
|
||||
Reference in New Issue
Block a user