""" Face Classifier Module Valida caras y detecta género usando DeepFace para filtrar falsos positivos y asignar nombres automáticos según el género detectado. """ import logging from typing import Optional, Dict, Any logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Configuración de thresholds # FACE_CONFIDENCE_THRESHOLD: Confianza mínima para aceptar una cara # Valores: 0.5 = permisivo (acepta muchos falsos positivos) # 0.7 = balanceado # 0.85 = estricto (elimina falsos positivos pero puede perder caras reales) # 0.92 = MUY estricto (solo caras muy claras) FACE_CONFIDENCE_THRESHOLD = 0.92 # MUY ESTRICTO: preferir perder caras a tener falsos positivos GENDER_NEUTRAL_THRESHOLD = 0.2 # Diferencia mínima para género neutro # Configuración adicional de filtrado MIN_FACE_SIZE_PIXELS = 48 # Tamaño mínimo de cara en píxeles (ancho o alto) MAX_ASPECT_RATIO = 2.0 # Proporción máxima ancho/alto (caras reales ~0.7-1.3) MIN_ASPECT_RATIO = 0.5 # Proporción mínima ancho/alto def validate_and_classify_face(image_path: str) -> Optional[Dict[str, Any]]: """ Valida si és una cara real i detecta el gènere usant DeepFace. Usa extract_faces() para obtener score de confianza REAL de detección. Args: image_path: Ruta a la imagen de la cara Returns: Dict amb: { 'is_valid_face': bool, # True si és una cara amb confiança alta 'face_confidence': float, # Score de detecció de cara (0-1) 'gender': 'Man' | 'Woman' | 'Neutral', 'gender_confidence': float, # Score de confiança del gènere (0-1) 'man_prob': float, 'woman_prob': float, 'rejection_reason': str | None # Motivo de rechazo si is_valid_face=False } o None si falla completament """ try: import cv2 import numpy as np from deepface import DeepFace print(f"[DeepFace] Analitzant: {image_path}") # Cargar imagen para verificaciones de tamaño img = cv2.imread(str(image_path)) if img is None: print(f"[DeepFace] No se pudo cargar la imagen: {image_path}") return None img_height, img_width = img.shape[:2] print(f"[DeepFace] Tamaño imagen: {img_width}x{img_height}") # VERIFICACIÓN 1: Tamaño mínimo de imagen if img_width < MIN_FACE_SIZE_PIXELS or img_height < MIN_FACE_SIZE_PIXELS: print(f"[DeepFace] ✗ RECHAZADA: Imagen demasiado pequeña ({img_width}x{img_height} < {MIN_FACE_SIZE_PIXELS}px)") return { 'is_valid_face': False, 'face_confidence': 0.0, 'gender': 'Neutral', 'gender_confidence': 0.0, 'man_prob': 0.0, 'woman_prob': 0.0, 'rejection_reason': f'Imagen demasiado pequeña ({img_width}x{img_height})' } # VERIFICACIÓN 2: Proporción de la imagen (caras reales tienen proporción ~0.7-1.3) aspect_ratio = img_width / img_height if aspect_ratio > MAX_ASPECT_RATIO or aspect_ratio < MIN_ASPECT_RATIO: print(f"[DeepFace] ✗ RECHAZADA: Proporción anómala ({aspect_ratio:.2f}, esperado {MIN_ASPECT_RATIO}-{MAX_ASPECT_RATIO})") return { 'is_valid_face': False, 'face_confidence': 0.0, 'gender': 'Neutral', 'gender_confidence': 0.0, 'man_prob': 0.0, 'woman_prob': 0.0, 'rejection_reason': f'Proporción anómala ({aspect_ratio:.2f})' } # PASO 1: Usar extract_faces() para obtener score de confianza REAL # Este es el método correcto para obtener el confidence score de detección print(f"[DeepFace] Ejecutando extract_faces() para obtener confidence score...") try: faces = DeepFace.extract_faces( img_path=str(image_path), detector_backend='retinaface', # Más preciso que opencv enforce_detection=True, align=True ) if not faces: print(f"[DeepFace] ✗ extract_faces() no detectó ninguna cara") return { 'is_valid_face': False, 'face_confidence': 0.0, 'gender': 'Neutral', 'gender_confidence': 0.0, 'man_prob': 0.0, 'woman_prob': 0.0, 'rejection_reason': 'No se detectó cara con retinaface' } # Tomar la cara con mayor confianza best_face = max(faces, key=lambda f: f.get('confidence', 0)) face_confidence = best_face.get('confidence', 0.0) facial_area = best_face.get('facial_area', {}) print(f"[DeepFace] extract_faces() encontró {len(faces)} cara(s)") print(f"[DeepFace] Mejor cara - confidence: {face_confidence:.4f}") print(f"[DeepFace] Facial area: {facial_area}") # VERIFICACIÓN 3: Score de confianza de detección if face_confidence < FACE_CONFIDENCE_THRESHOLD: print(f"[DeepFace] ✗ RECHAZADA: Confianza baja ({face_confidence:.4f} < {FACE_CONFIDENCE_THRESHOLD})") return { 'is_valid_face': False, 'face_confidence': face_confidence, 'gender': 'Neutral', 'gender_confidence': 0.0, 'man_prob': 0.0, 'woman_prob': 0.0, 'rejection_reason': f'Confianza baja ({face_confidence:.4f})' } # VERIFICACIÓN 4: Tamaño del área facial detectada face_w = facial_area.get('w', 0) face_h = facial_area.get('h', 0) if face_w < MIN_FACE_SIZE_PIXELS * 0.5 or face_h < MIN_FACE_SIZE_PIXELS * 0.5: print(f"[DeepFace] ✗ RECHAZADA: Área facial muy pequeña ({face_w}x{face_h})") return { 'is_valid_face': False, 'face_confidence': face_confidence, 'gender': 'Neutral', 'gender_confidence': 0.0, 'man_prob': 0.0, 'woman_prob': 0.0, 'rejection_reason': f'Área facial muy pequeña ({face_w}x{face_h})' } except ValueError as e: print(f"[DeepFace] ✗ extract_faces() ValueError: {e}") return { 'is_valid_face': False, 'face_confidence': 0.0, 'gender': 'Neutral', 'gender_confidence': 0.0, 'man_prob': 0.0, 'woman_prob': 0.0, 'rejection_reason': f'extract_faces falló: {e}' } # PASO 2: Analizar género (solo si pasó las verificaciones anteriores) print(f"[DeepFace] ✓ Cara válida, analizando género...") try: result = DeepFace.analyze( img_path=str(image_path), actions=['gender'], enforce_detection=False, # Ya verificamos que hay cara detector_backend='skip', # Saltar detección, ya la hicimos silent=True ) if isinstance(result, list): result = result[0] if result else {} gender_info = result.get('gender', {}) if isinstance(gender_info, dict): man_prob = gender_info.get('Man', 0) / 100.0 woman_prob = gender_info.get('Woman', 0) / 100.0 else: man_prob = 0.5 woman_prob = 0.5 gender_diff = abs(man_prob - woman_prob) if gender_diff < GENDER_NEUTRAL_THRESHOLD: gender = 'Neutral' gender_confidence = 0.5 else: gender = 'Man' if man_prob > woman_prob else 'Woman' gender_confidence = max(man_prob, woman_prob) except Exception as e: print(f"[DeepFace] Análisis de género falló: {e}, usando valores neutros") gender = 'Neutral' gender_confidence = 0.5 man_prob = 0.5 woman_prob = 0.5 print(f"[DeepFace] ===== RESUMEN FINAL =====") print(f"[DeepFace] ✓ is_valid_face: True") print(f"[DeepFace] face_confidence: {face_confidence:.4f} (threshold: {FACE_CONFIDENCE_THRESHOLD})") print(f"[DeepFace] gender: {gender}") print(f"[DeepFace] gender_confidence: {gender_confidence:.3f}") print(f"[DeepFace] ==========================") return { 'is_valid_face': True, 'face_confidence': face_confidence, 'gender': gender, 'gender_confidence': gender_confidence, 'man_prob': man_prob, 'woman_prob': woman_prob, 'rejection_reason': None } except Exception as e: print(f"[DeepFace] Error validant cara: {e}") return None def get_random_catalan_name_by_gender(gender: str, seed_value: str = "") -> str: """ Genera un nom català aleatori basat en el gènere. Args: gender: 'Man', 'Woman', o 'Neutral' seed_value: Valor per fer el random determinista (opcional) Returns: Nom català """ noms_home = [ "Jordi", "Marc", "Pau", "Pere", "Joan", "Josep", "David", "Guillem", "Albert", "Arnau", "Martí", "Bernat", "Oriol", "Roger", "Pol", "Lluís", "Sergi", "Carles", "Xavier" ] noms_dona = [ "Maria", "Anna", "Laura", "Marta", "Cristina", "Núria", "Montserrat", "Júlia", "Sara", "Carla", "Alba", "Elisabet", "Rosa", "Gemma", "Sílvia", "Teresa", "Irene", "Laia", "Marina", "Bet" ] noms_neutre = ["Àlex", "Andrea", "Francis", "Cris", "Noa"] # Seleccionar llista segons gènere if gender == 'Woman': noms = noms_dona elif gender == 'Man': noms = noms_home else: # Neutral noms = noms_neutre # Usar hash del seed per seleccionar nom de forma determinista if seed_value: hash_val = hash(seed_value) return noms[abs(hash_val) % len(noms)] else: import random return random.choice(noms)