AI-future commited on
Commit
c2e2e4e
·
verified ·
1 Parent(s): 0a0344f

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +423 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import gradio as gr
3
+ import numpy as np
4
+ import cv2
5
+ from deepface import DeepFace
6
+ import json
7
+ import time
8
+ import os
9
+
10
+ # Ensure OpenCV finds the Haar Cascade file
11
+ # This might be needed if the default path isn't automatically found in the HF environment
12
+ cv2_base_dir = os.path.dirname(os.path.abspath(cv2.__file__))
13
+ haar_cascade_path = os.path.join(cv2_base_dir, 'data', 'haarcascade_frontalface_default.xml')
14
+
15
+ # Check if the cascade file exists, raise error if not
16
+ if not os.path.isfile(haar_cascade_path):
17
+ # Attempt backup location if primary fails (less likely needed but safe)
18
+ backup_path = "/home/user/.local/lib/python3.x/site-packages/cv2/data/haarcascade_frontalface_default.xml" # Adjust python version if needed
19
+ if os.path.isfile(backup_path):
20
+ haar_cascade_path = backup_path
21
+ else:
22
+ raise RuntimeError(f"Could not find Haar Cascade file at expected locations: {haar_cascade_path} or backup paths.")
23
+
24
+
25
+ EMBEDDINGS_FILE = "stored_embeddings.json"
26
+
27
+ def load_embeddings():
28
+ """Load stored face embeddings (expects list of lists)."""
29
+ try:
30
+ with open(EMBEDDINGS_FILE, "r") as f:
31
+ embeddings_data = json.load(f)
32
+ # --- CRITICAL BUG FIX AREA (See Analysis) ---
33
+ # Assuming the file SHOULD contain the structure saved by register_face:
34
+ # a list of dictionaries like [{"embedding": [...], "name": "...", "timestamp": "..."}]
35
+ # We need to extract just the embedding lists for the current logic.
36
+ embeddings = [np.array(item["embedding"]) for item in embeddings_data if "embedding" in item]
37
+ return embeddings
38
+ except (FileNotFoundError, json.JSONDecodeError):
39
+ return []
40
+ except Exception as e:
41
+ print(f"Error loading embeddings: {e}. Assuming empty list.") # Add more logging
42
+ return []
43
+
44
+
45
+ # def save_embeddings(embeddings): # This function is defined but never used
46
+ # with open(EMBEDDINGS_FILE, "w") as f:
47
+ # json.dump([embedding.tolist() for embedding in embeddings], f)
48
+
49
+ # Load embeddings when the script starts
50
+ stored_embeddings = load_embeddings()
51
+ print(f"Loaded {len(stored_embeddings)} embeddings on startup.") # Add logging
52
+
53
+ def extract_face_embedding_from_frame(frame):
54
+ """Extract a facial embedding from a single frame (image)."""
55
+ # Use the validated haar_cascade_path
56
+ face_cascade = cv2.CascadeClassifier(haar_cascade_path)
57
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
58
+ faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
59
+
60
+ if len(faces) > 0:
61
+ # Process only the first detected face
62
+ x, y, w, h = faces[0]
63
+ face = frame[y:y + h, x:x + w]
64
+
65
+ # Create a copy for marking to avoid modifying the original frame if needed elsewhere
66
+ marked_frame = frame.copy()
67
+ cv2.rectangle(marked_frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
68
+
69
+ # Use Facenet model for representation
70
+ # Ensure backend='auto' or specify one like 'opencv', 'ssd', 'mtcnn' if needed
71
+ # Error handling for representation is important
72
+ try:
73
+ embedding_objs = DeepFace.represent(face, model_name="Facenet", enforce_detection=False) # Don't re-detect
74
+ if not embedding_objs:
75
+ raise Exception("DeepFace could not generate embedding.")
76
+ embedding = embedding_objs[0]["embedding"] # Access the first item's embedding
77
+ return np.array(embedding), marked_frame, face
78
+ except Exception as represent_error:
79
+ raise Exception(f"Failed to generate embedding: {represent_error}")
80
+
81
+ # If no faces were detected by Haar Cascade
82
+ raise Exception("No face detected in the frame.")
83
+
84
+ def verify_face_from_webcam(video, save_embedding=False, name=""):
85
+ """Verifies face from video OR attempts registration if save_embedding is True."""
86
+ global stored_embeddings
87
+
88
+ if video is None:
89
+ # Return signature must match the Gradio outputs: image, string, image, number
90
+ return None, "Please record a video first.", None, 0
91
+
92
+ try:
93
+ cap = cv2.VideoCapture(video)
94
+ frames = []
95
+ frame_count = 0
96
+ while frame_count < 60: # Limit frames read to prevent memory issues
97
+ ret, frame = cap.read()
98
+ if not ret:
99
+ break
100
+ frames.append(frame)
101
+ frame_count += 1
102
+ cap.release()
103
+
104
+ if not frames:
105
+ # Return signature: image, string, image, number
106
+ return None, "Error: Could not read video.", None, 0
107
+
108
+ # Use the middle frame for analysis
109
+ frame = frames[len(frames)//2]
110
+
111
+ # Extract embedding and get marked frame + face crop
112
+ embedding, marked_frame, face = extract_face_embedding_from_frame(frame)
113
+
114
+ max_similarity = 0
115
+ is_match = False
116
+
117
+ # Compare against loaded embeddings
118
+ for stored_embedding in stored_embeddings:
119
+ # Cosine Similarity Calculation
120
+ similarity = np.dot(embedding, stored_embedding) / (np.linalg.norm(embedding) * np.linalg.norm(stored_embedding))
121
+ max_similarity = max(max_similarity, similarity)
122
+
123
+ # Verification threshold
124
+ if similarity > 0.7: # Hardcoded threshold
125
+ is_match = True
126
+ # If only verifying (save_embedding=False), return success
127
+ if not save_embedding:
128
+ # Return signature: image, string, image, number
129
+ return marked_frame, f"✅ **AUTHENTICATED!** Similarity score: {similarity:.2f}", face, similarity
130
+ # If attempting to save but already matched, return warning (prevents duplicates somewhat)
131
+ else:
132
+ # Return signature: image, string, image, number (use 0 for confidence here maybe?)
133
+ return marked_frame, f"⚠️ This face seems to be already in the database (Similarity: {similarity:.2f}). Registration aborted.", face, similarity
134
+
135
+
136
+ # --- Logic for the "save_embedding" flag from the Authentication tab ---
137
+ # This part is problematic because the Auth tab doesn't provide a name and
138
+ # the return signature expects 4 items, but registration logic only naturally provides 3.
139
+ if save_embedding:
140
+ # This code block within verify_face_from_webcam is likely UNREACHABLE
141
+ # or will error due to mismatched return values for the Gradio component expecting 4 outputs.
142
+ # It duplicates logic from `register_face` unnecessarily.
143
+ # It's better to remove this `save_embedding` logic entirely from the verification function.
144
+ # Keeping it here as per "do not change code" rule, but highlighting it as flawed.
145
+ print("WARNING: 'save_embedding' path triggered in 'verify_face_from_webcam'. This is likely incorrect.")
146
+ if not name.strip():
147
+ # Trying to match 4 return values: image, string, image, number
148
+ return marked_frame, "⚠️ Please enter a name to save this face. (This shouldn't happen from Auth tab)", face, max_similarity # Problematic return
149
+
150
+ # (This saving logic is redundant with register_face)
151
+ metadata = {
152
+ "embedding": embedding.tolist(),
153
+ "name": name.strip(),
154
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
155
+ }
156
+ # --- BUG AREA ---
157
+ # Should load the FULL data, append, then save, not just append embedding to in-memory list
158
+ try:
159
+ with open(EMBEDDINGS_FILE, "r") as f:
160
+ all_data = json.load(f)
161
+ except (FileNotFoundError, json.JSONDecodeError):
162
+ all_data = []
163
+ all_data.append(metadata)
164
+ # Need to update the global variable too if we want consistency during session
165
+ stored_embeddings.append(embedding) # Appending only embedding, not metadata
166
+ with open(EMBEDDINGS_FILE, "w") as f:
167
+ json.dump(all_data, f)
168
+
169
+ # Trying to match 4 return values: image, string, image, number
170
+ return marked_frame, f"✅ **NEW FACE REGISTERED!** Welcome, {name}! (From verify_face_from_webcam - likely wrong)", face, 1.0 # Problematic return
171
+
172
+
173
+ # If we are here, it means we were only verifying (save_embedding=False) and no match was found > 0.7
174
+ # Return signature: image, string, image, number
175
+ return marked_frame, f"❌ **ACCESS DENIED!** Highest similarity: {max_similarity:.2f}", face, max_similarity
176
+
177
+ except Exception as e:
178
+ print(f"Error in verify_face_from_webcam: {e}") # Log the error
179
+ # Return signature: image, string, image, number
180
+ return None, f"⚠️ Error processing video: {str(e)}", None, 0
181
+
182
+
183
+ def register_face(video, name=""):
184
+ """Registers a new face from video."""
185
+ global stored_embeddings # Reference the global list
186
+
187
+ if video is None:
188
+ # Return signature: image, string, image
189
+ return None, "Please record a video first.", None
190
+
191
+ if not name.strip():
192
+ # Return signature: image, string, image
193
+ # Need to return *something* for the images if no video processed
194
+ return None, "⚠️ Please enter a name to save this face.", None
195
+
196
+ try:
197
+ cap = cv2.VideoCapture(video)
198
+ frames = []
199
+ frame_count = 0
200
+ while frame_count < 60: # Limit frames
201
+ ret, frame = cap.read()
202
+ if not ret:
203
+ break
204
+ frames.append(frame)
205
+ frame_count += 1
206
+ cap.release()
207
+
208
+ if not frames:
209
+ # Return signature: image, string, image
210
+ return None, "Error: Could not read video.", None
211
+
212
+ # Use the middle frame
213
+ frame = frames[len(frames)//2]
214
+
215
+ # Extract embedding
216
+ embedding, marked_frame, face = extract_face_embedding_from_frame(frame)
217
+
218
+ # Check for existing faces (using a higher threshold for registration uniqueness)
219
+ for i, stored_embedding in enumerate(stored_embeddings):
220
+ similarity = np.dot(embedding, stored_embedding) / (np.linalg.norm(embedding) * np.linalg.norm(stored_embedding))
221
+ # Using a stricter threshold during registration check
222
+ if similarity > 0.9: # Hardcoded threshold
223
+ # Return signature: image, string, image
224
+ # Optionally retrieve name if metadata loading is fixed:
225
+ # existing_name = get_name_for_embedding(i) # Requires modification
226
+ return marked_frame, f"⚠️ This face seems highly similar to an existing one in the database (Similarity: {similarity:.2f}). Registration aborted.", face
227
+
228
+
229
+ # If checks pass, prepare metadata and save
230
+ metadata = {
231
+ "embedding": embedding.tolist(),
232
+ "name": name.strip(),
233
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
234
+ }
235
+
236
+ # --- Corrected Saving Logic ---
237
+ all_data = []
238
+ try:
239
+ # Read the existing full data file
240
+ with open(EMBEDDINGS_FILE, "r") as f:
241
+ all_data = json.load(f)
242
+ # Ensure it's a list
243
+ if not isinstance(all_data, list):
244
+ print(f"Warning: Embeddings file was not a list. Re-initializing.")
245
+ all_data = []
246
+ except (FileNotFoundError, json.JSONDecodeError):
247
+ # If file doesn't exist or is invalid, start with an empty list
248
+ all_data = []
249
+ except Exception as e:
250
+ print(f"Error reading embeddings file before save: {e}. Starting fresh.")
251
+ all_data = []
252
+
253
+
254
+ # Append the new metadata dictionary
255
+ all_data.append(metadata)
256
+
257
+ # Write the updated list of dictionaries back to the file
258
+ try:
259
+ with open(EMBEDDINGS_FILE, "w") as f:
260
+ json.dump(all_data, f, indent=4) # Add indent for readability
261
+ # Update the in-memory list ONLY IF save was successful
262
+ stored_embeddings.append(embedding) # Add the numpy array to the in-memory list for immediate use
263
+ print(f"Successfully registered {name}. Total embeddings in memory: {len(stored_embeddings)}")
264
+ except Exception as e:
265
+ print(f"Error writing embeddings file: {e}")
266
+ # Return signature: image, string, image
267
+ return marked_frame, f"⚠️ Error saving embedding to file: {e}", face
268
+
269
+
270
+ # Return signature: image, string, image
271
+ return marked_frame, f"✅ **NEW FACE REGISTERED!** Welcome, {name}!", face
272
+
273
+ except Exception as e:
274
+ print(f"Error in register_face: {e}") # Log the error
275
+ # Return signature: image, string, image
276
+ return None, f"⚠️ Error processing registration: {str(e)}", None
277
+
278
+
279
+ def get_registered_count():
280
+ """Get the count of registered faces from the JSON file."""
281
+ try:
282
+ with open(EMBEDDINGS_FILE, "r") as f:
283
+ # Load the full data which should be a list of dictionaries
284
+ data = json.load(f)
285
+ if isinstance(data, list):
286
+ return f"Total registered faces: {len(data)}"
287
+ else:
288
+ return "Registered faces: Error (Invalid data format)"
289
+ except (FileNotFoundError, json.JSONDecodeError):
290
+ return "Total registered faces: 0"
291
+ except Exception as e:
292
+ print(f"Error reading count: {e}")
293
+ return "Registered faces: Error reading file"
294
+
295
+ # Define CSS
296
+ css = """
297
+ .gradio-container {
298
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
299
+ color: white;
300
+ }
301
+ .title-container {
302
+ text-align: center;
303
+ margin-bottom: 2rem;
304
+ }
305
+ .title-container h1 {
306
+ font-size: 2.5rem;
307
+ background: -webkit-linear-gradient(#eee, #0e93e0);
308
+ -webkit-background-clip: text;
309
+ -webkit-text-fill-color: transparent;
310
+ margin-bottom: 0.5rem;
311
+ }
312
+ .title-container p {
313
+ font-size: 1.2rem;
314
+ color: #ccc;
315
+ }
316
+ .status-authenticated { /* This class seems unused in the Markdown outputs */
317
+ color: #4CAF50;
318
+ font-weight: bold;
319
+ font-size: 1.2rem;
320
+ }
321
+ .status-denied { /* This class seems unused in the Markdown outputs */
322
+ color: #F44336;
323
+ font-weight: bold;
324
+ font-size: 1.2rem;
325
+ }
326
+ .output-image {
327
+ border-radius: 10px;
328
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
329
+ }
330
+ .footer {
331
+ text-align: center;
332
+ margin-top: 2rem;
333
+ font-size: 0.9rem;
334
+ color: #888;
335
+ }
336
+ /* Removed .face-container as it wasn't applied via elem_classes */
337
+ /* You might want to apply custom classes directly to Markdown/Image components if needed */
338
+ """
339
+
340
+ # Define the Gradio Interface
341
+ with gr.Blocks(css=css, theme=gr.themes.Soft()) as iface:
342
+ gr.HTML("""
343
+ <div class="title-container">
344
+ <h1>🔐 Secure Face Authentication System</h1>
345
+ <p>Record a video using your webcam to authenticate or register a new face</p>
346
+ </div>
347
+ """)
348
+
349
+ with gr.Tabs():
350
+ with gr.Tab("Authentication"):
351
+ with gr.Row():
352
+ with gr.Column(scale=3):
353
+ # Use 'webcam' source for direct recording if preferred over upload
354
+ # format="mp4" might require ffmpeg installed in the environment
355
+ video_input = gr.Video(label="Record/Upload a short video (1-2 seconds)", sources=["webcam", "upload"], format="mp4")
356
+ auth_button = gr.Button("Authenticate", variant="primary")
357
+
358
+ with gr.Column(scale=2):
359
+ output_image = gr.Image(label="Detection Result", type="numpy") # Specify type
360
+ face_image = gr.Image(label="Extracted Face", type="numpy", elem_classes="output-image") # Specify type
361
+
362
+ with gr.Row():
363
+ # Use Markdown for rich text status
364
+ status_text = gr.Markdown("Ready for authentication...")
365
+ confidence_meter = gr.Number(label="Confidence Score", value=0) # Removed min/max as similarity can vary
366
+
367
+ with gr.Tab("Registration"):
368
+ with gr.Row():
369
+ with gr.Column(scale=3):
370
+ # Use 'webcam' source for direct recording
371
+ reg_video_input = gr.Video(label="Record/Upload a short video (1-2 seconds)", sources=["webcam", "upload"], format="mp4")
372
+ name_input = gr.Textbox(label="Enter your name", placeholder="John Doe")
373
+ register_button = gr.Button("Register New Face", variant="secondary")
374
+
375
+ with gr.Column(scale=2):
376
+ reg_output_image = gr.Image(label="Detection Result", type="numpy") # Specify type
377
+ reg_face_image = gr.Image(label="Extracted Face", type="numpy", elem_classes="output-image") # Specify type
378
+
379
+ with gr.Row():
380
+ # Use Markdown for rich text status
381
+ reg_status_text = gr.Markdown("Ready for registration...")
382
+
383
+ with gr.Row():
384
+ # Initial state loaded by function call
385
+ stats_text = gr.Markdown(get_registered_count())
386
+ # Button to refresh the count
387
+ refresh_button = gr.Button("Refresh Stats", variant="tertiary", size="sm")
388
+
389
+
390
+ gr.HTML("""
391
+ <div class="footer">
392
+ <p>Powered by DeepFace + Gradio | © 2025 Face Authentication System</p>
393
+ </div>
394
+ """)
395
+
396
+ # Define interactions
397
+ # Authentication button click action
398
+ auth_button.click(
399
+ fn=verify_face_from_webcam,
400
+ # Pass video input. The save_embedding flag is hardcoded False here.
401
+ # The name input isn't relevant for pure authentication.
402
+ inputs=[video_input, gr.Checkbox(value=False, visible=False)],
403
+ # Map outputs to the correct components
404
+ outputs=[output_image, status_text, face_image, confidence_meter]
405
+ )
406
+
407
+ # Registration button click action
408
+ register_button.click(
409
+ fn=register_face,
410
+ # Pass registration video and name input
411
+ inputs=[reg_video_input, name_input],
412
+ # Map outputs to the correct components
413
+ outputs=[reg_output_image, reg_status_text, reg_face_image]
414
+ )
415
+
416
+ # Refresh button click action
417
+ refresh_button.click(fn=get_registered_count, inputs=None, outputs=[stats_text])
418
+
419
+
420
+ # Launch the interface
421
+ # The if __name__ == "__main__": block is good practice but not strictly required by Spaces
422
+ if __name__ == "__main__":
423
+ iface.launch() # debug=True can be helpful locally but remove for production
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio
2
+ numpy
3
+ opencv-python-headless
4
+ deepface
5
+ ffmpeg-python