# -*- coding: utf-8 -*- import gradio as gr import numpy as np import cv2 from deepface import DeepFace import json import time import os # Ensure OpenCV finds the Haar Cascade file # This might be needed if the default path isn't automatically found in the HF environment cv2_base_dir = os.path.dirname(os.path.abspath(cv2.__file__)) haar_cascade_path = os.path.join(cv2_base_dir, 'data', 'haarcascade_frontalface_default.xml') # Check if the cascade file exists, raise error if not if not os.path.isfile(haar_cascade_path): # Attempt backup location if primary fails (less likely needed but safe) backup_path = "/home/user/.local/lib/python3.x/site-packages/cv2/data/haarcascade_frontalface_default.xml" # Adjust python version if needed if os.path.isfile(backup_path): haar_cascade_path = backup_path else: raise RuntimeError(f"Could not find Haar Cascade file at expected locations: {haar_cascade_path} or backup paths.") EMBEDDINGS_FILE = "stored_embeddings.json" def load_embeddings(): """Load stored face embeddings (expects list of lists).""" try: with open(EMBEDDINGS_FILE, "r") as f: embeddings_data = json.load(f) # --- CRITICAL BUG FIX AREA (See Analysis) --- # Assuming the file SHOULD contain the structure saved by register_face: # a list of dictionaries like [{"embedding": [...], "name": "...", "timestamp": "..."}] # We need to extract just the embedding lists for the current logic. embeddings = [np.array(item["embedding"]) for item in embeddings_data if "embedding" in item] return embeddings except (FileNotFoundError, json.JSONDecodeError): return [] except Exception as e: print(f"Error loading embeddings: {e}. Assuming empty list.") # Add more logging return [] # def save_embeddings(embeddings): # This function is defined but never used # with open(EMBEDDINGS_FILE, "w") as f: # json.dump([embedding.tolist() for embedding in embeddings], f) # Load embeddings when the script starts stored_embeddings = load_embeddings() print(f"Loaded {len(stored_embeddings)} embeddings on startup.") # Add logging def extract_face_embedding_from_frame(frame): """Extract a facial embedding from a single frame (image).""" # Use the validated haar_cascade_path face_cascade = cv2.CascadeClassifier(haar_cascade_path) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)) if len(faces) > 0: # Process only the first detected face x, y, w, h = faces[0] face = frame[y:y + h, x:x + w] # Create a copy for marking to avoid modifying the original frame if needed elsewhere marked_frame = frame.copy() cv2.rectangle(marked_frame, (x, y), (x+w, y+h), (0, 255, 0), 2) # Use Facenet model for representation # Ensure backend='auto' or specify one like 'opencv', 'ssd', 'mtcnn' if needed # Error handling for representation is important try: embedding_objs = DeepFace.represent(face, model_name="Facenet", enforce_detection=False) # Don't re-detect if not embedding_objs: raise Exception("DeepFace could not generate embedding.") embedding = embedding_objs[0]["embedding"] # Access the first item's embedding return np.array(embedding), marked_frame, face except Exception as represent_error: raise Exception(f"Failed to generate embedding: {represent_error}") # If no faces were detected by Haar Cascade raise Exception("No face detected in the frame.") def verify_face_from_webcam(video, save_embedding=False, name=""): """Verifies face from video OR attempts registration if save_embedding is True.""" global stored_embeddings if video is None: # Return signature must match the Gradio outputs: image, string, image, number return None, "Please record a video first.", None, 0 try: cap = cv2.VideoCapture(video) frames = [] frame_count = 0 while frame_count < 60: # Limit frames read to prevent memory issues ret, frame = cap.read() if not ret: break frames.append(frame) frame_count += 1 cap.release() if not frames: # Return signature: image, string, image, number return None, "Error: Could not read video.", None, 0 # Use the middle frame for analysis frame = frames[len(frames)//2] # Extract embedding and get marked frame + face crop embedding, marked_frame, face = extract_face_embedding_from_frame(frame) max_similarity = 0 is_match = False # Compare against loaded embeddings for stored_embedding in stored_embeddings: # Cosine Similarity Calculation similarity = np.dot(embedding, stored_embedding) / (np.linalg.norm(embedding) * np.linalg.norm(stored_embedding)) max_similarity = max(max_similarity, similarity) # Verification threshold if similarity > 0.7: # Hardcoded threshold is_match = True # If only verifying (save_embedding=False), return success if not save_embedding: # Return signature: image, string, image, number return marked_frame, f"✅ **AUTHENTICATED!** Similarity score: {similarity:.2f}", face, similarity # If attempting to save but already matched, return warning (prevents duplicates somewhat) else: # Return signature: image, string, image, number (use 0 for confidence here maybe?) return marked_frame, f"⚠️ This face seems to be already in the database (Similarity: {similarity:.2f}). Registration aborted.", face, similarity # --- Logic for the "save_embedding" flag from the Authentication tab --- # This part is problematic because the Auth tab doesn't provide a name and # the return signature expects 4 items, but registration logic only naturally provides 3. if save_embedding: # This code block within verify_face_from_webcam is likely UNREACHABLE # or will error due to mismatched return values for the Gradio component expecting 4 outputs. # It duplicates logic from `register_face` unnecessarily. # It's better to remove this `save_embedding` logic entirely from the verification function. # Keeping it here as per "do not change code" rule, but highlighting it as flawed. print("WARNING: 'save_embedding' path triggered in 'verify_face_from_webcam'. This is likely incorrect.") if not name.strip(): # Trying to match 4 return values: image, string, image, number return marked_frame, "⚠️ Please enter a name to save this face. (This shouldn't happen from Auth tab)", face, max_similarity # Problematic return # (This saving logic is redundant with register_face) metadata = { "embedding": embedding.tolist(), "name": name.strip(), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") } # --- BUG AREA --- # Should load the FULL data, append, then save, not just append embedding to in-memory list try: with open(EMBEDDINGS_FILE, "r") as f: all_data = json.load(f) except (FileNotFoundError, json.JSONDecodeError): all_data = [] all_data.append(metadata) # Need to update the global variable too if we want consistency during session stored_embeddings.append(embedding) # Appending only embedding, not metadata with open(EMBEDDINGS_FILE, "w") as f: json.dump(all_data, f) # Trying to match 4 return values: image, string, image, number return marked_frame, f"✅ **NEW FACE REGISTERED!** Welcome, {name}! (From verify_face_from_webcam - likely wrong)", face, 1.0 # Problematic return # If we are here, it means we were only verifying (save_embedding=False) and no match was found > 0.7 # Return signature: image, string, image, number return marked_frame, f"❌ **ACCESS DENIED!** Highest similarity: {max_similarity:.2f}", face, max_similarity except Exception as e: print(f"Error in verify_face_from_webcam: {e}") # Log the error # Return signature: image, string, image, number return None, f"⚠️ Error processing video: {str(e)}", None, 0 def register_face(video, name=""): """Registers a new face from video.""" global stored_embeddings # Reference the global list if video is None: # Return signature: image, string, image return None, "Please record a video first.", None if not name.strip(): # Return signature: image, string, image # Need to return *something* for the images if no video processed return None, "⚠️ Please enter a name to save this face.", None try: cap = cv2.VideoCapture(video) frames = [] frame_count = 0 while frame_count < 60: # Limit frames ret, frame = cap.read() if not ret: break frames.append(frame) frame_count += 1 cap.release() if not frames: # Return signature: image, string, image return None, "Error: Could not read video.", None # Use the middle frame frame = frames[len(frames)//2] # Extract embedding embedding, marked_frame, face = extract_face_embedding_from_frame(frame) # Check for existing faces (using a higher threshold for registration uniqueness) for i, stored_embedding in enumerate(stored_embeddings): similarity = np.dot(embedding, stored_embedding) / (np.linalg.norm(embedding) * np.linalg.norm(stored_embedding)) # Using a stricter threshold during registration check if similarity > 0.9: # Hardcoded threshold # Return signature: image, string, image # Optionally retrieve name if metadata loading is fixed: # existing_name = get_name_for_embedding(i) # Requires modification return marked_frame, f"⚠️ This face seems highly similar to an existing one in the database (Similarity: {similarity:.2f}). Registration aborted.", face # If checks pass, prepare metadata and save metadata = { "embedding": embedding.tolist(), "name": name.strip(), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") } # --- Corrected Saving Logic --- all_data = [] try: # Read the existing full data file with open(EMBEDDINGS_FILE, "r") as f: all_data = json.load(f) # Ensure it's a list if not isinstance(all_data, list): print(f"Warning: Embeddings file was not a list. Re-initializing.") all_data = [] except (FileNotFoundError, json.JSONDecodeError): # If file doesn't exist or is invalid, start with an empty list all_data = [] except Exception as e: print(f"Error reading embeddings file before save: {e}. Starting fresh.") all_data = [] # Append the new metadata dictionary all_data.append(metadata) # Write the updated list of dictionaries back to the file try: with open(EMBEDDINGS_FILE, "w") as f: json.dump(all_data, f, indent=4) # Add indent for readability # Update the in-memory list ONLY IF save was successful stored_embeddings.append(embedding) # Add the numpy array to the in-memory list for immediate use print(f"Successfully registered {name}. Total embeddings in memory: {len(stored_embeddings)}") except Exception as e: print(f"Error writing embeddings file: {e}") # Return signature: image, string, image return marked_frame, f"⚠️ Error saving embedding to file: {e}", face # Return signature: image, string, image return marked_frame, f"✅ **NEW FACE REGISTERED!** Welcome, {name}!", face except Exception as e: print(f"Error in register_face: {e}") # Log the error # Return signature: image, string, image return None, f"⚠️ Error processing registration: {str(e)}", None def get_registered_count(): """Get the count of registered faces from the JSON file.""" try: with open(EMBEDDINGS_FILE, "r") as f: # Load the full data which should be a list of dictionaries data = json.load(f) if isinstance(data, list): return f"Total registered faces: {len(data)}" else: return "Registered faces: Error (Invalid data format)" except (FileNotFoundError, json.JSONDecodeError): return "Total registered faces: 0" except Exception as e: print(f"Error reading count: {e}") return "Registered faces: Error reading file" # Define CSS css = """ .gradio-container { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: white; } .title-container { text-align: center; margin-bottom: 2rem; } .title-container h1 { font-size: 2.5rem; background: -webkit-linear-gradient(#eee, #0e93e0); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.5rem; } .title-container p { font-size: 1.2rem; color: #ccc; } .status-authenticated { /* This class seems unused in the Markdown outputs */ color: #4CAF50; font-weight: bold; font-size: 1.2rem; } .status-denied { /* This class seems unused in the Markdown outputs */ color: #F44336; font-weight: bold; font-size: 1.2rem; } .output-image { border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } .footer { text-align: center; margin-top: 2rem; font-size: 0.9rem; color: #888; } /* Removed .face-container as it wasn't applied via elem_classes */ /* You might want to apply custom classes directly to Markdown/Image components if needed */ """ # Define the Gradio Interface with gr.Blocks(css=css, theme=gr.themes.Soft()) as iface: gr.HTML("""

