pet-matcher-api / app.py
Llmb0's picture
Update app.py
2113372 verified
Raw History Blame Contribute Delete
6.77 kB
import os
import io
import uuid
import spaces
import torch
import torch.nn.functional as F
import gradio as gr
from PIL import Image
from ultralytics import YOLO
from transformers import AutoImageProcessor, AutoModel
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
import cloudinary
import cloudinary.uploader
from typing import List
# ==================== 1. CONFIGURACIÓN DE CREDENCIALES ====================
QDRANT_URL = os.environ.get("QDRANT_URL")
QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY")
cloudinary.config(
cloud_name=os.environ.get("CLOUDINARY_CLOUD_NAME"),
api_key=os.environ.get("CLOUDINARY_API_KEY"),
api_secret=os.environ.get("CLOUDINARY_API_SECRET"),
secure=True
)
# Conectar a Qdrant y crear la colección si no existe
qdrant = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
COLLECTION_NAME = "mascotas_perdidas"
VECTOR_SIZE = 1024 # Dimensión exacta de facebook/convnext-base-224
if not qdrant.collection_exists(COLLECTION_NAME):
qdrant.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=VECTOR_SIZE, distance=Distance.COSINE)
)
# ==================== 2. CARGAR MODELOS DE IA ====================
print("Cargando YOLOv8 y ConvNeXt...")
detector = YOLO("yolov8n.pt")
model_name = "facebook/convnext-base-224"
processor = AutoImageProcessor.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
model.eval()
print("¡Modelos listos!")
# ==================== 3. LÓGICA DE VISIÓN Y EMBEDDINGS (CON ZEROGPU) ====================
def crop_pet(pil_img: Image.Image) -> Image.Image:
"""Detecta y recorta el perro o gato de la imagen."""
results = detector(pil_img, verbose=False)
PET_CLASSES = [15, 16] # 15: gato, 16: perro
for r in results:
for box in r.boxes:
if int(box.cls[0]) in PET_CLASSES:
x1, y1, x2, y2 = map(int, box.xyxy[0])
w, h = pil_img.size
x1, y1 = max(0, x1 - 10), max(0, y1 - 10)
x2, y2 = min(w, x2 + 10), min(h, y2 + 10)
return pil_img.crop((x1, y1, x2, y2))
return pil_img
@spaces.GPU
def get_average_embedding(pil_images: List[Image.Image]) -> List[float]:
"""Genera el ADN visual promedio usando ZeroGPU."""
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
embeddings = []
for img in pil_images:
cropped = crop_pet(img.convert("RGB"))
inputs = processor(images=cropped, return_tensors="pt").to(device)
with torch.no_grad():
outputs = model(**inputs)
tensor = outputs.last_hidden_state.mean(dim=[2, 3])
embeddings.append(F.normalize(tensor, p=2, dim=1))
mean_emb = torch.mean(torch.stack(embeddings), dim=0)
final_emb = F.normalize(mean_emb, p=2, dim=1)
return final_emb.squeeze().cpu().tolist()
# ==================== 4. FUNCIONES (INTERFAZ + API REST AUTOMÁTICA) ====================
def registrar_mascota(nombre: str, contacto: str, descripcion: str, archivos):
if not archivos:
return "⚠️ Sube entre 1 y 3 fotos del perro."
imgs = [Image.open(f.name if hasattr(f, "name") else f) for f in archivos[:3]]
# 1. Subir la foto principal a Cloudinary
buf = io.BytesIO()
imgs[0].convert("RGB").save(buf, format="JPEG", quality=85)
buf.seek(0)
upload_res = cloudinary.uploader.upload(buf, folder="mascotas_perdidas")
foto_url = upload_res["secure_url"]
# 2. Calcular embedding promedio (soporta cachorro + adulto)
vector = get_average_embedding(imgs)
# 3. Guardar en Qdrant Cloud
pet_id = str(uuid.uuid4())
qdrant.upsert(
collection_name=COLLECTION_NAME,
points=[
PointStruct(
id=pet_id,
vector=vector,
payload={
"nombre": nombre,
"contacto": contacto,
"descripcion": descripcion,
"foto_url": foto_url
}
)
]
)
return f"✅ ¡Mascota guardada en Qdrant y Cloudinary!\nID: {pet_id}\nFoto URL: {foto_url}"
def buscar_mascota(imagen_pil: Image.Image, umbral: float = 0.75):
if imagen_pil is None:
return "⚠️ Sube una foto del perro encontrado.", None
vector = get_average_embedding([imagen_pil])
resultados = qdrant.query_points(
collection_name=COLLECTION_NAME,
query=vector,
limit=3,
score_threshold=umbral
).points
if not resultados:
return "❌ No se encontraron coincidencias por encima del umbral.", None
texto = "🎉 ¡POSIBLES COINCIDENCIAS ENCONTRADAS!\n\n"
for idx, hit in enumerate(resultados, 1):
similitud = round(hit.score * 100, 2)
texto += f"#{idx} | Coincidencia: {similitud}%\n"
texto += f"🐶 Nombre: {hit.payload.get('nombre')}\n"
texto += f"📞 Contacto: {hit.payload.get('contacto')}\n"
texto += f"📝 Notas: {hit.payload.get('descripcion')}\n\n"
return texto, resultados[0].payload.get("foto_url")
# ==================== 5. INTERFAZ WEB Y API ====================
with gr.Blocks(title="Buscador IA de Mascotas") as demo:
gr.Markdown("# 🐾 Sistema Híbrido de Reconocimiento de Mascotas")
with gr.Tab("1. Reportar Mascota Perdida"):
in_nombre = gr.Textbox(label="Nombre de la mascota")
in_contacto = gr.Textbox(label="Teléfono / WhatsApp del dueño")
in_desc = gr.Textbox(label="Señas particulares (collar, zona, etc.)")
in_fotos = gr.File(file_count="multiple", file_types=["image"], label="Sube 1 a 3 fotos (ej. cachorro y adulto)")
btn_reg = gr.Button("Guardar Mascota Perdida", variant="primary")
out_reg = gr.Textbox(label="Estado del registro")
btn_reg.click(
fn=registrar_mascota,
inputs=[in_nombre, in_contacto, in_desc, in_fotos],
outputs=out_reg,
api_name="registrar"
)
with gr.Tab("2. Buscar Perro Encontrado en la Calle"):
in_buscar_img = gr.Image(type="pil", label="Sube la foto tomada en la calle")
in_umbral = gr.Slider(0.60, 0.95, value=0.75, step=0.01, label="Sensibilidad (0.75 recomendado para cambios de edad)")
btn_bus = gr.Button("Buscar Coincidencias", variant="primary")
out_bus_txt = gr.Textbox(label="Resultados", lines=8)
out_bus_img = gr.Image(label="Foto de la mejor coincidencia")
btn_bus.click(
fn=buscar_mascota,
inputs=[in_buscar_img, in_umbral],
outputs=[out_bus_txt, out_bus_img],
api_name="buscar"
)
demo.launch()