engine / face_classifier.py
VeuReu's picture
Upload 3 files
557665b verified
raw
history blame
11 kB
"""
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)