File size: 11,016 Bytes
557665b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
"""

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)