diff --git a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py index d8fda608..caae8c1c 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py +++ b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py @@ -27,6 +27,8 @@ from pyproj import Transformer from B04_PreProcess.B04_PreProcess_Engine_ModelContext import ( atomic_npz, clip_and_compact_mesh, + evaluate_nurbs_spline, + fit_nurbs_spline, grid_faces, grid_vertices, write_glb, @@ -41,6 +43,9 @@ from config.config_system import ( SHEET_SURFACE_MARGIN_M, SHEET_SURFACE_METHODS, SURFACE_MAX_PREVIEW_VERTICES, + SURFACE_NURBS_CONTROL_POINTS_PER_AXIS, + SURFACE_NURBS_DEGREE, + SURFACE_NURBS_PATCH_SIZE_M, ) logger = logging.getLogger(__name__) @@ -98,13 +103,45 @@ def _load_features_metric( def _preview_mesh( x: np.ndarray, y: np.ndarray, z: np.ndarray, valid: np.ndarray ) -> tuple[np.ndarray, np.ndarray]: - """프리뷰용 정점·면 — 정점 수가 상한을 넘으면 격자를 성기게 딴다.""" + """프리뷰용 정점·면 — 격자를 그대로 잇지 않고 **NURBS(B-spline) 곡면**으로 만든다. + + 격자 삼각형을 그대로 쓰면 1m 셀 경계가 계단처럼 보인다. LAS 파이프라인이 쓰는 + 것과 같은 곡면 적합(`fit_nurbs_spline`·`evaluate_nurbs_spline`)을 걸어 매끈한 + 면을 얻는다(2026-08-30 사용자 지시). **표고 정본(npz)은 격자 그대로**이고 여기서 + 바꾸는 것은 화면에 보이는 메시뿐이다 — 종·횡단은 격자를 샘플링한다. + + 적합에 쓰는 제어 간격은 LAS NURBS와 같은 config 값(패치 크기 ÷ 축당 제어점 수)이며, + 제어 격자가 격자보다 성기지 않도록 최소 1셀로 묶는다. + """ stride = 1 while (len(x) // stride + 1) * (len(y) // stride + 1) > SURFACE_MAX_PREVIEW_VERTICES: stride += 1 px, py = x[::stride], y[::stride] - pz, pv = z[::stride, ::stride], valid[::stride, ::stride] - vertices = grid_vertices(px, py, pz.astype(np.float64)) + pv = valid[::stride, ::stride] + + cell_m = float(x[1] - x[0]) if len(x) > 1 else SHEET_SURFACE_GRID_M + control_m = max( + SURFACE_NURBS_PATCH_SIZE_M / max(SURFACE_NURBS_CONTROL_POINTS_PER_AXIS - 1, 1), cell_m + ) + control_step = max(1, int(round(control_m / max(cell_m, 1e-6)))) + # 곡면 적합은 결측을 못 받는다 — 무효 셀은 가장 가까운 유효 표고로 메우고, + # 메시를 자를 때 원래 유효 마스크로 다시 도려낸다. + filled = z.astype(np.float64) + if not valid.all(): + from scipy.ndimage import distance_transform_edt + + _, (rows, cols) = distance_transform_edt(~valid, return_indices=True) + filled = filled[rows, cols] + cx, cy = x[::control_step], y[::control_step] + cz = filled[::control_step, ::control_step] + try: + spline, z_range = fit_nurbs_spline(cx, cy, cz, SURFACE_NURBS_DEGREE) + pz = evaluate_nurbs_spline(spline, py, px, z_range, "도엽 서피스") + except Exception as exc: # noqa: BLE001 — 적합 실패 시 격자 메시로 되돌린다 + logger.warning("도엽 서피스: NURBS 곡면 적합 실패(%s) — 격자 메시를 씁니다.", exc) + pz = filled[::stride, ::stride] + + vertices = grid_vertices(px, py, np.asarray(pz, dtype=np.float64)) faces = grid_faces(len(py), len(px)) return clip_and_compact_mesh(vertices, faces, pv.reshape(-1))