satdetect-dev / app /dda /local_routes.py
coderuday21's picture
Improve detection accuracy with full-res tiling, config flags, and evaluation.
99e1f27
Raw
History Blame Contribute Delete
6.97 kB
"""Slim local helpers: thumb, resolve, detect — backed by tree library."""
import logging
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request
from fastapi.responses import FileResponse
from PIL import Image
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import User
from .dda_auth import current_dda_user
from .detect_service import run_detection_and_save
from .geotiff_io import load_rgb_pil
from .config import (
IS_DDA_MODE,
MAX_GEOTIFF_BYTES,
MAX_IMAGE_BYTES,
geotiff_io_available,
get_detection_max_side,
get_library_roots,
get_storage_root,
is_hf_hosted,
)
from .tree.image_service import get_or_build_thumb, list_all_images
from .tree.path_service import resolve_file
from .tree.tree_service import build_tree
logger = logging.getLogger(__name__)
router = APIRouter()
def _require_dda():
if not IS_DDA_MODE:
raise HTTPException(
status_code=404,
detail="DDA mode is not enabled. On HF dev Space this is automatic; locally use: python run.py",
)
def safe_resolve(relative_path: str) -> Path:
try:
return resolve_file(relative_path)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Image file not found")
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.get("/local/config")
def local_library_config():
_require_dda()
storage = str(get_storage_root())
hosted = is_hf_hosted()
instructions = (
"Select a node in the tree, choose image type, and upload. "
"Files are stored under {zone}/{area}/…/Images/ in persistent storage."
if hosted
else "Use the tree library to organize images by zone/area/year, or upload via the form."
)
return {
"source": "tree_library",
"isHosted": hosted,
"spaceId": __import__("os").environ.get("SPACE_ID", ""),
"appMode": "dda" if IS_DDA_MODE else "legacy",
"rootPath": storage,
"rootPaths": [str(r) for r in get_library_roots()],
"writablePath": storage,
"storageRoot": storage,
"instructions": instructions,
"geotiffEnabled": geotiff_io_available(),
"detectionMaxSide": get_detection_max_side(),
"maxGeotiffMb": MAX_GEOTIFF_BYTES // (1024 * 1024),
"maxGeotiffBytes": MAX_GEOTIFF_BYTES,
"maxImageMb": MAX_IMAGE_BYTES // (1024 * 1024),
"maxImageBytes": MAX_IMAGE_BYTES,
"maxUploadGb": round(MAX_GEOTIFF_BYTES / (1024 ** 3), 2),
}
@router.get("/local/images")
def local_images(
node_id: Optional[int] = Query(None),
q: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
_require_dda()
return list_all_images(db, node_id=node_id, query=q)
@router.get("/local/thumb")
def local_thumb(path: str = Query(...)):
_require_dda()
try:
thumb = get_or_build_thumb(path)
return FileResponse(thumb, media_type="image/png")
except Exception as exc:
logger.warning("Thumb endpoint fallback for %s: %s", path, exc)
from .config import LOCAL_THUMB_CACHE
from .geotiff_io import write_placeholder_png
cache = LOCAL_THUMB_CACHE / "fallback.png"
write_placeholder_png(cache, Path(path).name)
return FileResponse(cache, media_type="image/png")
@router.post("/local/rescan")
def local_rescan(db: Session = Depends(get_db)):
_require_dda()
from .tree.sync_service import sync_from_filesystem
try:
sync_stats = sync_from_filesystem(db)
except Exception as exc:
logger.exception("Filesystem rescan failed")
raise HTTPException(status_code=500, detail=f"Rescan failed: {exc}") from exc
tree = build_tree(db)
images = list_all_images(db)
return {
"ok": True,
"tree": tree,
"totalImages": len(images),
"storageRoot": str(get_storage_root()),
"sync": sync_stats,
}
@router.post("/detect/from-library")
async def detect_from_library(
request: Request,
base_path: str = Form(...),
comparison_path: str = Form(...),
method: str = Form("AI-Based Deep Learning"),
title: str = Form("Untitled run"),
zone: str = Form(""),
village: str = Form(""),
enable_registration: bool = Form(True),
enable_normalization: bool = Form(True),
detection_sensitivity: float = Form(0.5),
min_region_area: Optional[int] = Form(150),
notify_email: Optional[str] = Form(None),
db: Session = Depends(get_db),
user: User = Depends(current_dda_user),
):
"""Run change detection on two library images by relative path."""
_require_dda()
base_norm = base_path.replace("\\", "/").strip()
comp_norm = comparison_path.replace("\\", "/").strip()
if not base_norm or not comp_norm:
raise HTTPException(status_code=400, detail="base_path and comparison_path are required")
if base_norm == comp_norm:
raise HTTPException(status_code=400, detail="Base and comparison images must be different")
try:
base_file = safe_resolve(base_norm)
comp_file = safe_resolve(comp_norm)
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid library path: {exc}") from exc
try:
from ..detection_config import get_load_max_side
load_side = get_load_max_side()
before_pil = load_rgb_pil(base_file, max_side=load_side)
after_pil = load_rgb_pil(comp_file, max_side=load_side)
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Could not load images: {exc}") from exc
if before_pil.size != after_pil.size:
after_pil = after_pil.resize(before_pil.size, Image.Resampling.LANCZOS)
if title == "Untitled run":
title = f"{Path(base_norm).name} vs {Path(comp_norm).name}"
try:
return run_detection_and_save(
db,
before_pil,
after_pil,
method=method,
title=title,
zone=zone,
village=village,
enable_registration=enable_registration,
enable_normalization=enable_normalization,
detection_sensitivity=detection_sensitivity,
min_region_area=min_region_area,
notify_email=notify_email,
max_size=get_detection_max_side(),
geo_bounds_path=base_file,
comparison_file=comp_file,
base_path=base_norm,
user_id=user.id,
)
except Exception as exc:
logger.exception("Library detection failed for %s vs %s", base_norm, comp_norm)
raise HTTPException(status_code=500, detail=f"Detection failed: {exc}") from exc