| """Telemetry endpoint — read flight metadata from an uploaded image. |
| |
| Lets the dashboard auto-fill survey geometry (altitude, FOV, nadir/oblique) |
| from a drone frame's EXIF/XMP instead of asking the user to type it. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import shutil |
| import tempfile |
| from pathlib import Path |
|
|
| from fastapi import APIRouter, File, HTTPException, UploadFile |
|
|
| router = APIRouter(prefix="/api/telemetry", tags=["telemetry"]) |
|
|
|
|
| @router.post("/read") |
| async def read_telemetry(file: UploadFile = File(...)) -> dict: |
| """Parse EXIF/XMP flight telemetry from an uploaded image. |
| |
| Returns what was found (altitude, gimbal angle → nadir/oblique, FOV, GPS), |
| whether it's enough to georeference, and which fields are missing. Never |
| guesses — missing stays missing. |
| """ |
| try: |
| from src.geolocation.telemetry import parse_telemetry |
| except ImportError as exc: |
| raise HTTPException(status_code=503, detail="Pillow not installed.") from exc |
|
|
| suffix = Path(file.filename or "frame.jpg").suffix or ".jpg" |
| with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: |
| shutil.copyfileobj(file.file, tmp) |
| tmp_path = tmp.name |
| try: |
| tel = parse_telemetry(tmp_path) |
| finally: |
| Path(tmp_path).unlink(missing_ok=True) |
|
|
| return { |
| "source": tel.source, |
| "viewpoint": tel.viewpoint, |
| "has_geometry": tel.to_camera() is not None, |
| "missing": tel.missing(), |
| "altitude_m": tel.altitude_m, |
| "gimbal_pitch_deg": tel.gimbal_pitch_deg, |
| "cam_pitch_deg": tel.cam_pitch_deg, |
| "gimbal_yaw_deg": tel.gimbal_yaw_deg, |
| "hfov_deg": tel.hfov_deg, |
| "focal_35mm": tel.focal_35mm, |
| "gps_lat": tel.gps_lat, |
| "gps_lon": tel.gps_lon, |
| "image_width": tel.image_width, |
| "image_height": tel.image_height, |
| } |
|
|