feat(B03·B04): 지형 라이다 여러 장 입력·병합 전처리

- 지형 파일 개수 제한 해제(정확히 1개 → 1장 이상), 한 카드에 여러 장 담기
  (화면 표시 「용화_서편.las 외 1장」, 업로드는 한 장씩 차례로 전송)
- 구조화 엔진이 여러 파일을 합친 범위로 한 벌 생성 — WF1 자동 전처리·B04 재분석 모두
  프로젝트 지형 파일 전부를 대상으로 실행
- 점이 1억 개를 넘으면 씨닝 — 지면 분류점은 전부 남기고 나머지만 0.5m 칸 최저점으로 축소
  (용화 실측: 4,900만점 → 249만점, 지면점 1,140,716개 그대로, 1m 지면격자 표고차 0.0000m)
- 머리글만 읽어 5km 넘게 떨어진 파일은 업로드 거부 (임도는 길어도 2~3km)
- 화면 조립부 700줄 준수를 위해 terrainCoverage 를 판정 모듈로 이동

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 17:45:59 +09:00
co-authored by Claude Opus 5
parent 2d0895d975
commit 72075563e3
11 changed files with 457 additions and 122 deletions
+30 -14
View File
@@ -7,7 +7,7 @@
import json
import logging
import time
from collections.abc import Callable
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Any
@@ -37,14 +37,22 @@ 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 _source_identity(las_paths: list[Path]) -> dict[str, Any]:
"""입력 지형 파일들의 정체성(이름·크기·수정시각)으로 캐시 세대를 식별한다 (PLAN B-2).
여러 장을 병합하므로 **한 장이라도 바뀌거나 늘고 줄면** 다시 계산해야 한다
(2026-09-06 다중 입력).
"""
files = [
{
"filename": path.name,
"size_bytes": int(path.stat().st_size),
"mtime": float(path.stat().st_mtime),
}
for path in sorted(las_paths, key=lambda item: item.name)
]
# 한 장일 때는 옛 형식과 같은 모양을 유지한다 — 이미 만든 캐시를 헛되이 버리지 않는다.
return files[0] if len(files) == 1 else {"files": files}
def _relative_to_project(project_root: Path, path: Path) -> str:
@@ -108,7 +116,7 @@ def cache_ground_points(
def run_surface_analysis(
project_root: Path,
las_path: Path,
las_path: Path | Sequence[Path],
*,
source_filters: list[str] | None,
methods: list[str],
@@ -117,6 +125,9 @@ def run_surface_analysis(
) -> dict[str, Any]:
"""구조화→필터→모델 빌드를 수행하고 산출 메타데이터를 반환한다.
`las_path`는 지형 파일 한 장 또는 여러 장이다 — 여러 장이면 합친 범위로 한 벌을
만든다(2026-09-06 사용자 확정).
`source_filters`가 비면 입력 LAS를 보고 기본 필터를 정한다(자동 전처리 경로).
반환 dict:
@@ -132,6 +143,9 @@ def run_surface_analysis(
on_progress(percent, stage, message)
total_started = time.monotonic()
las_paths = [las_path] if isinstance(las_path, Path) else [Path(item) for item in las_path]
if not las_paths:
raise ValueError("지형 파일이 없습니다.")
stage_root = project_root / "B04_PreProcess"
processed_dir = stage_root / "processed"
models_dir = stage_root / "models"
@@ -140,7 +154,7 @@ def run_surface_analysis(
# 0. 입력 세대 검증: LAS가 바뀌었으면 모든 캐시를 재계산한다 (PLAN B-2)
identity_path = processed_dir / "source_identity.json"
current_identity = _source_identity(las_path)
current_identity = _source_identity(las_paths)
stored_identity: dict[str, Any] | None = None
if identity_path.is_file():
try:
@@ -154,10 +168,12 @@ def run_surface_analysis(
if rebuild or not structured_path.is_file():
_report(10, "structurize", "LAS 구조화 중")
step_started = time.monotonic()
structured_path = structurize_las(las_path, processed_dir)
structured_path = structurize_las(las_paths, processed_dir)
atomic_write_json(identity_path, current_identity)
logger.info(
"B04 LAS 구조화 완료: %s (%.1fs)", las_path.name, time.monotonic() - step_started
"B04 LAS 구조화 완료: %s (%.1fs)",
", ".join(path.name for path in las_paths),
time.monotonic() - step_started,
)
else:
_report(10, "structurize", "구조화 캐시 재사용")
@@ -254,7 +270,7 @@ def run_surface_analysis(
"z": [float(bounds[2, 0]), float(bounds[2, 1])],
}
download_geodata(
project_root, processed_dir, las_bounds_dict, las_path.parent, rebuild, report=_report
project_root, processed_dir, las_bounds_dict, las_paths[0].parent, rebuild, report=_report
)
# 3-4. 도엽등고선 3D 서피스 — LAS가 있어도 참고용으로 같이 만들어 영구저장한다