222 lines
7.4 KiB
Python
222 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, File, HTTPException, UploadFile
|
|
from fastapi import Response
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
|
|
from .analyzer import (
|
|
LAS_PREVIEW_PATH,
|
|
PREVIEW_PATH,
|
|
ROOT_DIR,
|
|
SAMPLE_DIR,
|
|
analyze_project,
|
|
analyze_sample_project,
|
|
collect_project_files,
|
|
confirm_sample_release,
|
|
create_ground_filter_cache,
|
|
find_las_in_dir,
|
|
get_project_source_dir,
|
|
get_project_storage_dir,
|
|
load_analysis,
|
|
load_analysis_for_project,
|
|
load_or_create_las_points_for_project,
|
|
load_or_create_las_points_sample,
|
|
)
|
|
from .classifier import ClassifyParams, run_classification
|
|
|
|
|
|
app = FastAPI(title="Forest Road Phase 1 Analyzer", version="0.1.0")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=False,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
FRONTEND_DIST = ROOT_DIR / "frontend" / "dist"
|
|
FRONTEND_ASSETS = FRONTEND_DIST / "assets"
|
|
|
|
if FRONTEND_ASSETS.exists():
|
|
app.mount("/assets", StaticFiles(directory=FRONTEND_ASSETS), name="frontend-assets")
|
|
|
|
|
|
# ---- Health / Root ----
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/", response_model=None)
|
|
def root() -> FileResponse | dict[str, object]:
|
|
index_path = FRONTEND_DIST / "index.html"
|
|
if index_path.exists():
|
|
return FileResponse(index_path)
|
|
return {
|
|
"name": "Forest Road Phase 1 Analyzer",
|
|
"status": "running",
|
|
"frontend_dev": "http://localhost:5173",
|
|
}
|
|
|
|
|
|
@app.get("/favicon.ico", include_in_schema=False)
|
|
def favicon() -> Response:
|
|
return Response(status_code=204)
|
|
|
|
|
|
# ---- Upload & Sample Check ----
|
|
|
|
class CheckSampleRequest(BaseModel):
|
|
filenames: list[str]
|
|
|
|
|
|
@app.post("/api/check-sample")
|
|
def check_sample(body: CheckSampleRequest) -> dict[str, object]:
|
|
sample_filenames = {f.name for f in SAMPLE_DIR.iterdir() if f.is_file()}
|
|
matched = all(fn in sample_filenames for fn in body.filenames)
|
|
return {"matched": matched, "project_id": "sample" if matched else None}
|
|
|
|
|
|
@app.post("/api/upload")
|
|
async def upload_files(
|
|
las_file: UploadFile = File(...),
|
|
prj_file: UploadFile = File(...),
|
|
tfw_file: UploadFile = File(...),
|
|
tif_file: UploadFile | None = File(None),
|
|
) -> dict[str, object]:
|
|
project_id = f"upload_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
raw_dir = ROOT_DIR / "storage" / "projects" / project_id / "raw"
|
|
raw_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for upload in [las_file, prj_file, tfw_file] + ([tif_file] if tif_file else []):
|
|
if upload and upload.filename:
|
|
dest = raw_dir / upload.filename
|
|
content = await upload.read()
|
|
dest.write_bytes(content)
|
|
|
|
result = analyze_project(project_id)
|
|
return {"project_id": project_id, "status": result.get("status", "ready")}
|
|
|
|
|
|
# ---- Dynamic project endpoints ----
|
|
|
|
@app.get("/api/projects/{project_id}/analysis")
|
|
def get_project_analysis(project_id: str) -> dict[str, object]:
|
|
analysis = load_analysis_for_project(project_id)
|
|
if analysis is None:
|
|
if project_id == "sample":
|
|
return analyze_sample_project()
|
|
return analyze_project(project_id)
|
|
return analysis
|
|
|
|
|
|
@app.get("/api/projects/{project_id}/las-points-sample")
|
|
def get_project_las_points(project_id: str) -> dict[str, object]:
|
|
return load_or_create_las_points_for_project(project_id)
|
|
|
|
|
|
@app.post("/api/projects/{project_id}/analyze-ground")
|
|
def run_ground_analysis(project_id: str) -> dict[str, object]:
|
|
storage_dir = get_project_storage_dir(project_id)
|
|
stats_path = storage_dir / "processed" / "ground-stats.json"
|
|
if stats_path.exists():
|
|
return json.loads(stats_path.read_text(encoding="utf-8"))
|
|
result = create_ground_filter_cache(project_id)
|
|
stats_path.parent.mkdir(parents=True, exist_ok=True)
|
|
stats_path.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8")
|
|
return result
|
|
|
|
|
|
class ClassifyRequest(BaseModel):
|
|
rgb_exg_threshold: float = 0.05
|
|
ground_height_threshold: float = 1.5
|
|
|
|
|
|
@app.post("/api/projects/{project_id}/classify")
|
|
def classify_points(project_id: str, body: ClassifyRequest) -> dict[str, object]:
|
|
source_dir = get_project_source_dir(project_id)
|
|
las_path = find_las_in_dir(source_dir)
|
|
if not las_path.exists():
|
|
raise HTTPException(status_code=404, detail="LAS 파일을 찾을 수 없습니다.")
|
|
cache_path = get_project_storage_dir(project_id) / "processed" / "features.npz"
|
|
params = ClassifyParams(
|
|
rgb_exg_threshold=body.rgb_exg_threshold,
|
|
ground_height_threshold=body.ground_height_threshold,
|
|
)
|
|
return run_classification(las_path, cache_path, params)
|
|
|
|
|
|
@app.get("/api/projects/{project_id}/all-points")
|
|
def get_all_points(project_id: str) -> dict[str, object]:
|
|
path = get_project_storage_dir(project_id) / "processed" / "all-points.json"
|
|
if not path.exists():
|
|
raise HTTPException(status_code=404, detail="전체 포인트 캐시가 없습니다. analyze-ground를 먼저 실행하세요.")
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
@app.get("/api/projects/{project_id}/ground-points")
|
|
def get_ground_points(project_id: str) -> dict[str, object]:
|
|
path = get_project_storage_dir(project_id) / "processed" / "ground-points.json"
|
|
if not path.exists():
|
|
raise HTTPException(status_code=404, detail="지면 포인트 캐시가 없습니다. analyze-ground를 먼저 실행하세요.")
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
@app.get("/api/projects/{project_id}/preview")
|
|
def get_project_preview(project_id: str) -> FileResponse:
|
|
preview_path = get_project_storage_dir(project_id) / "processed" / "preview.png"
|
|
if not preview_path.exists():
|
|
if project_id == "sample":
|
|
if not PREVIEW_PATH.exists():
|
|
analyze_sample_project()
|
|
preview_path = PREVIEW_PATH
|
|
else:
|
|
raise HTTPException(status_code=404, detail="미리보기 이미지가 없습니다.")
|
|
if not preview_path.exists():
|
|
raise HTTPException(status_code=404, detail="미리보기 이미지를 생성하지 못했습니다.")
|
|
return FileResponse(preview_path, media_type="image/png")
|
|
|
|
|
|
# ---- Legacy sample endpoints (backward compat) ----
|
|
|
|
@app.get("/api/projects/sample")
|
|
def get_sample_project() -> dict[str, str]:
|
|
return {
|
|
"id": "sample",
|
|
"name": "개발용 샘플 프로젝트",
|
|
"description": "samples/step1_scan 기반 Phase 1 분석 대상",
|
|
}
|
|
|
|
|
|
@app.get("/api/projects/sample/files")
|
|
def get_sample_files() -> dict[str, object]:
|
|
return {"project_id": "sample", "files": collect_project_files()}
|
|
|
|
|
|
@app.post("/api/projects/sample/analyze")
|
|
def run_sample_analysis() -> dict[str, object]:
|
|
return analyze_sample_project()
|
|
|
|
|
|
@app.get("/api/projects/sample/las-preview")
|
|
def get_sample_las_preview() -> FileResponse:
|
|
if not LAS_PREVIEW_PATH.exists():
|
|
analyze_sample_project()
|
|
if not LAS_PREVIEW_PATH.exists():
|
|
raise HTTPException(status_code=404, detail="LAS preview image is not available.")
|
|
return FileResponse(Path(LAS_PREVIEW_PATH), media_type="image/png")
|
|
|
|
|
|
@app.post("/api/projects/sample/confirm")
|
|
def confirm_sample() -> dict[str, object]:
|
|
return confirm_sample_release()
|