From 95a8d125def529530e0bf234ea676d4a3058e99d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 18 Jul 2026 12:25:14 +0900 Subject: [PATCH] 260718_0 --- B03_FileInput/B03_FileInput_Router.py | 19 ++- B04_wf1_Surface/B04_wf1_Surface_Engine.py | 151 +++++++++++++++--- .../B04_wf1_Surface_Engine_Contour.py | 36 +---- .../B04_wf1_Surface_Engine_Pipeline.py | 11 +- B04_wf1_Surface/B04_wf1_Surface_Repository.py | 18 ++- .../B04_wf1_Surface_Router_Contour.py | 28 ++-- .../B04_wf1_Surface_UI_TerrainViewer.ts | 13 ++ config/config_system.py | 17 +- 8 files changed, 210 insertions(+), 83 deletions(-) diff --git a/B03_FileInput/B03_FileInput_Router.py b/B03_FileInput/B03_FileInput_Router.py index 803fc53d..6a69697b 100644 --- a/B03_FileInput/B03_FileInput_Router.py +++ b/B03_FileInput/B03_FileInput_Router.py @@ -196,7 +196,16 @@ async def trigger_wf1_analysis_and_email( ) from B04_wf1_Surface.B04_wf1_Surface_Engine import run_surface_analysis - from B04_wf1_Surface.B04_wf1_Surface_Router import save_surface_analysis_to_db + from B04_wf1_Surface.B04_wf1_Surface_Router import ( + save_surface_analysis_to_db, + write_surface_progress, + ) + + # 업로드 자동 분석도 progress.json에 진행률을 기록한다 (PLAN C-4) + write_surface_progress(project_root, 5, "analyzing", "WF1 분석을 시작합니다.") + + def _on_progress(percent: int, stage: str, message: str) -> None: + write_surface_progress(project_root, percent, stage, message) logger.info("WF1 분석 엔진 시작: las_path=%s", las_path) analysis_result = await asyncio.to_thread( @@ -206,6 +215,7 @@ async def trigger_wf1_analysis_and_email( source_filters=source_filters, methods=methods, force=False, + on_progress=_on_progress, ) logger.info("WF1 분석 엔진 완료: 모델 %d개 생성됨", len(analysis_result.get("models", []))) @@ -229,6 +239,13 @@ async def trigger_wf1_analysis_and_email( await connection.rollback() raise + write_surface_progress( + project_root, + 100, + "awaiting_confirmation", + "WF1 분석이 완료되었습니다. 사용할 모델을 확정하세요.", + ) + logger.info( "SEND_ANALYSIS_COMPLETION_EMAIL=%s, project_info=%s", SEND_ANALYSIS_COMPLETION_EMAIL, diff --git a/B04_wf1_Surface/B04_wf1_Surface_Engine.py b/B04_wf1_Surface/B04_wf1_Surface_Engine.py index f366f189..2b954538 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Engine.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Engine.py @@ -4,24 +4,44 @@ 동기 계산 파이프라인. 라우터에서 asyncio.to_thread로 호출한다. """ +import json +import logging +import time from collections.abc import Callable from pathlib import Path from typing import Any import numpy as np -from B04_wf1_Surface.B04_wf1_Surface_Engine_Ground import build_ground_masks, summarize_masks +from B04_wf1_Surface.B04_wf1_Surface_Engine_Ground import ( + build_ground_masks, + run_ground_filter, + summarize_masks, +) from B04_wf1_Surface.B04_wf1_Surface_Engine_Pipeline import build_all_terrain_models from B04_wf1_Surface.B04_wf1_Surface_Engine_Structurize import structurize_las from common_util.common_util_atomic import atomic_write_npz +from common_util.common_util_json import atomic_write_json from config.config_system import build_surface_model_config +logger = logging.getLogger(__name__) + # 진행 콜백 시그니처: (진행률 0~100, 현재 단계 키, 메시지) ProgressCallback = Callable[[int, str, str], None] GROUND_POINT_SAMPLE_LIMIT = 500_000 GROUND_POINT_CACHE_VERSION = 2 +def _source_identity(las_path: Path) -> dict[str, Any]: + """입력 LAS의 정체성(이름·크기·수정시각)으로 캐시 세대를 식별한다 (PLAN B-2).""" + stat = las_path.stat() + return { + "filename": las_path.name, + "size_bytes": int(stat.st_size), + "mtime": float(stat.st_mtime), + } + + def _relative_to_project(project_root: Path, path: Path) -> str: """프로젝트 루트 기준 posix 상대 경로 문자열.""" return path.relative_to(project_root).as_posix() @@ -32,11 +52,20 @@ def cache_ground_points( filter_key: str, mask: np.ndarray | None = None, ) -> Path: - """필터링된 지면 포인트 미리보기 캐시를 생성하고 경로를 반환한다.""" + """필터링된 지면 포인트 미리보기 캐시를 생성하고 경로를 반환한다. + + mask 미전달 시 영구 저장된 mask_{filter}.npy를 우선 재사용한다 (PLAN A-2). + """ with np.load(structured_path) as structured: xyz = np.asarray(structured["xyz"], dtype=np.float32) + mask_path = structured_path.parent / f"mask_{filter_key}.npy" + if mask is None and mask_path.is_file(): + stored_mask = np.load(mask_path) + if len(stored_mask) == len(xyz): + mask = np.asarray(stored_mask, dtype=bool) if mask is None: mask = build_ground_masks(structured, [filter_key])[filter_key] + np.save(mask_path, np.asarray(mask, dtype=bool)) ground_indexes = np.flatnonzero(mask) ground_points = xyz[ground_indexes] ground_point_count = int(len(ground_points)) @@ -94,15 +123,37 @@ def run_surface_analysis( if on_progress is not None: on_progress(percent, stage, message) + total_started = time.monotonic() stage_root = project_root / "B04_wf1_Surface" processed_dir = stage_root / "processed" models_dir = stage_root / "models" processed_dir.mkdir(parents=True, exist_ok=True) models_dir.mkdir(parents=True, exist_ok=True) - # 1. LAS 구조화 (structured.npz) - _report(10, "structurize", "LAS 구조화 중") - structured_path = structurize_las(las_path, processed_dir) + # 0. 입력 세대 검증: LAS가 바뀌었으면 모든 캐시를 재계산한다 (PLAN B-2) + identity_path = processed_dir / "source_identity.json" + current_identity = _source_identity(las_path) + stored_identity: dict[str, Any] | None = None + if identity_path.is_file(): + try: + stored_identity = json.loads(identity_path.read_text(encoding="utf-8")) + except (OSError, ValueError): + stored_identity = None + rebuild = force or stored_identity != current_identity + + # 1. LAS 구조화 (structured.npz) — 캐시가 유효하면 스킵 (PLAN B-1) + structured_path = processed_dir / "structured.npz" + if rebuild or not structured_path.is_file(): + _report(10, "structurize", "LAS 구조화 중") + step_started = time.monotonic() + structured_path = structurize_las(las_path, processed_dir) + atomic_write_json(identity_path, current_identity) + logger.info( + "B04 LAS 구조화 완료: %s (%.1fs)", las_path.name, time.monotonic() - step_started + ) + else: + _report(10, "structurize", "구조화 캐시 재사용") + logger.info("B04 구조화 캐시 재사용: %s", structured_path.name) with np.load(structured_path) as structured: xyz = structured["xyz"] bounds = structured["bounds"] @@ -120,11 +171,37 @@ def run_surface_analysis( } data = {"xyz": xyz, "bounds": bounds} - # 2. 지면 필터 실행 + # 2. 지면 필터 실행 — mask_{filter}.npy 영구 캐시 우선 재사용 (PLAN A-1) _report(40, "ground_filter", "지면 필터 적용 중") - masks = build_ground_masks(data, source_filters) + masks: dict[str, np.ndarray] = {} + for filter_key in source_filters: + mask_path = processed_dir / f"mask_{filter_key}.npy" + mask: np.ndarray | None = None + if not rebuild and mask_path.is_file(): + stored_mask = np.load(mask_path) + if len(stored_mask) == total_points: + mask = np.asarray(stored_mask, dtype=bool) + logger.info("B04 지면 필터 캐시 재사용: %s", mask_path.name) + if mask is None: + step_started = time.monotonic() + mask = np.asarray(run_ground_filter(filter_key, data), dtype=bool) + np.save(mask_path, mask) + logger.info( + "B04 지면 필터 계산 완료: %s (%.1fs)", + filter_key, + time.monotonic() - step_started, + ) + masks[filter_key] = mask ground_summary = summarize_masks(data, masks) for filter_key, mask in masks.items(): + cache_path = processed_dir / f"ground_points_{filter_key}.npz" + if not rebuild and cache_path.is_file(): + with np.load(cache_path) as cached: + if ( + "cache_version" in cached + and int(cached["cache_version"]) == GROUND_POINT_CACHE_VERSION + ): + continue cache_ground_points(structured_path, filter_key, mask) # 3. 지표면 5종 모델 빌드 @@ -132,18 +209,28 @@ def run_surface_analysis( config = build_surface_model_config() config["source_filters"] = list(source_filters) config["precompute"] = list(methods) - manifest = build_all_terrain_models(data, masks, models_dir, config, force=force) + step_started = time.monotonic() - # 3-2. VWorld 지도 및 국가 GIS 벡터 다운로드 + def _model_progress(percent: int, detail: str) -> None: + _report(70 + int(percent * 0.2), "surface_model", detail) + + manifest = build_all_terrain_models( + data, masks, models_dir, config, force=rebuild, progress=_model_progress + ) + logger.info( + "B04 지표면 모델 빌드 완료: status=%s (%.1fs)", + manifest.get("status"), + time.monotonic() - step_started, + ) + + # 3-2. VWorld 지도 및 국가 GIS 벡터 다운로드 (기존 산출물이 있으면 스킵) _report(90, "download_maps", "VWorld 지도 및 GIS 벡터 데이터 다운로드 중") try: - # project_root 내의 .prj 파일 탐색 (B03_FileInput/input 또는 root 내 존재할 수 있음) - prj_files = ( - list(project_root.glob("**/B03_FileInput/input/*.prj")) - + list(project_root.glob("*.prj")) - + list(project_root.glob("**/*.prj")) + # 입력 LAS와 같은 폴더의 .prj를 우선 사용 (PLAN E-1: 전체 재귀 glob 제거) + prj_candidates = sorted(las_path.parent.glob("*.prj")) or sorted( + project_root.glob("B03_FileInput/**/*.prj") ) - prj_path = prj_files[0] if prj_files else project_root / "result.prj" + prj_path = prj_candidates[0] if prj_candidates else project_root / "result.prj" bounds_dict_for_download = { "x": [float(bounds[0, 0]), float(bounds[0, 1])], @@ -161,7 +248,11 @@ def run_surface_analysis( {"layer": "white", "ext": "png"}, ] for item in layers: + meta_path = processed_dir / f"vworld_{item['layer'].lower()}_meta.json" + if not rebuild and meta_path.is_file(): + continue try: + step_started = time.monotonic() download_vworld_satellite_map( prj_path, bounds_dict_for_download, @@ -169,16 +260,25 @@ def run_surface_analysis( layer_name=item["layer"], ext=item["ext"], ) - except Exception: - pass + logger.info( + "B04 VWorld %s 지도 다운로드 완료 (%.1fs)", + item["layer"], + time.monotonic() - step_started, + ) + except Exception as exc: + logger.warning("B04 VWorld %s 지도 다운로드 실패: %s", item["layer"], exc) - try: - download_all_gis_vectors(prj_path, bounds_dict_for_download, processed_dir) - - except Exception: - pass - except Exception: - pass + if rebuild or not any(processed_dir.glob("*_bounds.geojson")): + try: + step_started = time.monotonic() + download_all_gis_vectors(prj_path, bounds_dict_for_download, processed_dir) + logger.info( + "B04 국가 GIS 벡터 다운로드 완료 (%.1fs)", time.monotonic() - step_started + ) + except Exception as exc: + logger.warning("B04 국가 GIS 벡터 다운로드 실패: %s", exc) + except Exception as exc: + logger.warning("B04 지도·GIS 다운로드 단계 실패: %s", exc) _report(95, "saving", "결과 저장 중") @@ -228,6 +328,9 @@ def run_surface_analysis( } ) + logger.info( + "B04 WF1 분석 완료: 모델 %d개, 총 %.1fs", len(models), time.monotonic() - total_started + ) return { "processed": processed, "ground_summary": ground_summary, diff --git a/B04_wf1_Surface/B04_wf1_Surface_Engine_Contour.py b/B04_wf1_Surface/B04_wf1_Surface_Engine_Contour.py index e38e2cca..2e793353 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Engine_Contour.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Engine_Contour.py @@ -189,9 +189,9 @@ def _tin_face_coverage_mask( if not len(boundary_edges): return np.zeros(xx.shape, dtype=bool) - from shapely import get_parts, linestrings, polygonize - import rasterio.features import affine + import rasterio.features + from shapely import get_parts, linestrings, polygonize boundary_lines = linestrings(vertices[boundary_edges, :2]) polygons = list(get_parts(polygonize(boundary_lines))) @@ -203,16 +203,10 @@ def _tin_face_coverage_mask( dx = float(x_coords[1] - x_coords[0]) if len(x_coords) > 1 else 1.0 dy = float(y_coords[1] - y_coords[0]) if len(y_coords) > 1 else 1.0 - transform = affine.Affine(dx, 0.0, x_coords[0] - dx / 2.0, - 0.0, dy, y_coords[0] - dy / 2.0) + transform = affine.Affine(dx, 0.0, x_coords[0] - dx / 2.0, 0.0, dy, y_coords[0] - dy / 2.0) mask = rasterio.features.rasterize( - polygons, - out_shape=xx.shape, - transform=transform, - fill=0, - default_value=1, - dtype='uint8' + polygons, out_shape=xx.shape, transform=transform, fill=0, default_value=1, dtype="uint8" ) return mask.astype(bool) @@ -225,26 +219,6 @@ def _grid_axes(x_min: float, x_max: float, y_min: float, y_max: float, target_gr return x_coords, y_coords -def adaptive_contour_grid_resolution( - model_npz_path: Path, representation: str, minimum_meters: float = 1.0 -) -> float: - """모델 범위가 넓어도 온디맨드 격자가 10만 셀을 넘지 않게 조정한다.""" - with np.load(model_npz_path) as data: - if representation == "regular_grid": - x_values, y_values = data["x"], data["y"] - elif representation == "triangular_mesh": - x_values, y_values = data["vertices"][:, 0], data["vertices"][:, 1] - elif representation == "bspline_surface": - x_values, y_values = data["control_x"], data["control_y"] - elif representation == "local_rbf_height_field": - x_values, y_values = data["centers_xy"][:, 0], data["centers_xy"][:, 1] - else: - x_values, y_values = data["points"][:, 0], data["points"][:, 1] - width = float(np.ptp(x_values)) - height = float(np.ptp(y_values)) - return max(minimum_meters, float(np.sqrt(width * height / 100_000))) - - def extract_contours( model_npz_path: Path, representation: str, @@ -305,7 +279,7 @@ def extract_contours( control_z, kx=min(degree, len(control_y) - 1), ky=min(degree, len(control_x) - 1), - s=0, + s=float(len(control_x) * len(control_y)) * 0.01, ) x_coords, y_coords = _grid_axes( float(control_x[0]), diff --git a/B04_wf1_Surface/B04_wf1_Surface_Engine_Pipeline.py b/B04_wf1_Surface/B04_wf1_Surface_Engine_Pipeline.py index a9acd880..a331b32d 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Engine_Pipeline.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Engine_Pipeline.py @@ -252,7 +252,13 @@ def _build_all_terrain_models( _write_json_file(manifest_path, manifest) interval = float(config.get("contour_interval_meters", 1.0)) contour_path = output_dir / f"contour_{filter_key}_{method}_{interval}m.json" - if not contour_path.exists(): + needs_contours = not contour_path.exists() + if method in config.get("smoothing_methods", ("dtm", "tin")): + smooth_contour_path = ( + output_dir / f"contour_{filter_key}_{method}_smooth_{interval}m.json" + ) + needs_contours = needs_contours or not smooth_contour_path.exists() + if needs_contours: _cache_contours_safely( output_dir, stem, @@ -281,6 +287,9 @@ def _build_all_terrain_models( method_started = time.monotonic() filter_entry["methods"][method] = {"status": "running", "error": None} _write_json_file(manifest_path, manifest) + # 모델을 새로 만들기 전에 구세대 등고선 캐시를 제거해 모델-등고선 세대 불일치를 막는다. + for stale_contour in output_dir.glob(f"contour_{filter_key}_{method}_*.json"): + stale_contour.unlink(missing_ok=True) try: metadata = BUILDERS[method]( context, diff --git a/B04_wf1_Surface/B04_wf1_Surface_Repository.py b/B04_wf1_Surface/B04_wf1_Surface_Repository.py index 3fcd5e8a..a37f9896 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Repository.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Repository.py @@ -318,6 +318,21 @@ async def update_project_status( ) +async def delete_project_surface_models(connection: aiomysql.Connection, project_id: UUID) -> int: + """프로젝트의 기존 지표면 모델 행을 모두 제거한다. + + 모든 분석 세대가 동일 파일 경로를 재사용하므로 재분석 시 구세대 행을 + 남기면 존재하지 않는 파일을 가리키게 된다. terrain_layers는 FK CASCADE, + routes.surface_model_id는 FK SET NULL로 함께 정리된다. + """ + async with connection.cursor() as cursor: + await cursor.execute( + "DELETE FROM surface_models WHERE project_id = %s", + (str(project_id),), + ) + return int(cursor.rowcount) + + async def save_surface_analysis_to_db( connection: aiomysql.Connection, *, @@ -326,12 +341,13 @@ async def save_surface_analysis_to_db( analysis_result: dict[str, Any], source_filters: list[str], ) -> list[int]: - """WF1 분석 결과를 DB에 저장한다. + """WF1 분석 결과를 DB에 저장한다 (기존 모델 행은 교체). 이 함수는 트랜잭션을 시작하거나 종료하지 않는다. 호출자는 같은 커넥션에서 begin/commit/rollback을 한 번만 수행해야 한다. """ input_file = await get_input_file(connection, project_id, input_file_id) + await delete_project_surface_models(connection, project_id) processed = analysis_result["processed"] processed_cloud_id = await create_processed_point_cloud( connection, diff --git a/B04_wf1_Surface/B04_wf1_Surface_Router_Contour.py b/B04_wf1_Surface/B04_wf1_Surface_Router_Contour.py index 12401eff..82b11bdd 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Router_Contour.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Router_Contour.py @@ -16,12 +16,12 @@ from fastapi.responses import FileResponse, JSONResponse from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B04_wf1_Surface.B04_wf1_Surface_Engine_Contour import ( CONTOUR_EXTRACTOR_VERSION, - adaptive_contour_grid_resolution, extract_contours, ) from common_util.common_util_atomic import atomic_write_bytes from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool +from config.config_system import SURFACE_CONTOUR_GRID_RESOLUTION_M logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B04 Surface Contour"]) @@ -35,9 +35,11 @@ MODEL_REPRESENTATIONS = { } -def _is_contour_cache_current(contour_path: Path) -> bool: - """캐시 JSON 선두의 extractor_version이 현재 추출기 버전과 일치하는지 검사한다.""" +def _is_contour_cache_current(contour_path: Path, model_path: Path) -> bool: + """캐시가 현재 추출기 버전이고 기반 모델 npz보다 최신인지 검사한다.""" try: + if contour_path.stat().st_mtime < model_path.stat().st_mtime: + return False with contour_path.open("rb") as cache_file: head = cache_file.read(256).decode("utf-8", errors="ignore") except OSError: @@ -96,27 +98,19 @@ async def get_surface_model_contour( filter_key = "csf" if smooth and method in ("dtm", "tin"): contour_filename = f"contour_{filter_key}_{method}_smooth_{interval}m.json" + contour_model_path = models_dir / f"{stem}_smooth.npz" + representation = "regular_grid" if method == "dtm" else "triangular_mesh" else: contour_filename = f"contour_{filter_key}_{method}_{interval}m.json" + contour_model_path = model_path + representation = MODEL_REPRESENTATIONS.get(method) contour_path = models_dir / contour_filename - if recalculate or not _is_contour_cache_current(contour_path): + if recalculate or not _is_contour_cache_current(contour_path, contour_model_path): if not math.isfinite(interval) or interval < 0.5: return JSONResponse( status_code=400, content={"status": "error", "message": "등고선 간격은 0.5m 이상이어야 합니다."}, ) - contour_model_path = ( - models_dir / f"{stem}_smooth.npz" - if smooth and method in ("dtm", "tin") - else model_path - ) - representation = ( - "regular_grid" - if smooth and method == "dtm" - else "triangular_mesh" - if smooth and method == "tin" - else MODEL_REPRESENTATIONS.get(method) - ) if not contour_model_path.is_file() or representation is None: return JSONResponse( status_code=404, @@ -131,7 +125,7 @@ async def get_surface_model_contour( contour_model_path, representation, interval, - adaptive_contour_grid_resolution(contour_model_path, representation), + SURFACE_CONTOUR_GRID_RESOLUTION_M, None, ) logger.info( diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts index a2fb8deb..c111eee0 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts @@ -175,6 +175,8 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { let smoothPreferred = true; let currentModelId: number | null = null; let currentModelSmooth = false; + // 선택 변경 후 늦게 도착한 이전 로더 콜백이 장면을 오염시키지 않도록 세대를 추적한다. + let loadGeneration = 0; const scene = new THREE.Scene(); scene.background = new THREE.Color(0xf5f7f9); @@ -320,6 +322,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { const isSmooth = (activeMethod === "tin" || activeMethod === "dtm") && smoothCheck.checked; currentModelId = modelId; currentModelSmooth = isSmooth; + const generation = ++loadGeneration; statusSpan.textContent = "3D 메쉬 파일 다운로드 중..."; const previewUrl = `${API_BASE_URL}/projects/${currentProjectId}/surface/models/${modelId}/preview?smooth=${isSmooth}`; @@ -329,6 +332,10 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { new PLYLoader().load( previewUrl, async (geometry) => { + if (generation !== loadGeneration) { + geometry.dispose(); + return; + } geometry.computeBoundingSphere(); const material = new THREE.PointsMaterial({ size: 0.35, @@ -344,6 +351,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { }, undefined, () => { + if (generation !== loadGeneration) return; statusSpan.textContent = "3D 파일 로드에 실패했습니다."; }, ); @@ -351,6 +359,10 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { new GLTFLoader().load( previewUrl, async (gltf) => { + if (generation !== loadGeneration) { + disposeObject(gltf.scene); + return; + } gltf.scene.traverse((child) => { if (child instanceof THREE.Mesh) { child.material.side = THREE.DoubleSide; @@ -365,6 +377,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { }, undefined, () => { + if (generation !== loadGeneration) return; statusSpan.textContent = "3D 메쉬 파일이 없거나 로드할 수 없습니다."; }, ); diff --git a/config/config_system.py b/config/config_system.py index 3688b4e2..0ef0f45c 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -99,8 +99,9 @@ SURFACE_RANSAC_SEED = int(os.getenv("SURFACE_RANSAC_SEED", "42")) SURFACE_MODEL_SOURCE_FILTERS = tuple( os.getenv("SURFACE_MODEL_SOURCE_FILTERS", "grid_min_z,csf,pmf").split(",") ) +# dtm을 먼저 빌드해야 TIN 등고선 사전 캐시가 dtm footprint를 참조할 수 있다 (PLAN D-6) SURFACE_MODEL_PRECOMPUTE = tuple( - os.getenv("SURFACE_MODEL_PRECOMPUTE", "tin,dtm,nurbs,implicit,meshfree").split(",") + os.getenv("SURFACE_MODEL_PRECOMPUTE", "dtm,tin,nurbs,implicit,meshfree").split(",") ) SURFACE_MODEL_SMOOTHING_METHODS = tuple( os.getenv("SURFACE_MODEL_SMOOTHING_METHODS", "dtm,tin").split(",") @@ -115,10 +116,10 @@ SURFACE_KEEP_LARGEST_FOOTPRINT = ( os.getenv("SURFACE_KEEP_LARGEST_FOOTPRINT", "True").lower() == "true" ) SURFACE_TILE_SIZE_M = float(os.getenv("SURFACE_TILE_SIZE_M", "50.0")) -SURFACE_MAX_PREVIEW_VERTICES = int(os.getenv("SURFACE_MAX_PREVIEW_VERTICES", "120000")) +SURFACE_MAX_PREVIEW_VERTICES = int(os.getenv("SURFACE_MAX_PREVIEW_VERTICES", "500000")) -# 표현별 파라미터 -SURFACE_TIN_MAX_INPUT_POINTS = int(os.getenv("SURFACE_TIN_MAX_INPUT_POINTS", "200000")) +# 표현별 파라미터 (old 버전 검증값 기준 — PLAN F) +SURFACE_TIN_MAX_INPUT_POINTS = int(os.getenv("SURFACE_TIN_MAX_INPUT_POINTS", "500000")) SURFACE_DTM_GRID_RESOLUTION_M = float(os.getenv("SURFACE_DTM_GRID_RESOLUTION_M", "1.0")) SURFACE_NURBS_DEGREE = int(os.getenv("SURFACE_NURBS_DEGREE", "3")) SURFACE_NURBS_PATCH_SIZE_M = float(os.getenv("SURFACE_NURBS_PATCH_SIZE_M", "50.0")) @@ -126,11 +127,11 @@ SURFACE_NURBS_CONTROL_POINTS_PER_AXIS = int( os.getenv("SURFACE_NURBS_CONTROL_POINTS_PER_AXIS", "16") ) SURFACE_IMPLICIT_MAX_POINTS_PER_TILE = int( - os.getenv("SURFACE_IMPLICIT_MAX_POINTS_PER_TILE", "20000") + os.getenv("SURFACE_IMPLICIT_MAX_POINTS_PER_TILE", "10000") ) -SURFACE_IMPLICIT_SMOOTHING = float(os.getenv("SURFACE_IMPLICIT_SMOOTHING", "0.5")) -SURFACE_MESHFREE_MAX_MODEL_POINTS = int(os.getenv("SURFACE_MESHFREE_MAX_MODEL_POINTS", "300000")) -SURFACE_MESHFREE_POINT_RADIUS_M = float(os.getenv("SURFACE_MESHFREE_POINT_RADIUS_M", "0.5")) +SURFACE_IMPLICIT_SMOOTHING = float(os.getenv("SURFACE_IMPLICIT_SMOOTHING", "0.1")) +SURFACE_MESHFREE_MAX_MODEL_POINTS = int(os.getenv("SURFACE_MESHFREE_MAX_MODEL_POINTS", "500000")) +SURFACE_MESHFREE_POINT_RADIUS_M = float(os.getenv("SURFACE_MESHFREE_POINT_RADIUS_M", "0.15")) # 스무딩 파라미터 SURFACE_SMOOTHING_DTM_SIGMA_M = float(os.getenv("SURFACE_SMOOTHING_DTM_SIGMA_M", "0.5"))