From 184296f5265bfa49a7b8246f8ddd797162ea5283 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 16:49:23 +0900 Subject: [PATCH] =?UTF-8?q?feat(B04):=20=EB=8F=84=EC=97=BD=20=EC=84=9C?= =?UTF-8?q?=ED=94=BC=EC=8A=A4=EC=97=90=20DTM=20=EC=8A=A4=EB=AC=B4=EB=94=A9?= =?UTF-8?q?=EC=9D=84=20=EC=A0=81=EC=9A=A9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LAS DTM 스무딩과 같은 두 단계를 계수 그대로 옮겨 방식마다 {stem}_smooth.npz·_smooth_preview.glb를 만든다(계수 변경 금지 — 사용자 지시): 정규화 가우시안(SURFACE_SMOOTHING_DTM_SIGMA_M) → C2 바이큐빅 B-spline 재평가 (kx=ky=3, s=SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH). smooth_dtm()은 라이다 발자국(TerrainContext)을 받아 그대로 못 쓴다. - 결측이 하나라도 있으면 스플라인 결과가 통째로 NaN이 된다(TIN·TIN 곡면은 볼록껍질 밖이 결측). 최근접 표고로 메워 적합하고 원래 마스크로 되돌린다. - 재평가 격자는 원본보다 성기게 잡지 않는다 — 이 npz는 스무딩 확정 시 종·횡단이 샘플링하는 표고 정본이라 화면 정점 상한으로 해상도를 깎으면 안 된다. 메시 정점 수는 _preview_mesh가 알아서 줄인다. - 화면: 방식 버튼 줄에 스무딩 드롭다운을 놓고 기본값을 켜 둔다. 실측(c1bb453f, 8종): 격자 767x840 1m 유지, 거칠기 감소(거리비례 0.341→0.217, TIN 0.067→0.051), 표고 변화 3~33mm. build_surface_sampler(smooth=True) 정상. Co-Authored-By: Claude Opus 5 (1M context) --- .../B04_PreProcess_Engine_SheetSurface.py | 73 +++++++++++++++++++ B04_PreProcess/B04_PreProcess_UI_Page.ts | 6 +- B04_PreProcess/B04_PreProcess_UI_Style.css | 18 +++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py index caae8c1c..113b5527 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py +++ b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py @@ -46,6 +46,9 @@ from config.config_system import ( SURFACE_NURBS_CONTROL_POINTS_PER_AXIS, SURFACE_NURBS_DEGREE, SURFACE_NURBS_PATCH_SIZE_M, + SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M, + SURFACE_SMOOTHING_DTM_SIGMA_M, + SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH, ) logger = logging.getLogger(__name__) @@ -146,6 +149,75 @@ def _preview_mesh( return clip_and_compact_mesh(vertices, faces, pv.reshape(-1)) +def _write_smoothed( + models_dir: Path, + stem: str, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + valid: np.ndarray, + bounds: np.ndarray, +) -> None: + """`{stem}_smooth.npz`·`_smooth_preview.glb`를 만든다 — LAS DTM 스무딩과 같은 절차. + + `B04_PreProcess_Engine_Smooth.smooth_dtm()`은 `TerrainContext`(라이다 발자국)를 + 받으므로 그대로 못 쓴다. 그래서 계수는 **config 값을 그대로** 두고 같은 두 단계만 + 옮긴다(2026-08-30 사용자 지시 — 계수 변경 금지): + + ① 무효 영역이 번지지 않는 정규화 가우시안 (`smoothing_dtm_sigma_meters`) + ② C² 바이큐빅 B-spline 재평가 (`kx=ky=3`, `s=smoothing_dtm_spline_smooth`)를 + `smoothing_dtm_preview_resolution_meters` 격자에서 + + 화면 스무딩 토글과 확정 스냅샷이 이 파일을 찾으므로 이름 규칙을 지켜야 한다. + """ + from scipy.interpolate import RectBivariateSpline + + from B04_PreProcess.B04_PreProcess_Engine_Smooth import _masked_gaussian_filter + + cell_m = float(x[1] - x[0]) if len(x) > 1 else SHEET_SURFACE_GRID_M + sigma_pixels = SURFACE_SMOOTHING_DTM_SIGMA_M / cell_m if cell_m > 0 else 0.0 + # 결측이 하나라도 있으면 스플라인 결과가 통째로 NaN이 된다(TIN·TIN 곡면은 볼록껍질 + # 밖이 결측이다). 최근접 표고로 메워 적합하고 아래에서 원래 마스크로 되돌린다. + filled = z.astype(np.float64) + if not valid.all(): + from scipy.ndimage import distance_transform_edt + + _, (near_row, near_col) = distance_transform_edt(~valid, return_indices=True) + filled = filled[near_row, near_col] + z_pre = _masked_gaussian_filter(filled, valid, sigma_pixels) + try: + spline = RectBivariateSpline(y, x, z_pre, kx=3, ky=3, s=SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH) + except Exception as exc: # noqa: BLE001 — 스무딩 실패가 원본 산출을 막으면 안 된다 + logger.warning("도엽 서피스(%s): 스무딩 스플라인 실패(%s) — 건너뜁니다.", stem, exc) + return + + # 재평가 격자는 config의 프리뷰 해상도를 쓰되 원본보다 성기게 잡지 않는다 — + # 이 npz는 화면용이자 **스무딩 확정 시 종·횡단이 샘플링하는 표고 정본**이라, + # 정점 상한(화면 사정)으로 해상도를 깎으면 설계 정밀도가 같이 깎인다. + # 메시 정점 수는 _preview_mesh가 알아서 성기게 딴다. + step = max(SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M, cell_m) + sx = np.arange(x[0], x[-1] + step * 0.5, step) + sy = np.arange(y[0], y[-1] + step * 0.5, step) + sz = np.asarray(spline(sy, sx), dtype=np.float32) + # 원본 유효 마스크를 최근접으로 옮겨 무효 영역을 그대로 지킨다. + col = np.clip(np.searchsorted(x, sx) - 1, 0, len(x) - 1) + row = np.clip(np.searchsorted(y, sy) - 1, 0, len(y) - 1) + svalid = valid[np.ix_(row, col)] + sz[~svalid] = np.nan + + atomic_npz( + models_dir / f"{stem}_smooth.npz", + x=sx, + y=sy, + z=sz, + valid_mask=svalid, + bounds=bounds, + resolution=np.array([step], np.float32), + ) + vertices, faces = _preview_mesh(sx, sy, np.nan_to_num(sz, nan=float(bounds[2, 0])), svalid) + write_glb(models_dir / f"{stem}_smooth_preview.glb", vertices, faces, bounds) + + def _rasterize_contour_levels(spec: Any, features: list[dict[str, Any]]) -> np.ndarray: """등고 라인을 격자에 굽는다 — 셀 = 그 위를 지나는 라인의 표고, 그 외 NaN. @@ -310,6 +382,7 @@ def _write_method_model( ) vertices, faces = _preview_mesh(x_coords, y_coords, z_grid, valid_grid) write_glb(preview_path, vertices, faces, bounds) + _write_smoothed(models_dir, stem, x_coords, y_coords, z_grid, valid_grid, bounds) return { "model_type": "dtm", "source_filter": source_filter, diff --git a/B04_PreProcess/B04_PreProcess_UI_Page.ts b/B04_PreProcess/B04_PreProcess_UI_Page.ts index f6e366ac..b6458a90 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Page.ts +++ b/B04_PreProcess/B04_PreProcess_UI_Page.ts @@ -524,8 +524,10 @@ export async function renderB04Surface(root: HTMLElement): Promise { sheetToolbar.append(button); sheetMethodButtons.set(method.key, button); } - sheetToolbar.append(lidarLabel); - sheetViewer.setSmoothing(false); + // 스무딩 드롭다운과 라이다 토글은 오른쪽 끝에 함께 둔다. + sheetViewer.smoothingField.classList.add("b04-surface__sheet-smoothing"); + sheetToolbar.append(sheetViewer.smoothingField, lidarLabel); + sheetViewer.setSmoothing(true); selectSheetMethod( sheetMethods.some((method) => method.key === sheetMethod) ? sheetMethod diff --git a/B04_PreProcess/B04_PreProcess_UI_Style.css b/B04_PreProcess/B04_PreProcess_UI_Style.css index ee130ad7..306c52cd 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Style.css +++ b/B04_PreProcess/B04_PreProcess_UI_Style.css @@ -837,3 +837,21 @@ .b04-surface__sheet-lidar { margin-left: auto; } + +/* 도엽 서피스 스무딩 드롭다운 — 방식 버튼 줄 오른쪽 끝 */ +.b04-surface__sheet-smoothing { + margin-left: auto; + display: flex; + align-items: center; + gap: var(--spacing-8); + font-size: var(--text-caption); +} + +.b04-surface__sheet-smoothing .b04-surface__select { + width: auto; + min-width: 96px; +} + +.b04-surface__sheet-toolbar .b04-surface__sheet-lidar { + margin-left: 0; +}