183 lines
7.1 KiB
Python
183 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from scipy import ndimage
|
|
from scipy.interpolate import RectBivariateSpline
|
|
import trimesh
|
|
|
|
# config 파라미터 로드
|
|
import config
|
|
from utils.terrain_model_converter import (
|
|
TerrainContext,
|
|
_atomic_npz,
|
|
_write_glb,
|
|
_scene_vertices,
|
|
_height_colors,
|
|
_grid_vertices,
|
|
_grid_faces,
|
|
_clip_and_compact_mesh,
|
|
_artifact_size
|
|
)
|
|
|
|
SMOOTHING_ALGORITHM_VERSION = 2
|
|
|
|
def compute_smoothing_signature(config_dict: dict[str, Any]) -> str:
|
|
"""스무딩 관련 파라미터의 해시 서명을 계산합니다."""
|
|
smooth_params = {k: v for k, v in config_dict.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:
|
|
"""NaN 및 무효 영역(mask == False) 오염을 방지하기 위한 정규화 가우시안 필터"""
|
|
if sigma <= 0:
|
|
return grid.copy()
|
|
|
|
# 무효 영역은 0으로 채우고 가중치 배열 생성
|
|
V = grid.copy()
|
|
V[~mask] = 0.0
|
|
W = np.zeros_like(grid, dtype=float)
|
|
W[mask] = 1.0
|
|
|
|
# 각각 가우시안 블러 수행
|
|
V_blur = ndimage.gaussian_filter(V, sigma=sigma, mode="constant", cval=0.0)
|
|
W_blur = ndimage.gaussian_filter(W, sigma=sigma, mode="constant", cval=0.0)
|
|
|
|
# 0 나누기 방지용 마스크
|
|
valid_denom = W_blur > 1e-10
|
|
|
|
result = grid.copy()
|
|
result[valid_denom] = V_blur[valid_denom] / W_blur[valid_denom]
|
|
return result
|
|
|
|
def smooth_dtm(
|
|
context: TerrainContext,
|
|
output_dir: Path,
|
|
stem: str,
|
|
original_model_path: Path,
|
|
) -> dict[str, Any]:
|
|
"""DTM 격자 지형 데이터를 로드하여 C2 바이큐빅 보간을 적용하고 스무딩 모델을 생성합니다."""
|
|
# 원본 npz 데이터 로드
|
|
data = np.load(original_model_path)
|
|
x_orig = data["x"]
|
|
y_orig = data["y"]
|
|
z_orig = data["z"]
|
|
valid_orig = 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
|
|
|
|
# 가우시안 마스크 가중 평활화 진행 (NaN/경계 오염 제거)
|
|
z_pre = _masked_gaussian_filter(z_orig, valid_orig, sigma_pixels)
|
|
|
|
# B-Spline C2 평가
|
|
spline = RectBivariateSpline(
|
|
y_orig, x_orig, z_pre,
|
|
kx=3, ky=3,
|
|
s=float(context.config.get("smoothing_dtm_spline_smooth", 0.0))
|
|
)
|
|
|
|
# 지정 해상도(예: 0.5m)로 C2 그리드 재작성
|
|
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)
|
|
|
|
# footprint 영역 검증
|
|
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"
|
|
|
|
# 압축 모델 저장 및 GLB 출력
|
|
_atomic_npz(model_path, x=x_coords, y=y_coords, z=z_smooth, valid_mask=valid_grid,
|
|
resolution=np.array([preview_res], np.float32))
|
|
_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 smooth_tin(
|
|
context: TerrainContext,
|
|
output_dir: Path,
|
|
stem: str,
|
|
original_model_path: Path,
|
|
) -> dict[str, Any]:
|
|
"""TIN 삼각망 모델을 로드하여 Taubin 저수축 스무딩 필터를 적용하고 저장합니다."""
|
|
# 원본 npz 데이터 로드
|
|
data = np.load(original_model_path)
|
|
vertices = data["vertices"]
|
|
faces = data["faces"]
|
|
|
|
if len(vertices) < 3 or not len(faces):
|
|
raise ValueError("TIN 스무딩을 수행할 삼각망 메쉬 데이터가 올바르지 않습니다.")
|
|
|
|
# Taubin 스무딩 강도 노브 파라미터 로드
|
|
iterations = int(context.config.get("smoothing_tin_taubin_iterations", 10))
|
|
lamb = float(context.config.get("smoothing_tin_taubin_lambda", 0.5))
|
|
mu = float(context.config.get("smoothing_tin_taubin_mu", -0.53))
|
|
|
|
mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
|
|
|
|
if iterations > 0:
|
|
# Taubin 스무딩 필터 적용
|
|
trimesh.smoothing.filter_taubin(mesh, lamb=lamb, nu=mu, iterations=iterations)
|
|
|
|
vertices_smooth = np.asarray(mesh.vertices, dtype=np.float32)
|
|
# 지표면 스무딩은 평면 위치를 변형하는 메시 리토폴로지가 아니다.
|
|
# DTM과 동일하게 XY 및 삼각형 연결은 원본을 유지하고 표고 Z만 평활화한다.
|
|
vertices_smooth[:, :2] = np.asarray(vertices[:, :2], dtype=np.float32)
|
|
faces_smooth = np.asarray(mesh.faces, dtype=np.uint32)
|
|
|
|
# 저장 파일 경로 정의
|
|
model_path = output_dir / f"{stem}_smooth.npz"
|
|
preview_path = output_dir / f"{stem}_smooth_preview.glb"
|
|
|
|
# 압축 모델 저장 및 GLB 출력
|
|
_atomic_npz(model_path, vertices=vertices_smooth, faces=faces_smooth)
|
|
_write_glb(preview_path, vertices_smooth, faces_smooth, context.bounds)
|
|
|
|
return {
|
|
"model_file": model_path.name,
|
|
"preview_file": preview_path.name,
|
|
"preview_media_type": "model/gltf-binary",
|
|
"vertex_count": int(len(vertices_smooth)),
|
|
"face_count": int(len(faces_smooth)),
|
|
"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)
|
|
elif method == "tin":
|
|
return smooth_tin(context, output_dir, stem, original_model_path)
|
|
else:
|
|
raise ValueError(f"스무딩이 불가능한 지형 표현 방식입니다: {method}")
|