agrovision / backend /api /count.py
alexp97's picture
feat: modo mock para validar el flujo de conteo sin modelo real
c136a98
Raw
History Blame Contribute Delete
4.92 kB
"""
Archivo: count.py
Fecha de modificación: 03/06/2026
Autor: Equipo AgroVisión
Descripción:
Define las rutas HTTP del MVP: el healthcheck `/api/status` y el conteo síncrono
`/api/count`. El conteo respeta el modo **standby**: mientras `COUNTING_ENABLED`
sea falso o el modelo no esté cargado, responde 503 con un mensaje claro en vez
de intentar inferir.
Acciones Principales:
- Expone `/api/status` (estado + bandera de conteo).
- Expone `/api/count` (inferencia síncrona, gateada por standby).
Estructura Interna:
- `router`: APIRouter con las rutas del MVP.
- `_draw_overlay`: dibuja las cajas detectadas sobre la imagen.
Entradas / Dependencias:
- `fastapi`, `numpy`, `opencv-python`, `backend.config`, `backend.core.*`, `backend.schemas`.
Salidas / Efectos:
- Ninguno persistente; la respuesta es efímera (overlay en base64).
Integración UI:
- Este router es montado por `backend.main:app`.
- La UI Shiny consume `/api/status` y `/api/count` vía HTTPS.
"""
from __future__ import annotations
import base64
import cv2
import numpy as np
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile, status
from backend.config import get_settings
from backend.core.detection import CLASS_PLANT, Detection
from backend.core.metrics import compute_metrics
from backend.schemas import CountResponse, StatusResponse
router = APIRouter()
_OVERLAY_COLOR_BGR: tuple[int, int, int] = (61, 128, 21) # verde Deep Canopy (#15803D) en BGR
_OVERLAY_THICKNESS: int = 2
def _draw_overlay(image_bgr: np.ndarray, detections: list[Detection]) -> str:
"""
Dibuja las cajas detectadas sobre la imagen y la codifica como PNG en base64.
Args:
image_bgr (np.ndarray): Imagen original en formato BGR.
detections (list[Detection]): Detecciones a superponer.
Returns:
str: Imagen anotada (PNG) codificada en base64.
"""
overlay = image_bgr.copy()
for detection in detections:
top_left = (int(detection.x1), int(detection.y1))
bottom_right = (int(detection.x2), int(detection.y2))
cv2.rectangle(overlay, top_left, bottom_right, _OVERLAY_COLOR_BGR, _OVERLAY_THICKNESS)
_, buffer = cv2.imencode(".png", overlay)
return base64.b64encode(buffer.tobytes()).decode("ascii")
@router.get("/api/status", response_model=StatusResponse)
def get_status() -> StatusResponse:
"""
Devuelve el estado del backend y si el módulo de conteo está activo.
Returns:
StatusResponse: Estado, nombre/versión del modelo y bandera de conteo.
"""
settings = get_settings()
return StatusResponse(
status="ok",
model="agrovision-plantcount",
version=settings.model_version,
counting_enabled=settings.counting_enabled,
model_backend=settings.model_backend if settings.counting_enabled else "standby",
)
@router.post("/api/count", response_model=CountResponse)
async def post_count(
request: Request,
file: UploadFile = File(...),
area_ha: float = Form(default=1.0),
) -> CountResponse:
"""
Ejecuta el conteo síncrono sobre un ortomosaico, respetando el modo standby.
Args:
request (Request): Petición; expone el adaptador en `request.app.state`.
file (UploadFile): Ortomosaico RGB en formato JPG/PNG/TIFF.
area_ha (float): Área del lote en hectáreas para calcular densidad.
Returns:
CountResponse: Conteo, densidad, malezas, fallas, confianza y overlay.
Raises:
HTTPException: 503 si el conteo está en standby; 400 si la imagen es inválida.
"""
settings = get_settings()
adapter = getattr(request.app.state, "adapter", None)
if not settings.counting_enabled or adapter is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=(
"Módulo de conteo en standby: el modelo aún no está disponible. "
"Se activará cuando el repo del modelo publique el artefacto."
),
)
raw_bytes = await file.read()
image_bgr = cv2.imdecode(np.frombuffer(raw_bytes, np.uint8), cv2.IMREAD_COLOR)
if image_bgr is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No se pudo decodificar la imagen. Use JPG, PNG o TIFF válido.",
)
detections = adapter.predict(image_bgr, confidence=settings.confidence_threshold)
metrics = compute_metrics(detections, area_ha=area_ha)
plant_detections = [d for d in detections if d.class_id == CLASS_PLANT]
overlay_b64 = _draw_overlay(image_bgr, plant_detections)
return CountResponse(
count=metrics["count"],
density=metrics["density"],
weeds=metrics["weeds"],
failures=metrics["failures"],
confidence=metrics["confidence"],
overlay_b64=overlay_b64,
)