kurukshetra / app.py
AI-future's picture
Upload 2 files
c2e2e4e verified
# -*- 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("""
<div class="title-container">
<h1>🔐 Secure Face Authentication System</h1>
<p>Record a video using your webcam to authenticate or register a new face</p>
</div>
""")
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("""
<div class="footer">
<p>Powered by DeepFace + Gradio | © 2025 Face Authentication System</p>
</div>
""")
# 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