Files
Aislo/B04_PreProcess/B04_PreProcess_Engine_Smooth.py
T
eomsangdonandClaude Opus 5.5 11c75f022a refactor(767-1): 안 쓰는 지면 필터 · 서피스 지움 — 필터 넷(CSF · 격자 최저 · PMF · RANSAC) · 표현 셋(TIN + Taubin 평활 · NURBS · meshfree) · 수치지형도 비교 방식 다섯 · 안 부르는 길 정리
- 필터 = 업체 분류 · 점진 TIN · 둘의 다듬기 판만 · 분석 허용 목록 = Ground 등록부(available_filters) 한 벌
- 표현 = DTM · implicit 만 · 평활 = DTM 만 · config 기본 목록 · PRECOMPUTE · 설정 사전 맞춤(설정 지문 바뀜 — 768 에서 다시 지음)
- 등고선 = 격자 · 굳힌 격자만(삼각망 굽기 · 외곽 맞춤 · implicit RBF 폴백 걷음) · 표본기 · 경로 찾기 tin · nurbs · meshfree 갈래 걷음
- 수치지형도 = 라플라스만(TIN 둘 · TPS · ANUDEM형 · 다중해상도 걷음) · B04 단추 하나
- 안 부르는 길 — B04 JSON 점군 GET · ground-stats GET + TS · B07 옛 계획평면도(라이다) 짓기 · lidar_source · LIDAR_ID · plan_lidar 종류 이름 · SOURCE_FILTER_LABELS · MODEL_METHODS(py) · MESH_* 설정
- 옛 manifest 의 지운 갈래 줄은 DB 로 되살리지 않음 · 옛 csf 행은 미리보기 · 등고선 그대로 열림 · 지운 이름 요청 = 400
- 낡은 글 — Ground · Pipeline 머리 · WF1 「없으면 csf」 · B07 음영 기본 「csf」 → 빈 값 · 등고선 폴백 「csf」 → 404 · B04 3D 보기 처음값 · B05 Schema · B06 Prebuild · 씨닝 설명
- 시험 지움 2 — test_b04_contour_raster · test_b04_contour_equivalence(지운 삼각망 굽기 · 없는 프로젝트를 봐 건너뛰던 것)
- 시험 덜어냄 — test_b04_ground_filters CSF · 격자 최저 네 경우(지운 필터)
- 시험 고침 — 허용 목록 · 형 검사에 걸린 csf · tin · grid_min_z · meshfree · tin_sheet 견본을 ptd · implicit · laplace 로(54_22 · 238_1 · 443 · 342_2 · 346_3 · 351_1 · 442_2 · 271 + helper · 344_1 · 133_1 · 343_2 · 637 · 754_1 · 763_1 깨진 tin → 깨진 implicit · 763_3 · b05 둘 · b06 · sheet_surface TIN(도엽선) 경우 지움) · 이름만 바꿈(273_2 · 343_1 · 343_3 · 624_1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:35:12 +09:00

113 lines
4.3 KiB
Python

"""B04 지표면 스무딩 엔진 (DTM 가우시안+B-Spline).
원본 모델(npz)을 로드해 표고 Z만 평활화한 스무딩 모델과 프리뷰(GLB)를 만든다.
XY 격자는 원본을 유지한다.
"""
import hashlib
import json
from pathlib import Path
from typing import Any
import numpy as np
from scipy import ndimage
from scipy.interpolate import RectBivariateSpline
from B04_PreProcess.B04_PreProcess_Engine_ModelContext import (
TerrainContext,
artifact_size,
atomic_npz,
clip_and_compact_mesh,
grid_faces,
grid_vertices,
write_glb,
)
SMOOTHING_ALGORITHM_VERSION = 2
def compute_smoothing_signature(config: dict[str, Any]) -> str:
"""스무딩 관련 파라미터의 해시 서명을 계산한다."""
smooth_params = {k: v for k, v in config.items() if k.startswith("smoothing_")}
smooth_params["algorithm_version"] = SMOOTHING_ALGORITHM_VERSION
encoded = json.dumps(smooth_params, sort_keys=True, default=list).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()[:16]
def _masked_gaussian_filter(grid: np.ndarray, mask: np.ndarray, sigma: float) -> np.ndarray:
"""무효 영역(mask==False) 오염을 방지하는 정규화 가우시안 필터."""
if sigma <= 0:
return grid.copy()
values = grid.copy()
values[~mask] = 0.0
weights = np.zeros_like(grid, dtype=float)
weights[mask] = 1.0
value_blur = ndimage.gaussian_filter(values, sigma=sigma, mode="constant", cval=0.0)
weight_blur = ndimage.gaussian_filter(weights, sigma=sigma, mode="constant", cval=0.0)
valid_denom = weight_blur > 1e-10
result = grid.copy()
result[valid_denom] = value_blur[valid_denom] / weight_blur[valid_denom]
return result
def smooth_dtm(
context: TerrainContext, output_dir: Path, stem: str, original_model_path: Path
) -> dict[str, Any]:
"""DTM 격자에 가우시안+C2 바이큐빅 보간을 적용해 스무딩 모델을 만든다."""
data = np.load(original_model_path)
x_orig, y_orig, z_orig, valid_orig = data["x"], data["y"], data["z"], data["valid_mask"]
resolution = float(context.config["dtm_grid_resolution_meters"])
sigma_meters = float(context.config.get("smoothing_dtm_sigma_meters", 0.5))
sigma_pixels = sigma_meters / resolution if resolution > 0 else 0.0
z_pre = _masked_gaussian_filter(z_orig, valid_orig, sigma_pixels)
spline = RectBivariateSpline(
y_orig,
x_orig,
z_pre,
kx=3,
ky=3,
s=float(context.config.get("smoothing_dtm_spline_smooth", 0.0)),
)
preview_res = float(context.config.get("smoothing_dtm_preview_resolution_meters", 0.5))
x_coords, y_coords, _ = context.preview_grid(preview_res)
z_smooth = np.asarray(spline(y_coords, x_coords), dtype=np.float32)
valid_grid = context.contains_xy(*np.meshgrid(x_coords, y_coords)).reshape(
len(y_coords), len(x_coords)
)
vertices = grid_vertices(x_coords, y_coords, z_smooth)
faces = grid_faces(len(y_coords), len(x_coords))
preview_valid = context.contains_xy(vertices[:, 0], vertices[:, 1])
vertices_compact, faces_compact = clip_and_compact_mesh(vertices, faces, preview_valid)
model_path = output_dir / f"{stem}_smooth.npz"
preview_path = output_dir / f"{stem}_smooth_preview.glb"
atomic_npz(
model_path,
x=x_coords,
y=y_coords,
z=z_smooth,
valid_mask=valid_grid,
resolution=np.array([x_coords[1] - x_coords[0]], np.float32), # 넓힌 실제 칸(633)
)
write_glb(preview_path, vertices_compact, faces_compact, context.bounds)
return {
"model_file": model_path.name,
"preview_file": preview_path.name,
"preview_media_type": "model/gltf-binary",
"vertex_count": int(len(vertices_compact)),
"face_count": int(len(faces_compact)),
"artifact_bytes": artifact_size(model_path, preview_path),
"smoothing_signature": compute_smoothing_signature(context.config),
}
def run_smoothing(
method: str, context: TerrainContext, output_dir: Path, stem: str, original_model_path: Path
) -> dict[str, Any]:
"""방식에 따라 적절한 스무딩 엔진을 수행한다."""
if method == "dtm":
return smooth_dtm(context, output_dir, stem, original_model_path)
raise ValueError(f"스무딩이 불가능한 지형 표현 방식입니다: {method}")