diff --git a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py index a06831ab..e4b4a40f 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py +++ b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py @@ -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]), diff --git a/config/config_system.py b/config/config_system.py index 8272c5ac..56bb9ba2 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -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")