VeuReu commited on
Commit
557665b
·
verified ·
1 Parent(s): c73374c

Upload 3 files

Browse files
Files changed (1) hide show
  1. face_classifier.py +260 -215
face_classifier.py CHANGED
@@ -1,215 +1,260 @@
1
- """
2
- Face Classifier Module
3
- Valida caras y detecta género usando DeepFace para filtrar falsos positivos
4
- y asignar nombres automáticos según el género detectado.
5
- """
6
- import logging
7
- from typing import Optional, Dict, Any
8
-
9
- logging.basicConfig(level=logging.INFO)
10
- logger = logging.getLogger(__name__)
11
-
12
- # Configuración de thresholds
13
- # FACE_CONFIDENCE_THRESHOLD: Confianza mínima para aceptar una cara
14
- # Valores: 0.3 = permisivo (acepta muchos falsos positivos)
15
- # 0.6 = balanceado
16
- # 0.8 = estricto (elimina falsos positivos pero puede perder caras reales)
17
- # 0.85 = MUY estricto (solo caras muy claras)
18
- FACE_CONFIDENCE_THRESHOLD = 0.85 # MUY ESTRICTO: eliminar camisetas, letreros, etc.
19
- GENDER_NEUTRAL_THRESHOLD = 0.2 # Diferencia mínima para género neutro
20
-
21
-
22
- def validate_and_classify_face(image_path: str) -> Optional[Dict[str, Any]]:
23
- """
24
- Valida si és una cara real i detecta el gènere usant DeepFace.
25
-
26
- Args:
27
- image_path: Ruta a la imagen de la cara
28
-
29
- Returns:
30
- Dict amb: {
31
- 'is_valid_face': bool, # True si és una cara amb confiança alta
32
- 'face_confidence': float, # Score de detecció de cara (0-1)
33
- 'gender': 'Man' | 'Woman' | 'Neutral',
34
- 'gender_confidence': float, # Score de confiança del gènere (0-1)
35
- 'man_prob': float,
36
- 'woman_prob': float
37
- }
38
- o None si falla completament
39
- """
40
- try:
41
- import cv2
42
- import numpy as np
43
- from deepface import DeepFace
44
-
45
- print(f"[DeepFace] Analitzant: {image_path}")
46
-
47
- # PREPROCESAMIENTO: Normalizar iluminación y mejorar contraste
48
- # Esto reduce el impacto de luz/oscuridad en la detección
49
- img = cv2.imread(str(image_path))
50
- if img is None:
51
- print(f"[DeepFace] No se pudo cargar la imagen: {image_path}")
52
- return None
53
-
54
- # Convertir a escala de grises (más robusto para detección)
55
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
56
-
57
- # CLAHE: Adaptive Histogram Equalization
58
- # Normaliza el contraste de forma local, reduciendo efectos de luz
59
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
60
- normalized = clahe.apply(gray)
61
-
62
- # Volver a BGR para DeepFace
63
- normalized_bgr = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
64
-
65
- # Guardar imagen preprocesada temporalmente
66
- import tempfile
67
- import os
68
- temp_dir = tempfile.gettempdir()
69
- temp_path = os.path.join(temp_dir, f"normalized_{os.path.basename(image_path)}")
70
- cv2.imwrite(temp_path, normalized_bgr)
71
-
72
- print(f"[DeepFace] Imagen preprocesada con CLAHE: {temp_path}")
73
-
74
- # Analitzar gènere amb detecció de cara (usando imagen normalizada)
75
- result = DeepFace.analyze(
76
- img_path=temp_path,
77
- actions=['gender'],
78
- enforce_detection=True, # Intentar detectar cara
79
- detector_backend='opencv',
80
- silent=True
81
- )
82
-
83
- # Limpiar archivo temporal
84
- try:
85
- os.remove(temp_path)
86
- except:
87
- pass
88
-
89
- # DeepFace pot retornar llista si detecta múltiples cares
90
- if isinstance(result, list):
91
- print(f"[DeepFace] Resultado es lista con {len(result)} elementos")
92
- result = result[0] if result else None
93
-
94
- if not result:
95
- print(f"[DeepFace] No s'ha detectat cap cara")
96
- return {
97
- 'is_valid_face': False,
98
- 'face_confidence': 0.0,
99
- 'gender': 'Neutral',
100
- 'gender_confidence': 0.0,
101
- 'man_prob': 0.0,
102
- 'woman_prob': 0.0
103
- }
104
-
105
- # LOG: Ver estructura completa del resultado
106
- print(f"[DeepFace] Resultado completo de analyze: {result}")
107
-
108
- # Extreure informació de gènere
109
- gender_info = result.get('gender', {})
110
- print(f"[DeepFace] gender_info type: {type(gender_info)}, value: {gender_info}")
111
-
112
- if isinstance(gender_info, dict):
113
- # DeepFace retorna percentatges, convertir a 0-1
114
- man_prob = gender_info.get('Man', 0) / 100.0
115
- woman_prob = gender_info.get('Woman', 0) / 100.0
116
- print(f"[DeepFace] Extraído de dict - Man: {man_prob:.3f}, Woman: {woman_prob:.3f}")
117
- else:
118
- # Fallback si el format és diferent
119
- print(f"[DeepFace] gender_info NO es dict, usando fallback 0.5/0.5")
120
- man_prob = 0.5
121
- woman_prob = 0.5
122
-
123
- # Determinar gènere basat en les probabilitats
124
- gender_diff = abs(man_prob - woman_prob)
125
-
126
- print(f"[DeepFace] Diferencia Man-Woman: {gender_diff:.3f} (threshold neutral={GENDER_NEUTRAL_THRESHOLD})")
127
-
128
- # Si la diferència és petita (< threshold), considerar neutre
129
- if gender_diff < GENDER_NEUTRAL_THRESHOLD:
130
- gender = 'Neutral'
131
- gender_confidence = 0.5
132
- print(f"[DeepFace] → Asignado NEUTRAL (diferencia {gender_diff:.3f} < {GENDER_NEUTRAL_THRESHOLD})")
133
- else:
134
- gender = 'Man' if man_prob > woman_prob else 'Woman'
135
- gender_confidence = max(man_prob, woman_prob)
136
- print(f"[DeepFace] Asignado {gender.upper()} (man_prob={man_prob:.3f}, woman_prob={woman_prob:.3f})")
137
-
138
- # Confiança de detecció de cara
139
- # DeepFace no proporciona score directamente en analyze(), pero si retornó resultado
140
- # asumimos que es cara válida con confianza alta
141
- face_confidence = result.get('face_confidence', 0.9) # Default alto si detecta
142
-
143
- # Si DeepFace va retornar resultat, assumir que és cara vàlida
144
- is_valid_face = True
145
-
146
- print(f"[DeepFace] ===== RESUMEN FINAL =====")
147
- print(f"[DeepFace] is_valid_face: {is_valid_face}")
148
- print(f"[DeepFace] face_confidence: {face_confidence:.3f}")
149
- print(f"[DeepFace] gender: {gender}")
150
- print(f"[DeepFace] gender_confidence: {gender_confidence:.3f}")
151
- print(f"[DeepFace] man_prob: {man_prob:.3f}")
152
- print(f"[DeepFace] woman_prob: {woman_prob:.3f}")
153
- print(f"[DeepFace] ==========================")
154
-
155
- return {
156
- 'is_valid_face': is_valid_face,
157
- 'face_confidence': face_confidence,
158
- 'gender': gender,
159
- 'gender_confidence': gender_confidence,
160
- 'man_prob': man_prob,
161
- 'woman_prob': woman_prob
162
- }
163
-
164
- except ValueError as e:
165
- # ValueError significa que no es va detectar cara
166
- print(f"[DeepFace] No s'ha detectat cara (ValueError): {e}")
167
- return {
168
- 'is_valid_face': False,
169
- 'face_confidence': 0.0,
170
- 'gender': 'Neutral',
171
- 'gender_confidence': 0.0,
172
- 'man_prob': 0.0,
173
- 'woman_prob': 0.0
174
- }
175
- except Exception as e:
176
- print(f"[DeepFace] Error validant cara: {e}")
177
- return None
178
-
179
-
180
- def get_random_catalan_name_by_gender(gender: str, seed_value: str = "") -> str:
181
- """
182
- Genera un nom català aleatori basat en el gènere.
183
-
184
- Args:
185
- gender: 'Man', 'Woman', o 'Neutral'
186
- seed_value: Valor per fer el random determinista (opcional)
187
-
188
- Returns:
189
- Nom català
190
- """
191
- noms_home = [
192
- "Jordi", "Marc", "Pau", "Pere", "Joan", "Josep", "David", "Guillem", "Albert",
193
- "Arnau", "Martí", "Bernat", "Oriol", "Roger", "Pol", "Lluís", "Sergi", "Carles", "Xavier"
194
- ]
195
- noms_dona = [
196
- "Maria", "Anna", "Laura", "Marta", "Cristina", "Núria", "Montserrat", "Júlia", "Sara", "Carla",
197
- "Alba", "Elisabet", "Rosa", "Gemma", "Sílvia", "Teresa", "Irene", "Laia", "Marina", "Bet"
198
- ]
199
- noms_neutre = ["Àlex", "Andrea", "Francis", "Cris", "Noa"]
200
-
201
- # Seleccionar llista segons gènere
202
- if gender == 'Woman':
203
- noms = noms_dona
204
- elif gender == 'Man':
205
- noms = noms_home
206
- else: # Neutral
207
- noms = noms_neutre
208
-
209
- # Usar hash del seed per seleccionar nom de forma determinista
210
- if seed_value:
211
- hash_val = hash(seed_value)
212
- return noms[abs(hash_val) % len(noms)]
213
- else:
214
- import random
215
- return random.choice(noms)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Face Classifier Module
3
+ Valida caras y detecta género usando DeepFace para filtrar falsos positivos
4
+ y asignar nombres automáticos según el género detectado.
5
+ """
6
+ import logging
7
+ from typing import Optional, Dict, Any
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Configuración de thresholds
13
+ # FACE_CONFIDENCE_THRESHOLD: Confianza mínima para aceptar una cara
14
+ # Valores: 0.5 = permisivo (acepta muchos falsos positivos)
15
+ # 0.7 = balanceado
16
+ # 0.85 = estricto (elimina falsos positivos pero puede perder caras reales)
17
+ # 0.92 = MUY estricto (solo caras muy claras)
18
+ FACE_CONFIDENCE_THRESHOLD = 0.92 # MUY ESTRICTO: preferir perder caras a tener falsos positivos
19
+ GENDER_NEUTRAL_THRESHOLD = 0.2 # Diferencia mínima para género neutro
20
+
21
+ # Configuración adicional de filtrado
22
+ MIN_FACE_SIZE_PIXELS = 48 # Tamaño mínimo de cara en píxeles (ancho o alto)
23
+ MAX_ASPECT_RATIO = 2.0 # Proporción máxima ancho/alto (caras reales ~0.7-1.3)
24
+ MIN_ASPECT_RATIO = 0.5 # Proporción mínima ancho/alto
25
+
26
+
27
+ def validate_and_classify_face(image_path: str) -> Optional[Dict[str, Any]]:
28
+ """
29
+ Valida si és una cara real i detecta el gènere usant DeepFace.
30
+ Usa extract_faces() para obtener score de confianza REAL de detección.
31
+
32
+ Args:
33
+ image_path: Ruta a la imagen de la cara
34
+
35
+ Returns:
36
+ Dict amb: {
37
+ 'is_valid_face': bool, # True si és una cara amb confiança alta
38
+ 'face_confidence': float, # Score de detecció de cara (0-1)
39
+ 'gender': 'Man' | 'Woman' | 'Neutral',
40
+ 'gender_confidence': float, # Score de confiança del gènere (0-1)
41
+ 'man_prob': float,
42
+ 'woman_prob': float,
43
+ 'rejection_reason': str | None # Motivo de rechazo si is_valid_face=False
44
+ }
45
+ o None si falla completament
46
+ """
47
+ try:
48
+ import cv2
49
+ import numpy as np
50
+ from deepface import DeepFace
51
+
52
+ print(f"[DeepFace] Analitzant: {image_path}")
53
+
54
+ # Cargar imagen para verificaciones de tamaño
55
+ img = cv2.imread(str(image_path))
56
+ if img is None:
57
+ print(f"[DeepFace] No se pudo cargar la imagen: {image_path}")
58
+ return None
59
+
60
+ img_height, img_width = img.shape[:2]
61
+ print(f"[DeepFace] Tamaño imagen: {img_width}x{img_height}")
62
+
63
+ # VERIFICACIÓN 1: Tamaño mínimo de imagen
64
+ if img_width < MIN_FACE_SIZE_PIXELS or img_height < MIN_FACE_SIZE_PIXELS:
65
+ print(f"[DeepFace] RECHAZADA: Imagen demasiado pequeña ({img_width}x{img_height} < {MIN_FACE_SIZE_PIXELS}px)")
66
+ return {
67
+ 'is_valid_face': False,
68
+ 'face_confidence': 0.0,
69
+ 'gender': 'Neutral',
70
+ 'gender_confidence': 0.0,
71
+ 'man_prob': 0.0,
72
+ 'woman_prob': 0.0,
73
+ 'rejection_reason': f'Imagen demasiado pequeña ({img_width}x{img_height})'
74
+ }
75
+
76
+ # VERIFICACIÓN 2: Proporción de la imagen (caras reales tienen proporción ~0.7-1.3)
77
+ aspect_ratio = img_width / img_height
78
+ if aspect_ratio > MAX_ASPECT_RATIO or aspect_ratio < MIN_ASPECT_RATIO:
79
+ print(f"[DeepFace] ✗ RECHAZADA: Proporción anómala ({aspect_ratio:.2f}, esperado {MIN_ASPECT_RATIO}-{MAX_ASPECT_RATIO})")
80
+ return {
81
+ 'is_valid_face': False,
82
+ 'face_confidence': 0.0,
83
+ 'gender': 'Neutral',
84
+ 'gender_confidence': 0.0,
85
+ 'man_prob': 0.0,
86
+ 'woman_prob': 0.0,
87
+ 'rejection_reason': f'Proporción anómala ({aspect_ratio:.2f})'
88
+ }
89
+
90
+ # PASO 1: Usar extract_faces() para obtener score de confianza REAL
91
+ # Este es el método correcto para obtener el confidence score de detección
92
+ print(f"[DeepFace] Ejecutando extract_faces() para obtener confidence score...")
93
+
94
+ try:
95
+ faces = DeepFace.extract_faces(
96
+ img_path=str(image_path),
97
+ detector_backend='retinaface', # Más preciso que opencv
98
+ enforce_detection=True,
99
+ align=True
100
+ )
101
+
102
+ if not faces:
103
+ print(f"[DeepFace] ✗ extract_faces() no detectó ninguna cara")
104
+ return {
105
+ 'is_valid_face': False,
106
+ 'face_confidence': 0.0,
107
+ 'gender': 'Neutral',
108
+ 'gender_confidence': 0.0,
109
+ 'man_prob': 0.0,
110
+ 'woman_prob': 0.0,
111
+ 'rejection_reason': 'No se detectó cara con retinaface'
112
+ }
113
+
114
+ # Tomar la cara con mayor confianza
115
+ best_face = max(faces, key=lambda f: f.get('confidence', 0))
116
+ face_confidence = best_face.get('confidence', 0.0)
117
+ facial_area = best_face.get('facial_area', {})
118
+
119
+ print(f"[DeepFace] extract_faces() encontró {len(faces)} cara(s)")
120
+ print(f"[DeepFace] Mejor cara - confidence: {face_confidence:.4f}")
121
+ print(f"[DeepFace] Facial area: {facial_area}")
122
+
123
+ # VERIFICACIÓN 3: Score de confianza de detección
124
+ if face_confidence < FACE_CONFIDENCE_THRESHOLD:
125
+ print(f"[DeepFace] ✗ RECHAZADA: Confianza baja ({face_confidence:.4f} < {FACE_CONFIDENCE_THRESHOLD})")
126
+ return {
127
+ 'is_valid_face': False,
128
+ 'face_confidence': face_confidence,
129
+ 'gender': 'Neutral',
130
+ 'gender_confidence': 0.0,
131
+ 'man_prob': 0.0,
132
+ 'woman_prob': 0.0,
133
+ 'rejection_reason': f'Confianza baja ({face_confidence:.4f})'
134
+ }
135
+
136
+ # VERIFICACIÓN 4: Tamaño del área facial detectada
137
+ face_w = facial_area.get('w', 0)
138
+ face_h = facial_area.get('h', 0)
139
+ if face_w < MIN_FACE_SIZE_PIXELS * 0.5 or face_h < MIN_FACE_SIZE_PIXELS * 0.5:
140
+ print(f"[DeepFace] RECHAZADA: Área facial muy pequeña ({face_w}x{face_h})")
141
+ return {
142
+ 'is_valid_face': False,
143
+ 'face_confidence': face_confidence,
144
+ 'gender': 'Neutral',
145
+ 'gender_confidence': 0.0,
146
+ 'man_prob': 0.0,
147
+ 'woman_prob': 0.0,
148
+ 'rejection_reason': f'Área facial muy pequeña ({face_w}x{face_h})'
149
+ }
150
+
151
+ except ValueError as e:
152
+ print(f"[DeepFace] ✗ extract_faces() ValueError: {e}")
153
+ return {
154
+ 'is_valid_face': False,
155
+ 'face_confidence': 0.0,
156
+ 'gender': 'Neutral',
157
+ 'gender_confidence': 0.0,
158
+ 'man_prob': 0.0,
159
+ 'woman_prob': 0.0,
160
+ 'rejection_reason': f'extract_faces falló: {e}'
161
+ }
162
+
163
+ # PASO 2: Analizar género (solo si pasó las verificaciones anteriores)
164
+ print(f"[DeepFace] Cara válida, analizando género...")
165
+
166
+ try:
167
+ result = DeepFace.analyze(
168
+ img_path=str(image_path),
169
+ actions=['gender'],
170
+ enforce_detection=False, # Ya verificamos que hay cara
171
+ detector_backend='skip', # Saltar detección, ya la hicimos
172
+ silent=True
173
+ )
174
+
175
+ if isinstance(result, list):
176
+ result = result[0] if result else {}
177
+
178
+ gender_info = result.get('gender', {})
179
+
180
+ if isinstance(gender_info, dict):
181
+ man_prob = gender_info.get('Man', 0) / 100.0
182
+ woman_prob = gender_info.get('Woman', 0) / 100.0
183
+ else:
184
+ man_prob = 0.5
185
+ woman_prob = 0.5
186
+
187
+ gender_diff = abs(man_prob - woman_prob)
188
+
189
+ if gender_diff < GENDER_NEUTRAL_THRESHOLD:
190
+ gender = 'Neutral'
191
+ gender_confidence = 0.5
192
+ else:
193
+ gender = 'Man' if man_prob > woman_prob else 'Woman'
194
+ gender_confidence = max(man_prob, woman_prob)
195
+
196
+ except Exception as e:
197
+ print(f"[DeepFace] Análisis de género falló: {e}, usando valores neutros")
198
+ gender = 'Neutral'
199
+ gender_confidence = 0.5
200
+ man_prob = 0.5
201
+ woman_prob = 0.5
202
+
203
+ print(f"[DeepFace] ===== RESUMEN FINAL =====")
204
+ print(f"[DeepFace] ✓ is_valid_face: True")
205
+ print(f"[DeepFace] face_confidence: {face_confidence:.4f} (threshold: {FACE_CONFIDENCE_THRESHOLD})")
206
+ print(f"[DeepFace] gender: {gender}")
207
+ print(f"[DeepFace] gender_confidence: {gender_confidence:.3f}")
208
+ print(f"[DeepFace] ==========================")
209
+
210
+ return {
211
+ 'is_valid_face': True,
212
+ 'face_confidence': face_confidence,
213
+ 'gender': gender,
214
+ 'gender_confidence': gender_confidence,
215
+ 'man_prob': man_prob,
216
+ 'woman_prob': woman_prob,
217
+ 'rejection_reason': None
218
+ }
219
+
220
+ except Exception as e:
221
+ print(f"[DeepFace] Error validant cara: {e}")
222
+ return None
223
+
224
+
225
+ def get_random_catalan_name_by_gender(gender: str, seed_value: str = "") -> str:
226
+ """
227
+ Genera un nom català aleatori basat en el gènere.
228
+
229
+ Args:
230
+ gender: 'Man', 'Woman', o 'Neutral'
231
+ seed_value: Valor per fer el random determinista (opcional)
232
+
233
+ Returns:
234
+ Nom català
235
+ """
236
+ noms_home = [
237
+ "Jordi", "Marc", "Pau", "Pere", "Joan", "Josep", "David", "Guillem", "Albert",
238
+ "Arnau", "Martí", "Bernat", "Oriol", "Roger", "Pol", "Lluís", "Sergi", "Carles", "Xavier"
239
+ ]
240
+ noms_dona = [
241
+ "Maria", "Anna", "Laura", "Marta", "Cristina", "Núria", "Montserrat", "Júlia", "Sara", "Carla",
242
+ "Alba", "Elisabet", "Rosa", "Gemma", "Sílvia", "Teresa", "Irene", "Laia", "Marina", "Bet"
243
+ ]
244
+ noms_neutre = ["Àlex", "Andrea", "Francis", "Cris", "Noa"]
245
+
246
+ # Seleccionar llista segons gènere
247
+ if gender == 'Woman':
248
+ noms = noms_dona
249
+ elif gender == 'Man':
250
+ noms = noms_home
251
+ else: # Neutral
252
+ noms = noms_neutre
253
+
254
+ # Usar hash del seed per seleccionar nom de forma determinista
255
+ if seed_value:
256
+ hash_val = hash(seed_value)
257
+ return noms[abs(hash_val) % len(noms)]
258
+ else:
259
+ import random
260
+ return random.choice(noms)