|
|
"""
|
|
|
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__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FACE_CONFIDENCE_THRESHOLD = 0.92
|
|
|
GENDER_NEUTRAL_THRESHOLD = 0.2
|
|
|
|
|
|
|
|
|
MIN_FACE_SIZE_PIXELS = 48
|
|
|
MAX_ASPECT_RATIO = 2.0
|
|
|
MIN_ASPECT_RATIO = 0.5
|
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
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})'
|
|
|
}
|
|
|
|
|
|
|
|
|
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})'
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[DeepFace] Ejecutando extract_faces() para obtener confidence score...")
|
|
|
|
|
|
try:
|
|
|
faces = DeepFace.extract_faces(
|
|
|
img_path=str(image_path),
|
|
|
detector_backend='retinaface',
|
|
|
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'
|
|
|
}
|
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
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})'
|
|
|
}
|
|
|
|
|
|
|
|
|
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}'
|
|
|
}
|
|
|
|
|
|
|
|
|
print(f"[DeepFace] ✓ Cara válida, analizando género...")
|
|
|
|
|
|
try:
|
|
|
result = DeepFace.analyze(
|
|
|
img_path=str(image_path),
|
|
|
actions=['gender'],
|
|
|
enforce_detection=False,
|
|
|
detector_backend='skip',
|
|
|
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"]
|
|
|
|
|
|
|
|
|
if gender == 'Woman':
|
|
|
noms = noms_dona
|
|
|
elif gender == 'Man':
|
|
|
noms = noms_home
|
|
|
else:
|
|
|
noms = noms_neutre
|
|
|
|
|
|
|
|
|
if seed_value:
|
|
|
hash_val = hash(seed_value)
|
|
|
return noms[abs(hash_val) % len(noms)]
|
|
|
else:
|
|
|
import random
|
|
|
return random.choice(noms)
|
|
|
|