# from fastapi import APIRouter, UploadFile, File, Form, HTTPException # from pathlib import Path # import cv2 # import numpy as np # from .config import UPLOAD_DIR # from .utils import ( # validate_form, # process_image, # save_image, # load_json, # save_json, # validate_user_and_camera,extract_metadata # ) # router = APIRouter() # @router.post("/predict") # async def predict( # user_id: str = Form(...), # camera_name: str = Form(...), # images: list[UploadFile] = File(...) # ): # images = validate_form(user_id, camera_name, images) # validate_user_and_camera(user_id, camera_name) # base = Path(UPLOAD_DIR) / user_id / camera_name # base.mkdir(parents=True, exist_ok=True) # json_path = base / f"{camera_name}_detections.json" # data = load_json(json_path) # new_results = [] # for file in images: # raw = await file.read() # metadata = extract_metadata(raw) # nparr = np.frombuffer(raw, np.uint8) # img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # if img is None: # raise HTTPException(400, f"Invalid image: {file.filename}") # detections = process_image(img) # url = save_image(user_id, camera_name, file.filename, raw) # record = { # "filename": file.filename, # "image_url": url, # "detections": detections, # "metadata": metadata # } # data.append(record) # new_results.append(record) # save_json(json_path, data) # return { # "message": "Images processed successfully", # "camera": camera_name, # "results": new_results # } from fastapi import APIRouter, UploadFile, File, Form, HTTPException from pydantic import BaseModel from pathlib import Path from typing import Optional, List, Literal import cv2 import numpy as np import logging from .config import UPLOAD_DIR from .utils import ( validate_form, process_image, save_image, load_json, save_json, validate_user_and_camera, extract_metadata ) router = APIRouter() logger = logging.getLogger(__name__) # ─── existing endpoint @router.post("/predict") async def predict( user_id: str = Form(...), camera_name: str = Form(...), images: list[UploadFile] = File(...) ): images = validate_form(user_id, camera_name, images) validate_user_and_camera(user_id, camera_name) base = Path(UPLOAD_DIR) / user_id / camera_name base.mkdir(parents=True, exist_ok=True) json_path = base / f"{camera_name}_detections.json" data = load_json(json_path) new_results = [] for file in images: raw = await file.read() metadata = extract_metadata(raw) nparr = np.frombuffer(raw, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: raise HTTPException(400, f"Invalid image: {file.filename}") detections = process_image(img) url = save_image(user_id, camera_name, file.filename, raw) record = { "filename": file.filename, "image_url": url, "detections": detections, "metadata": metadata } data.append(record) new_results.append(record) save_json(json_path, data) return { "message": "Images processed successfully", "camera": camera_name, "results": new_results } # ───────────────── # Request Models # ───────────────── class DetectionOperation(BaseModel): action: Literal["add", "update", "delete"] detection_index: Optional[int] = None label: Optional[str] = None bbox: Optional[List[float]] = None # [x1, y1, x2, y2] class MultiUpdateRequest(BaseModel): user_id: str camera_name: str image_url: str operations: List[DetectionOperation] # ───────────────── # Endpoint # ───────────────── @router.post("/modify_detections") async def modify_detections(req: MultiUpdateRequest): """ Add, update, and delete detections (tags) for a given image. Supports multiple operations in a single request. """ # Validate user user_path = Path(UPLOAD_DIR) / req.user_id if not user_path.exists() or not user_path.is_dir(): raise HTTPException(status_code=404, detail="user not found") # Validate camera camera_path = user_path / req.camera_name if not camera_path.exists() or not camera_path.is_dir(): raise HTTPException(status_code=404, detail="camera not found") # Validate JSON file json_path = camera_path / f"{req.camera_name}_detections.json" if not json_path.exists(): raise HTTPException(status_code=404, detail="detections file not found") # Load data data = load_json(json_path) # Find image record target_filename = req.image_url.split("/")[-1].split("?")[0] record = None for item in data: stored = item.get("image_url", item.get("filename", "")) stored_filename = stored.split("/")[-1].split("?")[0] if stored_filename == target_filename: record = item break if record is None: raise HTTPException(status_code=404, detail="image not found") # ── 6. Ensure detections list exists if "detections" not in record or not isinstance(record["detections"], list): record["detections"] = [] dets = record["detections"] # ── 7. Apply operations safely ────── # NOTE: Reverse delete operations to avoid index shifting issues delete_ops = [op for op in req.operations if op.action == "delete"] other_ops = [op for op in req.operations if op.action != "delete"] # Handle DELETE (reverse order) for op in sorted(delete_ops, key=lambda x: x.detection_index or -1, reverse=True): if op.detection_index is None or op.detection_index >= len(dets): raise HTTPException( status_code=400, detail=f"Invalid delete index {op.detection_index}" ) dets.pop(op.detection_index) # Handle ADD + UPDATE for op in other_ops: # ADD if op.action == "add": dets.append({ "label": op.label or "Unknown", "confidence": 1.0, "bbox": op.bbox or [], "manually_edited": True }) # UPDATE elif op.action == "update": if op.detection_index is None or op.detection_index >= len(dets): raise HTTPException( status_code=400, detail=f"Invalid update index {op.detection_index}" ) if op.label is not None: dets[op.detection_index]["label"] = op.label if op.bbox is not None: dets[op.detection_index]["bbox"] = op.bbox dets[op.detection_index]["manually_edited"] = True # ── 8. Save back ──────────────────── save_json(json_path, data) logger.info( "Detections modified | user=%s camera=%s file=%s ops=%d final_count=%d", req.user_id, req.camera_name, target_filename, len(req.operations), len(dets) ) # ── 9. Response ───────────────────── return { "success": True, "message": "Detections modified successfully", "filename": target_filename, "total_detections": len(dets), "detections": dets }