Spaces:
Sleeping
Sleeping
File size: 7,549 Bytes
6241844 9993c90 bfc0af1 1143d23 9993c90 6241844 a918efd 9993c90 6241844 a918efd bfc0af1 9993c90 6241844 9993c90 a918efd 9993c90 a918efd 9993c90 d35ef57 9993c90 a918efd 9993c90 a918efd 9993c90 d35ef57 6241844 9993c90 a918efd 9993c90 a918efd 9993c90 1143d23 5f084a6 1143d23 bfc0af1 0d15c78 5f084a6 796dd6f 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 bfc0af1 5f084a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from pydantic import BaseModel,validator
from typing import Optional, List, Literal
import cv2
import numpy as np
import logging
import time
from .utils import (
validate_form,
process_image,
save_image,
load_json,
save_json,
validate_user_and_camera,
extract_metadata,
_bucket_key,
_key_exists,
)
router = APIRouter()
logger = logging.getLogger(__name__)
@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)
json_path = _bucket_key(user_id, camera_name, 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}")
t0 = time.perf_counter()
detections = process_image(img)
logger.info(f"[{file.filename}] inference: {round((time.perf_counter() - t0) * 1000, 2)}ms")
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
}
# βββββββββββββββββ
# VALID LABELS
# βββββββββββββββββ
VALID_LABELS = {
"Deer | Doe",
"Deer | Buck | White Tail Bucks",
"Deer | Buck | Mule Bucks",
}
VALID_LABELS_DISPLAY = list(VALID_LABELS)
# Precompute normalized β canonical mapping (FAST lookup)
NORMALIZED_LABEL_MAP = {l.strip(): l for l in VALID_LABELS}
def normalize_label(label: str) -> str:
return label.strip()
def validate_label(label: str) -> str:
norm = normalize_label(label)
if norm not in NORMALIZED_LABEL_MAP:
raise HTTPException(
status_code=422,
detail=f"Invalid label '{label}'. Must be one of: {VALID_LABELS_DISPLAY}"
)
return NORMALIZED_LABEL_MAP[norm]
def extract_filename(url: str) -> str:
return url.split("/")[-1].split("?")[0]
# βββββββββββββββββ
# 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]
@validator("label")
def validate_label_field(cls, v):
if v is None:
return v
return validate_label(v)
@validator("bbox")
def validate_bbox(cls, v):
if v is None:
return v
if len(v) != 4:
raise ValueError("bbox must have exactly 4 values: [x1, y1, x2, y2]")
x1, y1, x2, y2 = v
if x2 <= x1 or y2 <= y1:
raise ValueError("bbox must satisfy x2 > x1 and y2 > y1")
return v
@validator("detection_index")
def validate_index(cls, v):
if v is not None and v < 0:
raise ValueError("detection_index must be >= 0")
return v
class MultiUpdateRequest(BaseModel):
user_id: str
camera_name: str
image_url: str
operations: List[DetectionOperation]
@validator("operations")
def validate_operations(cls, ops):
if not ops:
raise ValueError("operations list cannot be empty")
for op in ops:
if op.action == "add":
if op.label is None or op.bbox is None:
raise ValueError("'add' requires both label and bbox")
elif op.action == "update":
if op.detection_index is None:
raise ValueError("'update' requires detection_index")
if op.label is None and op.bbox is None:
raise ValueError("'update' requires label or bbox")
elif op.action == "delete":
if op.detection_index is None:
raise ValueError("'delete' requires detection_index")
return ops
# βββββββββββββββββ
# Endpoint
# βββββββββββββββββ
@router.post("/modify_detections")
async def modify_detections(req: MultiUpdateRequest):
validate_user_and_camera(req.user_id, req.camera_name)
json_key = _bucket_key(
req.user_id,
req.camera_name,
f"{req.camera_name}_detections.json"
)
if not _key_exists(json_key):
raise HTTPException(status_code=404, detail="Detections file not found")
data = load_json(json_key)
target_filename = extract_filename(req.image_url)
# Find record (optimized)
record = next(
(
item for item in data
if extract_filename(item.get("image_url", item.get("filename", ""))) == target_filename
),
None
)
if record is None:
raise HTTPException(status_code=404, detail="Image not found")
dets = record.setdefault("detections", [])
# ββ Split operations βββββββββββββββββ
delete_ops = [op for op in req.operations if op.action == "delete"]
other_ops = [op for op in req.operations if op.action != "delete"]
# ββ DELETE (reverse order) βββββββββββ
for op in sorted(delete_ops, key=lambda x: x.detection_index, reverse=True):
idx = op.detection_index
if idx is None or idx >= len(dets):
raise HTTPException(
status_code=400,
detail=f"Invalid delete index {idx} β only {len(dets)} detection(s) exist"
)
dets.pop(idx)
# ββ ADD + UPDATE ββββββββββββββββββββ
for op in other_ops:
if op.action == "add":
dets.append({
"label": op.label,
"confidence": 1.0,
"bbox": op.bbox,
"manually_edited": True
})
elif op.action == "update":
idx = op.detection_index
if idx is None or idx >= len(dets):
raise HTTPException(
status_code=400,
detail=f"Invalid update index {idx} β only {len(dets)} detection(s) exist"
)
if op.label is not None:
dets[idx]["label"] = op.label
if op.bbox is not None:
dets[idx]["bbox"] = op.bbox
dets[idx]["manually_edited"] = True
save_json(json_key, 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)
)
return {
"success": True,
"message": "Detections modified successfully",
"filename": target_filename,
"total_detections": len(dets),
"detections": dets
} |