from __future__ import annotations import json import gradio as gr import pandas as pd from PIL import Image from src import config from src.data.gallery_store import DemoGallery from src.pipeline.image_pipeline import ImagePipeline from src.protocol.fpsi_adapter import FuzzyPSIAdapter gallery = DemoGallery() image_pipeline = ImagePipeline() adapter = FuzzyPSIAdapter(config.DEFAULT_MODE) CSS = """ footer, .built-with, a[href*="gradio.app"] { display: none !important; } .main-title { text-align: center; padding: 10px 0 6px; } .main-title h1 { font-size: 1.7em; margin: 0; letter-spacing: -0.5px; } .main-title p { color: #555; margin: 2px 0 0; font-size: 0.92em; } .compact-note textarea { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; } .small-copy { color: #666; font-size: 0.9em; } """ def _example_paths() -> list[list[str]]: return [[path] for _, path in gallery.example_choices()] def _empty_metrics() -> pd.DataFrame: return pd.DataFrame(columns=["Metric", "Value"]) def _source_label(match_source: str) -> str: if match_source == "enrolled": return "Enrolled runtime library" return "Packaged LFW gallery" def _gallery_metrics(summary_json: str, notes: str) -> tuple[str, pd.DataFrame, str, str]: payload = json.loads(summary_json) summary = payload["summary"] match = payload["match"] details = payload["details"] source_label = _source_label(details.get("match_source", "demo")) verdict = ( f"MATCH: {match['person']}\n" f"Source: {source_label} | Distance: {match['hamming_distance']} | Score: {match['score']:.4f}" if match["matched"] else f"NO MATCH\nBest: {match['person']} | Source: {source_label} | Distance: {match['hamming_distance']} | Score: {match['score']:.4f}" ) metrics_df = pd.DataFrame( [ ("Result source", source_label), ("Mode", summary["mode"]), ("Dimension", summary["dim"]), ("Threshold δ", summary["delta"]), ("E-LSH L", summary["L"]), ("Latency (s)", f"{summary['time_s']:.3f}"), ("Communication (MB)", f"{summary['communication_mb']:.3f}"), ("Gallery images", str(summary["gallery_size"])), ("Gao feasible", "YES" if summary["gao_feasible"] else "NO"), ("Top-5 distances", ", ".join(map(str, details["top5_distances"]))), ], columns=["Metric", "Value"], ) compact_stats = ( f"src={details.get('match_source', 'demo')} | mode={summary['mode']} | d={summary['dim']} | " f"δ={summary['delta']} | L={summary['L']} | latency={summary['time_s']:.3f}s" ) return verdict, metrics_df, compact_stats, notes def _query_gallery(query_embedding, features, people, filenames, dim: int): if len(people) == 0: return None return adapter.query_against_gallery( query_embedding, features, people, filenames, dim, calibration_features=gallery.calibration_features, calibration_people=gallery.calibration_people, ) def _match_from_image(image: Image.Image, dim: int, mode: str): adapter.mode = mode analysis = image_pipeline.analyze(image) checked_enrolled = gallery.has_enrollments() enrolled_result = None if checked_enrolled: enrolled_result = _query_gallery( analysis.embedding, gallery.enrolled_features, gallery.enrolled_people, gallery.enrolled_filenames, dim, ) if enrolled_result is not None and enrolled_result[0].matched: match, summary, details = enrolled_result details["match_source"] = "enrolled" notes_prefix = "matched enrolled-user overlay" else: match, summary, details = _query_gallery( analysis.embedding, gallery.features, gallery.people, gallery.filenames, dim, ) details["match_source"] = "demo" if checked_enrolled: notes_prefix = "no enrolled-user match; used packaged LFW gallery" else: notes_prefix = "no enrolled users registered; used packaged LFW gallery" details["checked_enrolled"] = checked_enrolled details["enrolled_gallery_size"] = int(len(gallery.enrolled_people)) gallery_image = Image.open(gallery.gallery_image_path(match.filename, match.person)).convert("RGB") summary_json = adapter.export_summary(match, summary, details) notes = "\n".join(f"- {note}" for note in ([notes_prefix] + analysis.notes + summary.notes)) verdict, metrics_df, compact_stats, note_text = _gallery_metrics(summary_json, notes) return analysis.preview, gallery_image, verdict, compact_stats, metrics_df, note_text, summary_json def run_image_demo(image: Image.Image, dim: int, mode: str): if image is None: raise gr.Error("Please upload or capture a face image.") return _match_from_image(image, dim, mode) def run_realtime_demo(image: Image.Image | None, dim: int, mode: str): if image is None: return None, None, "Waiting for webcam frame.", "", _empty_metrics(), "", "" try: return _match_from_image(image, dim, mode) except Exception as exc: return None, None, f"Realtime error: {exc}", "", _empty_metrics(), "", "" def register_person(image: Image.Image | None, person: str): if image is None: raise gr.Error("Please upload or capture a face image.") normalized_name = " ".join((person or "").split()) if not normalized_name: raise gr.Error("Please enter a name before registering.") analysis = image_pipeline.analyze(image) record, replaced = gallery.enroll(normalized_name, analysis.preview, analysis.embedding) saved_image = Image.open(record.gallery_path).convert("RGB") status = ( f"Updated {normalized_name} in the shared temporary database." if replaced else f"Registered {normalized_name} in the shared temporary database." ) details = ( "Upload and Realtime queries will search enrolled users first. " "This shared temporary database may reset when the Space restarts." ) return analysis.preview, saved_image, status, details, gallery.gallery_items(1), gallery.database_info(1), 1 def load_gallery_page(page: float | int | None): page_num = 1 if page is None else int(page) return gallery.gallery_items(page_num), gallery.database_info(page_num) def build_app() -> gr.Blocks: summary = gallery.summary() with gr.Blocks(title=config.SPACE_TITLE, theme=gr.themes.Base(), css=CSS) as demo: gr.HTML( f'

{config.SPACE_TITLE}

{config.SPACE_SUBTITLE}

' ) with gr.Tabs(): with gr.Tab("Database"): with gr.Row(): with gr.Column(scale=4): database_gallery = gr.Gallery( value=gallery.gallery_items(1), label="Visible Gallery", columns=4, rows=4, height=520, object_fit="cover", preview=True, allow_preview=True, ) with gr.Column(scale=1): database_info = gr.Textbox( value=gallery.database_info(1), label="Info", interactive=False, lines=4, ) gallery_page = gr.Number(label="Page", value=1, minimum=1, maximum=gallery.total_pages(), precision=0) load_page_btn = gr.Button("Load") with gr.Accordion("Parameters", open=False): gr.Dataframe( headers=["Param", "Value"], value=[ ["Backend", config.DEFAULT_MODE], ["Dimensions", ", ".join(map(str, config.DEFAULT_DIMENSIONS))], ["Matching images", str(summary["gallery_size"])], ["Matching identities", str(summary["identity_count"])], ["Enrolled images", str(summary["enrolled_gallery_size"])], ["Enrolled identities", str(summary["enrolled_identity_count"])], ["Visible thumbnails", str(summary["visible_gallery_size"])], ["Calibration", str(summary["calibration_size"])], ["Embedding dim", str(summary["dimensions"])], ], interactive=False, ) load_page_btn.click(load_gallery_page, inputs=[gallery_page], outputs=[database_gallery, database_info], api_name=False) with gr.Tab("Enroll"): gr.Markdown("Register a face with a name into the shared temporary database. Later Upload and Realtime queries will search this registered library first.") with gr.Row(): with gr.Column(): enroll_image = gr.Image( label="Registration Face", type="pil", sources=["upload", "webcam"], height=280, ) enroll_name = gr.Textbox(label="Name", placeholder="Enter the identity name to register") enroll_btn = gr.Button("Register Face", variant="primary") with gr.Column(): enroll_saved = gr.Image(label="Saved Database Face", height=280, interactive=False) enroll_status = gr.Textbox(label="Registration Status", lines=2, interactive=False) with gr.Row(): with gr.Column(): enroll_preview = gr.Image(label="Processed Registration Face", height=220, interactive=False) with gr.Column(): enroll_notes = gr.Textbox(label="Notes", lines=4, interactive=False) enroll_btn.click( register_person, inputs=[enroll_image, enroll_name], outputs=[enroll_preview, enroll_saved, enroll_status, enroll_notes, database_gallery, database_info, gallery_page], api_name=False, ) with gr.Tab("Upload"): gr.Markdown("Upload a query face and match it against the enrolled-user overlay first, then the larger LFW-derived database.") with gr.Row(): with gr.Column(): image_input = gr.Image( label="Query Face", type="pil", sources=["upload", "webcam"], height=280, ) gr.Examples(examples=_example_paths(), inputs=[image_input], label="Examples") with gr.Accordion("Advanced Settings", open=False): upload_dim = gr.Dropdown( choices=list(config.DEFAULT_DIMENSIONS), value=config.DEFAULT_DIM, label="Binary dimension", ) upload_mode = gr.Dropdown( choices=list(config.SUPPORTED_MODES), value=config.DEFAULT_MODE, label="Backend mode", ) upload_btn = gr.Button("Run Identification", variant="primary") with gr.Column(): upload_match = gr.Image(label="Matched Identity", height=280, interactive=False) upload_verdict = gr.Textbox(label="Verdict", lines=3, interactive=False) upload_summary = gr.Textbox(label="Summary", lines=2, interactive=False) with gr.Row(): with gr.Column(): upload_preview = gr.Image(label="Processed Query", height=220, interactive=False) with gr.Column(): upload_metrics = gr.Dataframe(label="Protocol Metrics", interactive=False) with gr.Accordion("Execution Notes", open=False): upload_notes = gr.Textbox(label="Notes", lines=6, interactive=False, elem_classes=["compact-note"]) with gr.Accordion("Detailed JSON Summary", open=False): upload_json = gr.Textbox(label="JSON", lines=14, interactive=False, elem_classes=["compact-note"]) upload_btn.click( run_image_demo, inputs=[image_input, upload_dim, upload_mode], outputs=[upload_preview, upload_match, upload_verdict, upload_summary, upload_metrics, upload_notes, upload_json], api_name=False, ) with gr.Tab("Realtime"): gr.Markdown("Browser webcam recognition. Frames are processed on a throttled interval for stability.") with gr.Row(): with gr.Column(): webcam_input = gr.Image( label="Realtime Camera", type="pil", sources=["webcam"], streaming=True, height=280, ) gr.Markdown( f"
Auto-matches every {config.REALTIME_STREAM_SECONDS:.1f}s while frames are available.
" ) with gr.Accordion("Advanced Settings", open=False): realtime_dim = gr.Dropdown( choices=list(config.DEFAULT_DIMENSIONS), value=config.DEFAULT_DIM, label="Binary dimension", ) realtime_mode = gr.Dropdown( choices=list(config.SUPPORTED_MODES), value=config.DEFAULT_MODE, label="Backend mode", ) realtime_btn = gr.Button("Identify Current Frame") with gr.Column(): realtime_match = gr.Image(label="Matched Identity", height=280, interactive=False) realtime_verdict = gr.Textbox(label="Realtime Verdict", lines=3, interactive=False) realtime_summary = gr.Textbox(label="Realtime Summary", lines=2, interactive=False) with gr.Row(): with gr.Column(): realtime_preview = gr.Image(label="Current Frame Preview", height=220, interactive=False) with gr.Column(): realtime_metrics = gr.Dataframe(label="Realtime Metrics", interactive=False) with gr.Accordion("Realtime Notes", open=False): realtime_notes = gr.Textbox(label="Notes", lines=6, interactive=False, elem_classes=["compact-note"]) with gr.Accordion("Realtime JSON Summary", open=False): realtime_json = gr.Textbox(label="JSON", lines=14, interactive=False, elem_classes=["compact-note"]) webcam_input.stream( run_realtime_demo, inputs=[webcam_input, realtime_dim, realtime_mode], outputs=[realtime_preview, realtime_match, realtime_verdict, realtime_summary, realtime_metrics, realtime_notes, realtime_json], api_name=False, trigger_mode="always_last", concurrency_limit=1, show_progress="hidden", stream_every=config.REALTIME_STREAM_SECONDS, ) realtime_btn.click( run_realtime_demo, inputs=[webcam_input, realtime_dim, realtime_mode], outputs=[realtime_preview, realtime_match, realtime_verdict, realtime_summary, realtime_metrics, realtime_notes, realtime_json], api_name=False, ) with gr.Accordion("Protocol Details", open=False): gr.Markdown( f""" **Pipeline**: image/webcam frame → face embedding → binary projection → enrolled-user overlay match → fallback LFW E-LSH/Hamming verification. **Modes**: `simulation` for public reliability, `full` for optional native FPSI execution when runtime support is present. **Database**: {summary['demo_gallery_size']} packaged LFW images plus a shared temporary enrolled-user overlay are used for matching and browsing. """ ) return demo