🔐 Secure Face Authentication System

Record a video using your webcam to authenticate or register a new face

""") with gr.Tabs(): with gr.Tab("Authentication"): with gr.Row(): with gr.Column(scale=3): # Use 'webcam' source for direct recording if preferred over upload # format="mp4" might require ffmpeg installed in the environment video_input = gr.Video(label="Record/Upload a short video (1-2 seconds)", sources=["webcam", "upload"], format="mp4") auth_button = gr.Button("Authenticate", variant="primary") with gr.Column(scale=2): output_image = gr.Image(label="Detection Result", type="numpy") # Specify type face_image = gr.Image(label="Extracted Face", type="numpy", elem_classes="output-image") # Specify type with gr.Row(): # Use Markdown for rich text status status_text = gr.Markdown("Ready for authentication...") confidence_meter = gr.Number(label="Confidence Score", value=0) # Removed min/max as similarity can vary with gr.Tab("Registration"): with gr.Row(): with gr.Column(scale=3): # Use 'webcam' source for direct recording reg_video_input = gr.Video(label="Record/Upload a short video (1-2 seconds)", sources=["webcam", "upload"], format="mp4") name_input = gr.Textbox(label="Enter your name", placeholder="John Doe") register_button = gr.Button("Register New Face", variant="secondary") with gr.Column(scale=2): reg_output_image = gr.Image(label="Detection Result", type="numpy") # Specify type reg_face_image = gr.Image(label="Extracted Face", type="numpy", elem_classes="output-image") # Specify type with gr.Row(): # Use Markdown for rich text status reg_status_text = gr.Markdown("Ready for registration...") with gr.Row(): # Initial state loaded by function call stats_text = gr.Markdown(get_registered_count()) # Button to refresh the count refresh_button = gr.Button("Refresh Stats", variant="tertiary", size="sm") gr.HTML(""" """) # Define interactions # Authentication button click action auth_button.click( fn=verify_face_from_webcam, # Pass video input. The save_embedding flag is hardcoded False here. # The name input isn't relevant for pure authentication. inputs=[video_input, gr.Checkbox(value=False, visible=False)], # Map outputs to the correct components outputs=[output_image, status_text, face_image, confidence_meter] ) # Registration button click action register_button.click( fn=register_face, # Pass registration video and name input inputs=[reg_video_input, name_input], # Map outputs to the correct components outputs=[reg_output_image, reg_status_text, reg_face_image] ) # Refresh button click action refresh_button.click(fn=get_registered_count, inputs=None, outputs=[stats_text]) # Launch the interface # The if __name__ == "__main__": block is good practice but not strictly required by Spaces if __name__ == "__main__": iface.launch() # debug=True can be helpful locally but remove for production