Files
Aislo/B04_PreProcess/B04_PreProcess_Router_Contour.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

216 lines
9.2 KiB
Python

"""B04 지표면 모델 등고선 FastAPI 라우터 (700줄 규정에 따라 본 라우터에서 분리)."""
import asyncio
import json
import logging
import math
import re
import time
from pathlib import Path
from uuid import UUID
import numpy as np
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, Response
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B04_PreProcess.B04_PreProcess_Engine_Contour import (
CONTOUR_EXTRACTOR_VERSION,
contour_cache_is_stale,
extract_contours,
)
from common_util.common_util_atomic import atomic_write_bytes
from common_util.common_util_http_cache import cached_file_response
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"])
MODEL_REPRESENTATIONS = {
"dtm": "regular_grid",
"implicit": "local_rbf_height_field",
}
def _is_contour_cache_current(contour_path: Path, model_path: Path) -> bool:
"""캐시가 현재 추출기 버전이고 모델 · 같은 필터 dtm · 굳힌 격자보다 최신인지 검사한다."""
if contour_cache_is_stale(contour_path, model_path):
return False
try:
with contour_path.open("rb") as cache_file:
head = cache_file.read(256).decode("utf-8", errors="ignore")
except OSError:
return False
match = re.search(r'"extractor_version"\s*:\s*(\d+)', head)
return bool(match) and int(match.group(1)) == CONTOUR_EXTRACTOR_VERSION
@router.get("/{project_id}/surface/models/{model_id}/contour", response_model=None)
async def get_surface_model_contour(
request: Request,
project_id: UUID,
model_id: int,
interval: float = 1.0,
smooth: bool = False,
recalculate: bool = False,
) -> Response | JSONResponse:
"""지표면 모델의 등고선 JSON 파일을 반환한다."""
pool = get_db_pool()
try:
async with pool.acquire() as connection:
stored_path = await get_project_storage_relative_path(connection, project_id)
async with connection.cursor() as cursor:
await cursor.execute(
"""
SELECT model_type, model_file_path
FROM surface_models
WHERE id = %s AND project_id = %s
""",
(model_id, str(project_id)),
)
row = await cursor.fetchone()
if not row:
return JSONResponse(
status_code=404,
content={"status": "error", "message": "해당 모델을 찾을 수 없습니다."},
)
model_file_path = row[1]
project_root = Path(resolve_stored_project_path(stored_path))
if not model_file_path:
return JSONResponse(
status_code=404,
content={"status": "error", "message": "모델 파일 경로가 없습니다."},
)
model_path = project_root / model_file_path
models_dir = model_path.parent
structured_path = project_root / "B04_PreProcess" / "processed" / "structured.npz"
stem = model_path.stem
# 파일 이름 = `{표현}_{필터}` · 지운 표현(608 · tin 등)이면 404
method, _, filter_key = stem.partition("_")
if smooth and method == "dtm":
contour_filename = f"contour_{filter_key}_{method}_smooth_{interval}m.json"
contour_model_path = models_dir / f"{stem}_smooth.npz"
representation = "regular_grid"
else:
contour_filename = f"contour_{filter_key}_{method}_{interval}m.json"
contour_model_path = model_path
representation = MODEL_REPRESENTATIONS.get(method) if filter_key else None
contour_path = models_dir / contour_filename
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 이상이어야 합니다."},
)
if not contour_model_path.is_file() or representation is None:
return JSONResponse(
status_code=404,
content={
"status": "error",
"message": "등고선 생성에 필요한 모델 파일이 없습니다.",
},
)
generation_started = time.monotonic()
contours = await asyncio.to_thread(
extract_contours,
contour_model_path,
representation,
interval,
SURFACE_CONTOUR_GRID_RESOLUTION_M,
None,
)
logger.info(
"등고선 온디맨드 계산: filter=%s, method=%s, smooth=%s, "
"interval=%.1f, duration=%.1fs",
filter_key,
method,
smooth,
interval,
time.monotonic() - generation_started,
)
# 화면은 이 상자의 중심을 원점 삼아 등고선을 놓는다 — 메시(glb)를 만들 때 쓴
# 상자와 같아야 등고선이 지형 위에 얹힌다. 도엽 서피스는 라이다와 원점을 맞추려
# `scene_bounds`를 따로 갖고 있으므로 그 값이 있으면 먼저 쓴다(2026-09-01).
with np.load(contour_model_path) as model_data:
if "scene_bounds" in model_data:
model_bounds = np.asarray(model_data["scene_bounds"], dtype=float)
elif "bounds" in model_data:
model_bounds = np.asarray(model_data["bounds"], dtype=float)
else:
with np.load(structured_path) as structured:
model_bounds = np.asarray(structured["bounds"], dtype=float)
bounds_payload = {
"x": model_bounds[0].tolist(),
"y": model_bounds[1].tolist(),
"z": model_bounds[2].tolist(),
}
payload = {
"extractor_version": CONTOUR_EXTRACTOR_VERSION,
"project_id": str(project_id),
"source_filter": filter_key,
"method": method,
"interval": interval,
"bounds": bounds_payload,
"contours": contours,
}
atomic_write_bytes(
contour_path,
json.dumps(payload, ensure_ascii=False).encode("utf-8"),
)
if not contour_path.is_file():
return JSONResponse(
status_code=404,
content={
"status": "error",
"message": "등고선 파일이 생성되지 않았거나 존재하지 않습니다.",
},
)
# 브라우저가 이미 같은 등고선 파일을 갖고 있으면 304만 돌려준다(새로고침이 빨라진다).
return cached_file_response(request, contour_path, "application/json", contour_filename)
except Exception:
logger.exception(
"지표면 모델 등고선 조회 실패: project_id=%s, model_id=%s", project_id, model_id
)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "등고선 파일 조회 중 오류가 발생했습니다."},
)
@router.get("/{project_id}/surface/models/{model_id}/quality")
async def get_surface_model_quality(project_id: UUID, model_id: int) -> JSONResponse:
"""서피스 품질 지표 넷(762-2) — 저장본 지문이 다르면 다시 셈 · 격자 표현 밖이면 null."""
from B04_PreProcess.B04_PreProcess_Engine_Quality import ensure_quality
pool = get_db_pool()
async with pool.acquire() as connection:
stored_path = await get_project_storage_relative_path(connection, project_id)
async with connection.cursor() as cursor:
await cursor.execute(
"SELECT model_file_path FROM surface_models WHERE id = %s AND project_id = %s",
(model_id, str(project_id)),
)
row = await cursor.fetchone()
if not row or not row[0]:
return JSONResponse(
status_code=404,
content={"status": "error", "message": "해당 모델을 찾을 수 없습니다."},
)
model_path = Path(resolve_stored_project_path(stored_path)) / row[0]
method, _, filter_key = model_path.stem.partition("_")
try:
quality = await asyncio.to_thread(ensure_quality, model_path.parent, filter_key, method)
except Exception:
logger.exception("서피스 품질 지표 실패: project_id=%s, model_id=%s", project_id, model_id)
quality = None
if quality:
quality = {key: value for key, value in quality.items() if key != "key"}
return JSONResponse(content={"status": "success", "quality": quality})