Spaces:
Sleeping
Sleeping
File size: 17,456 Bytes
c68a03b 79f6f0f b9f54f4 c68a03b b9f54f4 48b5d08 79f6f0f 48b5d08 79f6f0f 48b5d08 79f6f0f 48b5d08 c68a03b 79f6f0f 48b5d08 79f6f0f b9f54f4 79f6f0f c68a03b 79f6f0f 48b5d08 79f6f0f c68a03b 48b5d08 c68a03b 48b5d08 b9f54f4 c68a03b 48b5d08 79f6f0f c68a03b b9f54f4 48b5d08 b9f54f4 48b5d08 b9f54f4 c68a03b b9f54f4 79f6f0f c68a03b 79f6f0f c68a03b 79f6f0f c68a03b 79f6f0f b9f54f4 79f6f0f b9f54f4 79f6f0f c68a03b b9f54f4 79f6f0f 48b5d08 79f6f0f b9f54f4 79f6f0f b9f54f4 48b5d08 b9f54f4 79f6f0f b9f54f4 79f6f0f 48b5d08 b9f54f4 48b5d08 79f6f0f 1381884 79f6f0f b9f54f4 79f6f0f b9f54f4 79f6f0f b9f54f4 79f6f0f b9f54f4 48b5d08 b9f54f4 c68a03b 79f6f0f b9f54f4 79f6f0f b9f54f4 79f6f0f b9f54f4 79f6f0f b9f54f4 c68a03b b9f54f4 c68a03b b9f54f4 48b5d08 b9f54f4 29272af c68a03b 79f6f0f b9f54f4 48b5d08 c68a03b 79f6f0f c68a03b 48b5d08 79f6f0f c68a03b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | 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'<div class="main-title"><h1>{config.SPACE_TITLE}</h1><p>{config.SPACE_SUBTITLE}</p></div>'
)
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"<div class='small-copy'>Auto-matches every {config.REALTIME_STREAM_SECONDS:.1f}s while frames are available.</div>"
)
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
|