feat(B04): 도엽 서피스에 DTM 스무딩을 적용한다
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -524,8 +524,10 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user