187 lines
7.2 KiB
Python
187 lines
7.2 KiB
Python
"""B04 지표면 분석 엔진 오케스트레이터.
|
|
|
|
원본 LAS를 구조화하고 지면 필터를 실행한 뒤 지표면 5종 모델을 빌드하는
|
|
동기 계산 파이프라인. 라우터에서 asyncio.to_thread로 호출한다.
|
|
"""
|
|
|
|
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_Pipeline import build_all_terrain_models
|
|
from B04_wf1_Surface.B04_wf1_Surface_Engine_Structurize import structurize_las
|
|
from config.config_system import build_surface_model_config
|
|
|
|
# 진행 콜백 시그니처: (진행률 0~100, 현재 단계 키, 메시지)
|
|
ProgressCallback = Callable[[int, str, str], None]
|
|
|
|
|
|
def _relative_to_project(project_root: Path, path: Path) -> str:
|
|
"""프로젝트 루트 기준 posix 상대 경로 문자열."""
|
|
return path.relative_to(project_root).as_posix()
|
|
|
|
|
|
def run_surface_analysis(
|
|
project_root: Path,
|
|
las_path: Path,
|
|
*,
|
|
source_filters: list[str],
|
|
methods: list[str],
|
|
force: bool = False,
|
|
on_progress: ProgressCallback | None = None,
|
|
) -> dict[str, Any]:
|
|
"""구조화→필터→모델 빌드를 수행하고 산출 메타데이터를 반환한다.
|
|
|
|
반환 dict:
|
|
- processed: {processed_file_path, converted_file_path, point_count, bounds, statistics}
|
|
- ground_summary: 필터별 지면 포인트 요약
|
|
- manifest: 지표면 모델 파이프라인 manifest
|
|
- models: [{model_type, model_file_path, resolution_m, generation_params, layers}]
|
|
"""
|
|
|
|
def _report(percent: int, stage: str, message: str) -> None:
|
|
if on_progress is not None:
|
|
on_progress(percent, stage, message)
|
|
|
|
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)
|
|
with np.load(structured_path) as structured:
|
|
xyz = structured["xyz"]
|
|
bounds = structured["bounds"]
|
|
total_points = int(len(xyz))
|
|
stats = {
|
|
"min_z": float(bounds[2, 0]),
|
|
"max_z": float(bounds[2, 1]),
|
|
"mean_z": float(np.mean(xyz[:, 2])) if total_points else None,
|
|
}
|
|
bounds_dict = {
|
|
"x_min": float(bounds[0, 0]),
|
|
"x_max": float(bounds[0, 1]),
|
|
"y_min": float(bounds[1, 0]),
|
|
"y_max": float(bounds[1, 1]),
|
|
}
|
|
data = {"xyz": xyz, "bounds": bounds}
|
|
|
|
# 2. 지면 필터 실행
|
|
_report(40, "ground_filter", "지면 필터 적용 중")
|
|
masks = build_ground_masks(data, source_filters)
|
|
ground_summary = summarize_masks(data, masks)
|
|
|
|
# 3. 지표면 5종 모델 빌드
|
|
_report(70, "surface_model", "지표면 모델 생성 중")
|
|
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)
|
|
|
|
# 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"))
|
|
)
|
|
prj_path = prj_files[0] if prj_files else project_root / "result.prj"
|
|
|
|
bounds_dict_for_download = {
|
|
"x": [float(bounds[0, 0]), float(bounds[0, 1])],
|
|
"y": [float(bounds[1, 0]), float(bounds[1, 1])],
|
|
"z": [float(bounds[2, 0]), float(bounds[2, 1])],
|
|
}
|
|
|
|
from B04_wf1_Surface.B04_wf1_Surface_Engine_GisVector import download_all_gis_vectors
|
|
from B04_wf1_Surface.B04_wf1_Surface_Engine_VWorld import download_vworld_satellite_map
|
|
|
|
# VWorld 지도 및 GIS 데이터 저장 위치는 B04_wf1_Surface/processed에 보관.
|
|
layers = [
|
|
{"layer": "Satellite", "ext": "jpeg"},
|
|
{"layer": "Hybrid", "ext": "png"},
|
|
{"layer": "white", "ext": "png"},
|
|
]
|
|
for item in layers:
|
|
try:
|
|
download_vworld_satellite_map(
|
|
prj_path,
|
|
bounds_dict_for_download,
|
|
processed_dir,
|
|
layer_name=item["layer"],
|
|
ext=item["ext"],
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
download_all_gis_vectors(prj_path, bounds_dict_for_download, processed_dir)
|
|
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
_report(95, "saving", "결과 저장 중")
|
|
|
|
processed = {
|
|
"processed_file_path": _relative_to_project(project_root, structured_path),
|
|
"converted_file_path": None,
|
|
"point_count": total_points,
|
|
"bounds": bounds_dict,
|
|
"statistics": stats,
|
|
}
|
|
|
|
# manifest에서 저장된 모델별 정보 추출 (필터별 대표 모델)
|
|
models: list[dict[str, Any]] = []
|
|
for filter_key, filter_entry in manifest.get("source_filters", {}).items():
|
|
for method, meta in filter_entry.get("methods", {}).items():
|
|
if meta.get("status") != "completed":
|
|
continue
|
|
model_file = meta.get("model_file")
|
|
model_path = (models_dir / model_file) if model_file else None
|
|
layers: list[dict[str, Any]] = []
|
|
if meta.get("preview_file"):
|
|
layers.append(
|
|
{
|
|
"layer_name": f"{method}_{filter_key}_preview",
|
|
"geometry_type": "MESH" if method != "meshfree" else "POINTCLOUD",
|
|
"file_path": _relative_to_project(
|
|
project_root, models_dir / meta["preview_file"]
|
|
),
|
|
"file_format": "glb" if method != "meshfree" else "ply",
|
|
}
|
|
)
|
|
models.append(
|
|
{
|
|
"model_type": method,
|
|
"source_filter": filter_key,
|
|
"representation": meta.get("representation"),
|
|
"model_file_path": _relative_to_project(project_root, model_path)
|
|
if model_path
|
|
else None,
|
|
"resolution_m": meta.get("grid_resolution_meters"),
|
|
"generation_params": {
|
|
"source_filter": filter_key,
|
|
"representation": meta.get("representation"),
|
|
"footprint_area_m2": meta.get("footprint_area_m2"),
|
|
},
|
|
"layers": layers,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"processed": processed,
|
|
"ground_summary": ground_summary,
|
|
"manifest": manifest,
|
|
"models": models,
|
|
}
|