| """Detection endpoints.""" |
|
|
| from __future__ import annotations |
|
|
| import io |
| import shutil |
| import tempfile |
| from pathlib import Path |
|
|
| from fastapi import APIRouter, File, Form, HTTPException, UploadFile |
| from fastapi.responses import FileResponse |
|
|
| from api.schemas import DetectionResponse, VideoJobCreated, VideoJobStatus |
| from api.services import detection_service |
|
|
| router = APIRouter(prefix="/api/detect", tags=["detection"]) |
|
|
|
|
| @router.post("/image", response_model=DetectionResponse) |
| async def detect_image( |
| file: UploadFile = File(...), |
| model: str | None = Form(default=None), |
| conf: float = Form(default=0.25), |
| heatmap: bool = Form(default=True), |
| ) -> DetectionResponse: |
| |
| |
| try: |
| import numpy as np |
| from PIL import Image |
| except ImportError as exc: |
| raise HTTPException( |
| status_code=503, |
| detail=( |
| "Detection stack not installed on the server " |
| "(pip install -r requirements.txt). Catalog and metrics " |
| "endpoints remain available." |
| ), |
| ) from exc |
|
|
| if not file.content_type or not file.content_type.startswith("image/"): |
| raise HTTPException(status_code=400, detail="Expected an image upload.") |
|
|
| raw = await file.read() |
| try: |
| img_rgb = np.array(Image.open(io.BytesIO(raw)).convert("RGB")) |
| except Exception as exc: |
| raise HTTPException(status_code=400, detail=f"Unreadable image: {exc}") from exc |
|
|
| conf = float(min(max(conf, 0.01), 0.99)) |
| try: |
| result = detection_service.detect_image(img_rgb, model, conf, want_heatmap=heatmap) |
| except FileNotFoundError as exc: |
| raise HTTPException(status_code=503, detail=str(exc)) from exc |
|
|
| return DetectionResponse(**result) |
|
|
|
|
| @router.post("/video", response_model=VideoJobCreated) |
| async def detect_video( |
| file: UploadFile = File(...), |
| model: str | None = Form(default=None), |
| conf: float = Form(default=0.25), |
| stride: int = Form(default=3), |
| |
| |
| altitude: float | None = Form(default=None), |
| hfov: float | None = Form(default=None), |
| pitch: float = Form(default=0.0), |
| heading: float = Form(default=0.0), |
| frame_spacing_m: float = Form(default=30.0), |
| ) -> VideoJobCreated: |
| try: |
| from api.services import video_service |
| import cv2 |
| except ImportError as exc: |
| raise HTTPException( |
| status_code=503, |
| detail="Detection stack not installed on the server " |
| "(pip install -r requirements.txt).", |
| ) from exc |
|
|
| if not file.content_type or not file.content_type.startswith("video/"): |
| raise HTTPException(status_code=400, detail="Expected a video upload.") |
|
|
| conf = float(min(max(conf, 0.01), 0.99)) |
| stride = int(min(max(stride, 1), 30)) |
|
|
| survey = None |
| if altitude is not None and hfov is not None: |
| survey = {"altitude": altitude, "hfov": hfov, "pitch": pitch, |
| "heading": heading, "frame_spacing_m": frame_spacing_m} |
|
|
| |
| suffix = Path(file.filename or "clip.mp4").suffix or ".mp4" |
| with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: |
| shutil.copyfileobj(file.file, tmp) |
| tmp_path = tmp.name |
|
|
| job_id = video_service.create_job( |
| tmp_path, file.filename or "clip.mp4", model, conf, stride, survey=survey |
| ) |
| return VideoJobCreated(job_id=job_id) |
|
|
|
|
| @router.get("/video/{job_id}", response_model=VideoJobStatus) |
| def video_status(job_id: str) -> VideoJobStatus: |
| from api.services import video_service |
|
|
| job = video_service.get_job(job_id) |
| if job is None: |
| raise HTTPException(status_code=404, detail="Unknown video job.") |
| return VideoJobStatus(**{k: v for k, v in job.items() if not k.startswith("_")}) |
|
|
|
|
| @router.get("/video/{job_id}/file") |
| def video_file(job_id: str) -> FileResponse: |
| from api.services import video_service |
|
|
| job = video_service.get_job(job_id) |
| if job is None: |
| raise HTTPException(status_code=404, detail="Unknown video job.") |
| out = job.get("_output_path") |
| if job["status"] != "done" or not out or not Path(out).exists(): |
| raise HTTPException(status_code=409, detail="Video not ready.") |
| stem = Path(job.get("filename") or "clip").stem |
| return FileResponse(out, media_type="video/mp4", |
| filename=f"prometheus_{stem}_annotated.mp4") |
|
|