import streamlit as st
import os
import sys
import time
import tempfile
import numpy as np
import pandas as pd
from PIL import Image, ImageDraw, ImageFont
import cv2
# Add root directory to python path
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
from src.embedding import FaceEmbedder
from src.similarity import numpy_vectorized_cosine, numpy_vectorized_euclidean
from src.gallery import FaceGallery
# Page configuration
st.set_page_config(
page_title="FaceID - Real-Time Recognition & Verification",
page_icon="🛡️",
layout="wide",
initial_sidebar_state="expanded"
)
# Premium Custom CSS
st.markdown("""
""", unsafe_allow_html=True)
# Cache model loader
@st.cache_resource
def get_embedder(model_name="Facenet"):
return FaceEmbedder(model_name=model_name)
def save_uploaded_file(uploaded_file):
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp_file:
tmp_file.write(uploaded_file.getvalue())
return tmp_file.name
except Exception as e:
st.error(f"Error saving file: {e}")
return None
def draw_face_bbox(image_path_or_pil, facial_area, color=(56, 189, 248), label="Face"):
"""Draw bounding box on image using PIL."""
if isinstance(image_path_or_pil, str):
img = Image.open(image_path_or_pil).convert("RGB")
else:
img = image_path_or_pil.copy()
x, y, w, h = facial_area.get("x", 0), facial_area.get("y", 0), facial_area.get("w", 0), facial_area.get("h", 0)
if w == 0 or h == 0:
return img
draw = ImageDraw.Draw(img)
draw.rectangle([x, y, x + w, y + h], outline=color, width=4)
if label:
draw.rectangle([x, max(0, y - 24), x + w, y], fill=color)
draw.text((x + 6, max(0, y - 20)), label, fill=(0, 0, 0))
return img
def load_markdown_file(path):
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
return f.read()
return "File not found."
# Pre-populate 1:N Gallery with sample images if available
@st.cache_resource
def init_sample_gallery():
gallery = FaceGallery()
embedder = get_embedder("Facenet")
samples_dir = "data/lfw/test"
if os.path.exists(samples_dir):
count = 0
for person_name in os.listdir(samples_dir):
person_path = os.path.join(samples_dir, person_name)
if os.path.isdir(person_path):
images = [f for f in os.listdir(person_path) if f.endswith(('.jpg', '.png', '.jpeg'))]
if images:
first_img = os.path.join(person_path, images[0])
try:
emb = embedder.compute_embedding(first_img)
gallery.enroll(name=person_name.replace("_", " "), embedding=emb, image_path=first_img)
count += 1
except Exception as e:
pass
if count >= 10: # Limit initial gallery size for speed
break
return gallery
def main():
st.markdown("
FaceID Recognition Engine
", unsafe_allow_html=True)
st.markdown("Deep Learning Face Verification & 1:N Identification Suite
", unsafe_allow_html=True)
# Sidebar setup
st.sidebar.title("⚙️ Engine Settings")
model_choice = st.sidebar.selectbox("Backbone Model", ["Facenet", "VGG-Face", "ArcFace", "SFace"])
threshold = st.sidebar.slider("Verification Threshold (Cosine)", 0.0, 1.0, 0.35, 0.01)
st.sidebar.markdown("---")
st.sidebar.info("💡 **Hugging Face Space Live Demo**\nUses FaceNet Inception Architecture for 128D/512D embeddings.")
# Initialize embedder & gallery
embedder = get_embedder(model_choice)
gallery = init_sample_gallery()
# Main Navigation Tabs
tab1, tab2, tab3, tab4 = st.tabs([
"🔍 1:1 Verification",
"👤 1:N Gallery Search",
"📸 Face Inspector & Vectors",
"📊 System Insights & Cards"
])
# ==================== TAB 1: 1:1 VERIFICATION ====================
with tab1:
st.markdown("### 🔍 1:1 Face Verification")
st.caption("Compare two face images to verify if they belong to the same person.")
sample_pairs = {
"Custom Upload": (None, None),
"Same Identity: Albrecht Mentz": (
"data/lfw/test/Albrecht_Mentz/Albrecht_Mentz_0000.jpg",
"data/lfw/test/Albrecht_Mentz/Albrecht_Mentz_0001.jpg"
),
"Same Identity: Alejandro Toledo": (
"data/lfw/test/Alejandro_Toledo/Alejandro_Toledo_0000.jpg",
"data/lfw/test/Alejandro_Toledo/Alejandro_Toledo_0001.jpg"
),
"Different Identities: Albrecht vs Alejandro": (
"data/lfw/test/Albrecht_Mentz/Albrecht_Mentz_0000.jpg",
"data/lfw/test/Alejandro_Toledo/Alejandro_Toledo_0000.jpg"
)
}
selected_preset = st.selectbox("Quick Sample Presets:", list(sample_pairs.keys()))
s_img1, s_img2 = sample_pairs[selected_preset]
col1, col2 = st.columns(2)
with col1:
st.markdown("", unsafe_allow_html=True)
st.subheader("Subject A")
if s_img1 and os.path.exists(s_img1):
img1_path = s_img1
st.image(img1_path, use_container_width=True)
else:
up1 = st.file_uploader("Upload Image A", type=['jpg', 'jpeg', 'png'], key="v_img1")
img1_path = save_uploaded_file(up1) if up1 else None
if up1: st.image(up1, use_container_width=True)
st.markdown("
", unsafe_allow_html=True)
with col2:
st.markdown("", unsafe_allow_html=True)
st.subheader("Subject B")
if s_img2 and os.path.exists(s_img2):
img2_path = s_img2
st.image(img2_path, use_container_width=True)
else:
up2 = st.file_uploader("Upload Image B", type=['jpg', 'jpeg', 'png'], key="v_img2")
img2_path = save_uploaded_file(up2) if up2 else None
if up2: st.image(up2, use_container_width=True)
st.markdown("
", unsafe_allow_html=True)
if st.button("⚡ Run Verification", type="primary", use_container_width=True):
if img1_path and img2_path:
with st.spinner("Extracting facial embeddings & computing metrics..."):
t0 = time.time()
details1 = embedder.extract_face_details(img1_path)
details2 = embedder.extract_face_details(img2_path)
t_extract = (time.time() - t0) * 1000
emb1, emb2 = details1["embedding"], details2["embedding"]
sim_cos = float(numpy_vectorized_cosine(emb1.reshape(1, -1), emb2.reshape(1, -1))[0])
dist_euc = float(numpy_vectorized_euclidean(emb1.reshape(1, -1), emb2.reshape(1, -1))[0])
is_same = sim_cos >= threshold
confidence = min(100.0, max(50.0, (sim_cos / (threshold * 2.0)) * 100.0)) if is_same else min(50.0, (sim_cos / threshold) * 50.0)
# Display Result Badge
badge_cls = "match-same" if is_same else "match-diff"
decision_str = "MATCH: SAME PERSON" if is_same else "NO MATCH: DIFFERENT PERSONS"
st.markdown(f"{decision_str} (Confidence: {confidence:.1f}%)
", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
m1, m2, m3, m4 = st.columns(4)
m1.metric("Cosine Similarity", f"{sim_cos:.4f}")
m2.metric("Euclidean Distance", f"{dist_euc:.4f}")
m3.metric("Decision Threshold", f"{threshold:.2f}")
m4.metric("Extraction Latency", f"{t_extract:.1f} ms")
# Show Bounding Box overlays
b_col1, b_col2 = st.columns(2)
with b_col1:
boxed1 = draw_face_bbox(img1_path, details1["facial_area"], label="Subject A")
st.image(boxed1, caption="Detected Face A", use_container_width=True)
with b_col2:
boxed2 = draw_face_bbox(img2_path, details2["facial_area"], label="Subject B")
st.image(boxed2, caption="Detected Face B", use_container_width=True)
else:
st.warning("Please upload or select images for both Subject A and Subject B.")
# ==================== TAB 2: 1:N GALLERY SEARCH ====================
with tab2:
st.markdown("### 👤 1:N Face Database Identification")
st.caption("Search an unknown query face against enrolled identities in the Face Gallery.")
c_left, c_right = st.columns([1, 2])
with c_left:
st.markdown("", unsafe_allow_html=True)
st.subheader("➕ Enroll New Identity")
new_name = st.text_input("Person Name / ID", placeholder="e.g. Elon Musk")
new_img_up = st.file_uploader("Upload Enrollment Photo", type=['jpg', 'jpeg', 'png'], key="enroll_file")
if st.button("Register to Gallery", use_container_width=True):
if new_name and new_img_up:
saved_p = save_uploaded_file(new_img_up)
emb = embedder.compute_embedding(saved_p)
gallery.enroll(name=new_name, embedding=emb, image_path=saved_p)
st.success(f"Enrolled '{new_name}' successfully! Gallery size: {gallery.count()}")
else:
st.warning("Please enter name and upload photo.")
st.markdown("
", unsafe_allow_html=True)
st.markdown(f"**Enrolled Identities in Database:** `{gallery.count()}`")
with c_right:
st.markdown("", unsafe_allow_html=True)
st.subheader("🔍 Query Identity")
query_up = st.file_uploader("Upload Unknown Face Photo", type=['jpg', 'jpeg', 'png'], key="query_file")
top_k = st.slider("Top Results (K)", 1, 5, 3)
if query_up:
st.image(query_up, width=200, caption="Query Input")
if st.button("🔍 Search Face Gallery", type="primary", use_container_width=True):
q_path = save_uploaded_file(query_up)
with st.spinner("Searching gallery vectors..."):
q_emb = embedder.compute_embedding(q_path)
matches = gallery.search(q_emb, top_k=top_k, threshold=threshold)
if matches:
st.markdown("#### Top Matching Identities:")
for rank, match in enumerate(matches, 1):
sim = match["similarity"]
is_m = match["is_match"]
color_bar = "🟢" if is_m else "🔴"
res_col1, res_col2 = st.columns([1, 3])
with res_col1:
if match["image_path"] and os.path.exists(match["image_path"]):
st.image(match["image_path"], use_container_width=True)
with res_col2:
st.markdown(f"### #{rank} {match['name']} {color_bar}")
st.progress(max(0.0, min(1.0, sim)))
st.write(f"Similarity Score: `{sim:.4f}` | Decision: `{'MATCH' if is_m else 'NO MATCH'}`")
st.markdown("---")
else:
st.info("No identities in gallery. Please enroll faces first.")
st.markdown("
", unsafe_allow_html=True)
# ==================== TAB 3: FACE INSPECTOR & VECTORS ====================
with tab3:
st.markdown("### 📸 Face Inspector & Embedding Visualizer")
st.caption("Inspect facial alignment, bounding box coordinates, and 128D/512D deep feature vectors.")
insp_up = st.file_uploader("Upload Face Image for Analysis", type=['jpg', 'jpeg', 'png'], key="insp_file")
if insp_up:
insp_path = save_uploaded_file(insp_up)
details = embedder.extract_face_details(insp_path)
emb = details["embedding"]
area = details["facial_area"]
col_i1, col_i2 = st.columns(2)
with col_i1:
st.markdown("", unsafe_allow_html=True)
st.subheader("Facial Bounding Box & Detection")
boxed_img = draw_face_bbox(insp_path, area, label="Detected Face")
st.image(boxed_img, use_container_width=True)
st.write(f"**Bounding Box (x, y, w, h):** `{area}`")
st.markdown("
", unsafe_allow_html=True)
with col_i2:
st.markdown("", unsafe_allow_html=True)
st.subheader("Embedding Vector Statistics")
st.metric("Vector Dimension", f"{len(emb)}D")
st.metric("Vector L2 Norm", f"{np.linalg.norm(emb):.4f}")
st.metric("Mean Value", f"{np.mean(emb):.4f}")
st.metric("Standard Deviation", f"{np.std(emb):.4f}")
st.markdown("
", unsafe_allow_html=True)
st.markdown("", unsafe_allow_html=True)
st.subheader("128D Deep Feature Profile (Embedding Heatmap)")
df_emb = pd.DataFrame({"Feature Dimension": range(len(emb)), "Activation Value": emb})
st.line_chart(df_emb.set_index("Feature Dimension"))
st.markdown("
", unsafe_allow_html=True)
# ==================== TAB 4: SYSTEM INSIGHTS ====================
with tab4:
st.markdown("### 📊 System Insights & Documentation")
r_tab1, r_tab2, r_tab3 = st.tabs(["🚀 Latency & Profiling", "🛡️ System Card", "📈 ROC & Metrics"])
with r_tab1:
st.subheader("Hardware-Aware Latency Breakdown")
l_col1, l_col2 = st.columns(2)
with l_col1:
st.markdown("#### Latency Breakdown (CPU)")
latency_data = pd.DataFrame({
"Stage": ["Embedding Extraction", "Similarity Calculation"],
"Mean Latency (ms)": [464.76, 0.15]
})
st.bar_chart(latency_data.set_index("Stage"))
with l_col2:
st.markdown("#### Throughput Sensitivity (FPS vs Batch Size)")
throughput_data = pd.DataFrame({
"Batch Size": [1, 4, 8, 16],
"Throughput (FPS)": [2.10, 2.03, 2.04, 2.20]
})
st.line_chart(throughput_data.set_index("Batch Size"))
st.markdown("---")
st.markdown("#### Detailed Profiling Summary")
summary_txt = load_markdown_file("reports/profiling_summary.txt")
st.code(summary_txt, language="markdown")
with r_tab2:
st.subheader("System Card Documentation")
sys_card_md = load_markdown_file("reports/System_Card.md")
st.markdown(sys_card_md)
with r_tab3:
st.subheader("Model Evaluation Summary")
e1, e2, e3 = st.columns(3)
e1.metric("Verification Accuracy", "84.6%")
e2.metric("F1-Score", "0.8254")
e3.metric("Evaluated Pairs", "500")
if os.path.exists("reports/roc_curve.png"):
st.image("reports/roc_curve.png", caption="ROC Curve for Calibrated Model", use_container_width=True)
if __name__ == "__main__":
main()