Team Ai
Apppublic

Explicite/api-test

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
face_service.py170 linesDownload Raw Back to services
1# app/services/face_service.py2from io import BytesIO3import base644import numpy as np5from PIL import Image6from fastapi import HTTPException, UploadFile7import math8 9from app.core.config import settings10from app.services.insightface_engine import (11    get_face_app, pick_best_face, to_bbox_dict, cosine_similarity, augment_rgb12)13from app.services.doc_classifier import classify_document14from app.services.face_quality import assess_quality15 16def ensure_image(upload: UploadFile, field_name: str) -> None:17    if not upload:18        raise HTTPException(status_code=400, detail=f"Falta el archivo: {field_name}")19    if not upload.content_type or not upload.content_type.startswith("image/"):20        raise HTTPException(status_code=400, detail=f"'{field_name}' debe ser una imagen.")21 22async def read_as_np_rgb(upload: UploadFile) -> np.ndarray:23    raw = await upload.read()24    if not raw:25        raise HTTPException(status_code=400, detail="La imagen está vacía.")26    try:27        pil = Image.open(BytesIO(raw)).convert("RGB")28        return np.array(pil)  # RGB29    except Exception:30        raise HTTPException(status_code=400, detail="No pude leer la imagen. ¿Formato válido?")31 32def crop_face(np_rgb: np.ndarray, bbox: dict):33    pil_img = Image.fromarray(np_rgb)  # RGB34    width, height = pil_img.size35    top, right, bottom, left = bbox["top"], bbox["right"], bbox["bottom"], bbox["left"]36 37    top = max(0, top); left = max(0, left)38    bottom = min(height, bottom); right = min(width, right)39 40    return pil_img.crop((left, top, right, bottom))41 42def img_to_base64_jpeg(pil_img: Image.Image, quality: int = 95):43    buf = BytesIO()44    pil_img.save(buf, format="JPEG", quality=quality)45    b64 = base64.b64encode(buf.getvalue()).decode("utf-8")46    return b64, f"data:image/jpeg;base64,{b64}"47 48def similarity_to_percent_sigmoid(sim: float, mid: float, k: float) -> float:49    """50    sim suele estar entre ~0.0 y ~1.0 (cosine similarity).51    NO es probabilidad. Esto es un mapeo UI-friendly con curva sigmoid.52    """53    sim = max(0.0, min(1.0, float(sim)))54    x = (sim - mid) * k55    y = 1.0 / (1.0 + math.exp(-x))56    return round(y * 100.0, 2)57 58async def compare_two_uploads(image1: UploadFile, image2: UploadFile):59    ensure_image(image1, "image1")60    ensure_image(image2, "image2")61 62    np1 = await read_as_np_rgb(image1)63    np2 = await read_as_np_rgb(image2)64 65    # Clasificación documento (solo añade info)66    doc1 = classify_document(np1)67 68    face_app = get_face_app()69 70    # Detectar caras base (sin augment)71    faces1 = face_app.get(np1)72    faces2 = face_app.get(np2)73    f1 = pick_best_face(faces1)74    f2 = pick_best_face(faces2)75 76    if f1 is None or f2 is None:77        raise HTTPException(status_code=422, detail="No se detectó rostro en una de las imágenes.")78 79    # BBoxes80    bbox1 = to_bbox_dict(f1)81    bbox2_xyxy = f2.bbox  # para quality82    bbox1_xyxy = f1.bbox83 84    # Recorte para UI (img1)85    face1_crop = crop_face(np1, bbox1)86    _, face1_data_uri = img_to_base64_jpeg(face1_crop)87 88    # Quality gate (ambas)89    q1 = assess_quality(90        np1, bbox1_xyxy,91        min_face_area_ratio=settings.min_face_area_ratio,92        blur_var_min=settings.blur_var_min,93        brightness_min=settings.brightness_min,94        brightness_max=settings.brightness_max,95    )96    q2 = assess_quality(97        np2, bbox2_xyxy,98        min_face_area_ratio=settings.min_face_area_ratio,99        blur_var_min=settings.blur_var_min,100        brightness_min=settings.brightness_min,101        brightness_max=settings.brightness_max,102    )103 104    # Embeddings base105    emb1 = f1.normed_embedding106    emb2 = f2.normed_embedding107    base_sim = cosine_similarity(emb1, emb2)108 109    # Multi-sample similarity (jitter)110    sims = [base_sim]111    if settings.multi_samples and settings.multi_samples > 1:112        # Nota: re-ejecuta detector+embedding sobre variantes: más costo pero mejora robustez113        for i in range(settings.multi_samples - 1):114            np1_aug = augment_rgb(np1, seed=1000 + i)115            np2_aug = augment_rgb(np2, seed=2000 + i)116 117            fa1 = pick_best_face(face_app.get(np1_aug))118            fa2 = pick_best_face(face_app.get(np2_aug))119            if fa1 is None or fa2 is None:120                continue121 122            s = cosine_similarity(fa1.normed_embedding, fa2.normed_embedding)123            sims.append(s)124 125    # Score final126    raw_sim = max(sims) if settings.use_best_of_samples else float(np.median(np.array(sims)))127 128    # Porcentaje UI-friendly (sigmoid)129    similarity_percent = similarity_to_percent_sigmoid(raw_sim, settings.score_mid, settings.score_k)130 131    # Decision132    threshold = settings.sim_threshold133    if not (q1.ok and q2.ok):134        decision = "low_quality"135        is_match = False136    else:137        is_match = raw_sim >= threshold138        decision = "match" if is_match else "no_match"139 140    return {141        "tolerance": float(threshold),142        "distance": round(1.0 - float(raw_sim), 6),143        "is_match": bool(is_match),144        "similarity_percent": float(similarity_percent),145        "raw_similarity": round(float(raw_sim), 6),146        "decision": decision,147        "samples_used": int(len(sims)),148        "samples": [round(float(x), 6) for x in sims],149 150        "image1_bbox": bbox1,151        "image1_face_data_uri": face1_data_uri,152 153        "document1": doc1,154 155        "quality1": {156            "ok": q1.ok,157            "blur_var": round(q1.blur_var, 2),158            "brightness_mean": round(q1.brightness_mean, 2),159            "face_area_ratio": round(q1.face_area_ratio, 6),160            "signals": q1.signals,161        },162        "quality2": {163            "ok": q2.ok,164            "blur_var": round(q2.blur_var, 2),165            "brightness_mean": round(q2.brightness_mean, 2),166            "face_area_ratio": round(q2.face_area_ratio, 6),167            "signals": q2.signals,168        },169    }170