Spaces:
Sleeping
Sleeping
Upload deployment-ready Space app
Browse files- .gitignore +6 -0
- Dockerfile +25 -0
- README.md +59 -5
- app.py +18 -0
- assets/examples/examples_manifest.json +42 -0
- assets/examples/query_Ariel_Sharon_0002.jpg +0 -0
- assets/examples/query_Colin_Powell_0002.jpg +0 -0
- assets/examples/query_Donald_Rumsfeld_0002.jpg +0 -0
- assets/examples/query_George_W_Bush_0002.jpg +0 -0
- assets/examples/query_Gerhard_Schroeder_0002.jpg +0 -0
- assets/examples/query_Hugo_Chavez_0002.jpg +0 -0
- assets/examples/query_Junichiro_Koizumi_0002.jpg +0 -0
- assets/examples/query_Tony_Blair_0002.jpg +0 -0
- assets/gallery/Ariel_Sharon_0001.jpg +0 -0
- assets/gallery/Colin_Powell_0001.jpg +0 -0
- assets/gallery/Donald_Rumsfeld_0001.jpg +0 -0
- assets/gallery/George_W_Bush_0001.jpg +0 -0
- assets/gallery/Gerhard_Schroeder_0001.jpg +0 -0
- assets/gallery/Hugo_Chavez_0001.jpg +0 -0
- assets/gallery/Junichiro_Koizumi_0001.jpg +0 -0
- assets/gallery/README.txt +1 -0
- assets/gallery/Tony_Blair_0001.jpg +0 -0
- assets/gallery/gallery_manifest.json +58 -0
- assets/gallery/lfw_demo_calibration.npz +3 -0
- assets/gallery/lfw_demo_gallery.npz +3 -0
- assets/videos/README.txt +1 -0
- requirements.txt +10 -0
- src/__init__.py +1 -0
- src/config.py +36 -0
- src/data/__init__.py +1 -0
- src/data/gallery_store.py +59 -0
- src/pipeline/__init__.py +1 -0
- src/pipeline/image_pipeline.py +68 -0
- src/pipeline/video_pipeline.py +42 -0
- src/protocol/__init__.py +1 -0
- src/protocol/elsh_params.py +19 -0
- src/protocol/fpsi_adapter.py +286 -0
- src/protocol/runtime_stub.txt +1 -0
- src/ui/__init__.py +1 -0
- src/ui/components.py +208 -0
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
out/
|
| 4 |
+
.venv/
|
| 5 |
+
.env
|
| 6 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
ffmpeg \
|
| 7 |
+
git \
|
| 8 |
+
libgl1 \
|
| 9 |
+
libglib2.0-0 \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
WORKDIR /home/user/app
|
| 13 |
+
|
| 14 |
+
COPY --chown=user requirements.txt ./
|
| 15 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 16 |
+
pip install --no-cache-dir -r requirements.txt
|
| 17 |
+
|
| 18 |
+
COPY --chown=user . .
|
| 19 |
+
|
| 20 |
+
USER user
|
| 21 |
+
ENV HOME=/home/user \
|
| 22 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 23 |
+
PYTHONUNBUFFERED=1
|
| 24 |
+
|
| 25 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,64 @@
|
|
| 1 |
---
|
| 2 |
-
title: FuzzyPSI
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FuzzyPSI-hamming
|
| 3 |
+
emoji: 🔐
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# FuzzyPSI-hamming: Deployment-Oriented FPSI Demo
|
| 13 |
+
|
| 14 |
+
This Hugging Face Space presents **FuzzyPSI-hamming** as a reviewer-facing, deployment-oriented demo for privacy-preserving fuzzy private set intersection over Hamming distance.
|
| 15 |
+
|
| 16 |
+
## What this Space demonstrates
|
| 17 |
+
|
| 18 |
+
- **Image-driven query workflow** for a public demo frontend
|
| 19 |
+
- **Binary-code matching** derived from the Hamming-FPSI pipeline
|
| 20 |
+
- **E-LSH candidate generation + exact verification story** exposed in a product-style interface
|
| 21 |
+
- **Protocol metrics panels** showing threshold, communication, latency, and Gao-feasibility information
|
| 22 |
+
- **Dual backend modes**:
|
| 23 |
+
- **Simulation mode** for robust public interaction
|
| 24 |
+
- **Optional full protocol mode** when native FPSI binaries are configured
|
| 25 |
+
|
| 26 |
+
## Reviewer-facing positioning
|
| 27 |
+
|
| 28 |
+
This Space is designed to show the engineering effort beyond the paper artifact:
|
| 29 |
+
|
| 30 |
+
1. turning the protocol into an interactive web application;
|
| 31 |
+
2. bridging uploaded images to binary-code matching;
|
| 32 |
+
3. exposing system metrics and deployment status in a usable UI;
|
| 33 |
+
4. organizing the project as an isolated Hugging Face deployment target.
|
| 34 |
+
|
| 35 |
+
The current public release is **image-first**. The code layout also includes a path for short-video analysis, but the first deployment focuses on making the image path reliable.
|
| 36 |
+
|
| 37 |
+
## Demo data policy
|
| 38 |
+
|
| 39 |
+
The public demo uses a **fixed LFW-derived gallery** so reviewers can test the system in a stable and reproducible way. The deployment project is intentionally isolated from the main research artifact repository.
|
| 40 |
+
|
| 41 |
+
## Repository structure
|
| 42 |
+
|
| 43 |
+
```text
|
| 44 |
+
.
|
| 45 |
+
├── app.py
|
| 46 |
+
├── Dockerfile
|
| 47 |
+
├── requirements.txt
|
| 48 |
+
├── src/
|
| 49 |
+
│ ├── config.py
|
| 50 |
+
│ ├── protocol/
|
| 51 |
+
│ ├── pipeline/
|
| 52 |
+
│ ├── data/
|
| 53 |
+
│ └── ui/
|
| 54 |
+
└── assets/
|
| 55 |
+
├── gallery/
|
| 56 |
+
├── examples/
|
| 57 |
+
└── videos/
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
## Notes
|
| 61 |
+
|
| 62 |
+
- The protocol core is adapted into this Space as a separate deployment project.
|
| 63 |
+
- Public demo mode prioritizes reliability and reviewer usability.
|
| 64 |
+
- Full native protocol execution is exposed only when the deployment environment is provisioned for it.
|
app.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
|
| 5 |
+
|
| 6 |
+
from src.ui.components import build_app
|
| 7 |
+
|
| 8 |
+
app = build_app()
|
| 9 |
+
|
| 10 |
+
if __name__ == "__main__":
|
| 11 |
+
port = int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860")))
|
| 12 |
+
project_root = Path(__file__).resolve().parent
|
| 13 |
+
app.launch(
|
| 14 |
+
server_name="0.0.0.0",
|
| 15 |
+
server_port=port,
|
| 16 |
+
allowed_paths=[str(project_root / "assets")],
|
| 17 |
+
show_api=True,
|
| 18 |
+
)
|
assets/examples/examples_manifest.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"person": "George_W_Bush",
|
| 4 |
+
"filename": "query_George_W_Bush_0002.jpg",
|
| 5 |
+
"path": "query_George_W_Bush_0002.jpg"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"person": "Colin_Powell",
|
| 9 |
+
"filename": "query_Colin_Powell_0002.jpg",
|
| 10 |
+
"path": "query_Colin_Powell_0002.jpg"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"person": "Tony_Blair",
|
| 14 |
+
"filename": "query_Tony_Blair_0002.jpg",
|
| 15 |
+
"path": "query_Tony_Blair_0002.jpg"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"person": "Donald_Rumsfeld",
|
| 19 |
+
"filename": "query_Donald_Rumsfeld_0002.jpg",
|
| 20 |
+
"path": "query_Donald_Rumsfeld_0002.jpg"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"person": "Gerhard_Schroeder",
|
| 24 |
+
"filename": "query_Gerhard_Schroeder_0002.jpg",
|
| 25 |
+
"path": "query_Gerhard_Schroeder_0002.jpg"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"person": "Ariel_Sharon",
|
| 29 |
+
"filename": "query_Ariel_Sharon_0002.jpg",
|
| 30 |
+
"path": "query_Ariel_Sharon_0002.jpg"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"person": "Hugo_Chavez",
|
| 34 |
+
"filename": "query_Hugo_Chavez_0002.jpg",
|
| 35 |
+
"path": "query_Hugo_Chavez_0002.jpg"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"person": "Junichiro_Koizumi",
|
| 39 |
+
"filename": "query_Junichiro_Koizumi_0002.jpg",
|
| 40 |
+
"path": "query_Junichiro_Koizumi_0002.jpg"
|
| 41 |
+
}
|
| 42 |
+
]
|
assets/examples/query_Ariel_Sharon_0002.jpg
ADDED
|
assets/examples/query_Colin_Powell_0002.jpg
ADDED
|
assets/examples/query_Donald_Rumsfeld_0002.jpg
ADDED
|
assets/examples/query_George_W_Bush_0002.jpg
ADDED
|
assets/examples/query_Gerhard_Schroeder_0002.jpg
ADDED
|
assets/examples/query_Hugo_Chavez_0002.jpg
ADDED
|
assets/examples/query_Junichiro_Koizumi_0002.jpg
ADDED
|
assets/examples/query_Tony_Blair_0002.jpg
ADDED
|
assets/gallery/Ariel_Sharon_0001.jpg
ADDED
|
assets/gallery/Colin_Powell_0001.jpg
ADDED
|
assets/gallery/Donald_Rumsfeld_0001.jpg
ADDED
|
assets/gallery/George_W_Bush_0001.jpg
ADDED
|
assets/gallery/Gerhard_Schroeder_0001.jpg
ADDED
|
assets/gallery/Hugo_Chavez_0001.jpg
ADDED
|
assets/gallery/Junichiro_Koizumi_0001.jpg
ADDED
|
assets/gallery/README.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
This directory stores the fixed LFW-derived reviewer gallery used by the public Hugging Face demo. The packaged gallery is intentionally small, explicit, and deployment-friendly.
|
assets/gallery/Tony_Blair_0001.jpg
ADDED
|
assets/gallery/gallery_manifest.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"person": "George_W_Bush",
|
| 4 |
+
"gallery_filename": "George_W_Bush_0001.jpg",
|
| 5 |
+
"query_filename": "query_George_W_Bush_0002.jpg",
|
| 6 |
+
"gallery_path": "George_W_Bush_0001.jpg",
|
| 7 |
+
"query_path": "query_George_W_Bush_0002.jpg"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"person": "Colin_Powell",
|
| 11 |
+
"gallery_filename": "Colin_Powell_0001.jpg",
|
| 12 |
+
"query_filename": "query_Colin_Powell_0002.jpg",
|
| 13 |
+
"gallery_path": "Colin_Powell_0001.jpg",
|
| 14 |
+
"query_path": "query_Colin_Powell_0002.jpg"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"person": "Tony_Blair",
|
| 18 |
+
"gallery_filename": "Tony_Blair_0001.jpg",
|
| 19 |
+
"query_filename": "query_Tony_Blair_0002.jpg",
|
| 20 |
+
"gallery_path": "Tony_Blair_0001.jpg",
|
| 21 |
+
"query_path": "query_Tony_Blair_0002.jpg"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"person": "Donald_Rumsfeld",
|
| 25 |
+
"gallery_filename": "Donald_Rumsfeld_0001.jpg",
|
| 26 |
+
"query_filename": "query_Donald_Rumsfeld_0002.jpg",
|
| 27 |
+
"gallery_path": "Donald_Rumsfeld_0001.jpg",
|
| 28 |
+
"query_path": "query_Donald_Rumsfeld_0002.jpg"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"person": "Gerhard_Schroeder",
|
| 32 |
+
"gallery_filename": "Gerhard_Schroeder_0001.jpg",
|
| 33 |
+
"query_filename": "query_Gerhard_Schroeder_0002.jpg",
|
| 34 |
+
"gallery_path": "Gerhard_Schroeder_0001.jpg",
|
| 35 |
+
"query_path": "query_Gerhard_Schroeder_0002.jpg"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"person": "Ariel_Sharon",
|
| 39 |
+
"gallery_filename": "Ariel_Sharon_0001.jpg",
|
| 40 |
+
"query_filename": "query_Ariel_Sharon_0002.jpg",
|
| 41 |
+
"gallery_path": "Ariel_Sharon_0001.jpg",
|
| 42 |
+
"query_path": "query_Ariel_Sharon_0002.jpg"
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"person": "Hugo_Chavez",
|
| 46 |
+
"gallery_filename": "Hugo_Chavez_0001.jpg",
|
| 47 |
+
"query_filename": "query_Hugo_Chavez_0002.jpg",
|
| 48 |
+
"gallery_path": "Hugo_Chavez_0001.jpg",
|
| 49 |
+
"query_path": "query_Hugo_Chavez_0002.jpg"
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"person": "Junichiro_Koizumi",
|
| 53 |
+
"gallery_filename": "Junichiro_Koizumi_0001.jpg",
|
| 54 |
+
"query_filename": "query_Junichiro_Koizumi_0002.jpg",
|
| 55 |
+
"gallery_path": "Junichiro_Koizumi_0001.jpg",
|
| 56 |
+
"query_path": "query_Junichiro_Koizumi_0002.jpg"
|
| 57 |
+
}
|
| 58 |
+
]
|
assets/gallery/lfw_demo_calibration.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f2311f8e9eaefc33c921a9df7d3110038fe8c94b63eead35cda248985fd9b726
|
| 3 |
+
size 152062
|
assets/gallery/lfw_demo_gallery.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5e812c24e8c5c1ca3f2e026223736e95c8f74ba01273388d82a332b695cc2a90
|
| 3 |
+
size 18526
|
assets/videos/README.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Short uploaded video analysis is planned in the code layout but not enabled as the primary reviewer path in the first deployment. Place approved short demo clips in this directory if the video tab is activated later.
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
numpy>=1.24.0
|
| 3 |
+
pandas>=2.0.0
|
| 4 |
+
scikit-learn>=1.3.0
|
| 5 |
+
Pillow>=10.0.0
|
| 6 |
+
opencv-python-headless>=4.9.0.80
|
| 7 |
+
facenet-pytorch>=2.6.0
|
| 8 |
+
torch>=2.1.0
|
| 9 |
+
torchvision>=0.16.0
|
| 10 |
+
matplotlib>=3.7.0
|
src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""FuzzyPSI-hamming Hugging Face deployment package."""
|
src/config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 7 |
+
ASSETS_DIR = PROJECT_ROOT / "assets"
|
| 8 |
+
GALLERY_DIR = ASSETS_DIR / "gallery"
|
| 9 |
+
EXAMPLES_DIR = ASSETS_DIR / "examples"
|
| 10 |
+
VIDEOS_DIR = ASSETS_DIR / "videos"
|
| 11 |
+
OUTPUT_DIR = PROJECT_ROOT / "out"
|
| 12 |
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 13 |
+
|
| 14 |
+
SPACE_TITLE = "FuzzyPSI-hamming"
|
| 15 |
+
SPACE_SUBTITLE = "Deployment-oriented demo for privacy-preserving fuzzy private set intersection over Hamming distance"
|
| 16 |
+
|
| 17 |
+
DEFAULT_MODE = os.environ.get("FPSI_DEMO_MODE", "simulation")
|
| 18 |
+
SUPPORTED_MODES = ("simulation", "full")
|
| 19 |
+
|
| 20 |
+
DEFAULT_DIMENSIONS = (128, 256, 512)
|
| 21 |
+
DEFAULT_DIM = 128
|
| 22 |
+
DEFAULT_SECURITY_LAMBDA = 40
|
| 23 |
+
DEFAULT_N_SENDER = 512
|
| 24 |
+
DEFAULT_N_RECEIVER = 100
|
| 25 |
+
DEFAULT_FRAME_SAMPLE_LIMIT = 8
|
| 26 |
+
DEFAULT_VIDEO_SECONDS = 12
|
| 27 |
+
|
| 28 |
+
LFW_FEATURES_PATH = os.environ.get("LFW_FEATURES_PATH", str(GALLERY_DIR / "lfw_demo_gallery.npz"))
|
| 29 |
+
LFW_IMAGE_ROOT = os.environ.get("LFW_IMAGE_ROOT", "")
|
| 30 |
+
|
| 31 |
+
FULL_PROTOCOL_ENABLED = os.environ.get("FPSI_ENABLE_FULL_PROTOCOL", "0") == "1"
|
| 32 |
+
FULL_PROTOCOL_DIR = Path(os.environ.get("FPSI_PROTOCOL_ROOT", str(PROJECT_ROOT / "runtime")))
|
| 33 |
+
FULL_PROTOCOL_BUILD_DIR = Path(os.environ.get("FPSI_PROTOCOL_BUILD_DIR", str(FULL_PROTOCOL_DIR / "build")))
|
| 34 |
+
|
| 35 |
+
FACE_MODEL_DEVICE = os.environ.get("FPSI_FACE_DEVICE", "cpu")
|
| 36 |
+
MATCH_THRESHOLD_MARGIN = float(os.environ.get("FPSI_THRESHOLD_MARGIN", "0.0"))
|
src/data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Gallery and packaged asset loaders."""
|
src/data/gallery_store.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
from src import config
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class GalleryRecord:
|
| 15 |
+
person: str
|
| 16 |
+
gallery_filename: str
|
| 17 |
+
query_filename: str
|
| 18 |
+
gallery_path: str
|
| 19 |
+
query_path: str
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DemoGallery:
|
| 23 |
+
def __init__(self) -> None:
|
| 24 |
+
gallery_npz = config.GALLERY_DIR / 'lfw_demo_gallery.npz'
|
| 25 |
+
manifest_path = config.GALLERY_DIR / 'gallery_manifest.json'
|
| 26 |
+
example_manifest_path = config.EXAMPLES_DIR / 'examples_manifest.json'
|
| 27 |
+
|
| 28 |
+
data = np.load(gallery_npz, allow_pickle=True)
|
| 29 |
+
self.features = data['features'].astype(np.float32)
|
| 30 |
+
self.people = data['persons']
|
| 31 |
+
self.filenames = data['filenames']
|
| 32 |
+
|
| 33 |
+
calibration_npz = config.GALLERY_DIR / 'lfw_demo_calibration.npz'
|
| 34 |
+
calibration = np.load(calibration_npz, allow_pickle=True)
|
| 35 |
+
self.calibration_features = calibration['features'].astype(np.float32)
|
| 36 |
+
self.calibration_people = calibration['persons']
|
| 37 |
+
self.calibration_filenames = calibration['filenames']
|
| 38 |
+
|
| 39 |
+
with open(manifest_path) as fh:
|
| 40 |
+
self.records = [GalleryRecord(**row) for row in json.load(fh)]
|
| 41 |
+
with open(example_manifest_path) as fh:
|
| 42 |
+
self.examples = json.load(fh)
|
| 43 |
+
|
| 44 |
+
def summary(self) -> dict[str, object]:
|
| 45 |
+
return {
|
| 46 |
+
'gallery_size': int(len(self.people)),
|
| 47 |
+
'calibration_size': int(len(self.calibration_people)),
|
| 48 |
+
'people': [str(x.person) for x in self.records],
|
| 49 |
+
'dimensions': int(self.features.shape[1]),
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
def example_choices(self) -> list[tuple[str, str]]:
|
| 53 |
+
return [(item['person'], str(config.EXAMPLES_DIR / item['filename'])) for item in self.examples]
|
| 54 |
+
|
| 55 |
+
def load_example_image(self, filename: str) -> Image.Image:
|
| 56 |
+
return Image.open(config.EXAMPLES_DIR / filename).convert('RGB')
|
| 57 |
+
|
| 58 |
+
def gallery_image_path(self, gallery_filename: str) -> Path:
|
| 59 |
+
return config.GALLERY_DIR / gallery_filename
|
src/pipeline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Media preprocessing and embedding extraction."""
|
src/pipeline/image_pipeline.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from PIL import Image, ImageOps, ImageStat
|
| 9 |
+
import torch
|
| 10 |
+
from facenet_pytorch import MTCNN, InceptionResnetV1
|
| 11 |
+
|
| 12 |
+
from src import config
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class ImageAnalysis:
|
| 17 |
+
face_detected: bool
|
| 18 |
+
embedding: np.ndarray
|
| 19 |
+
preview: Image.Image
|
| 20 |
+
notes: list[str]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ImagePipeline:
|
| 24 |
+
def __init__(self) -> None:
|
| 25 |
+
self.device = torch.device(config.FACE_MODEL_DEVICE if torch.cuda.is_available() and config.FACE_MODEL_DEVICE == 'cuda' else 'cpu')
|
| 26 |
+
self.mtcnn = MTCNN(image_size=160, margin=16, post_process=True, device=self.device)
|
| 27 |
+
self.resnet = InceptionResnetV1(pretrained='vggface2').eval().to(self.device)
|
| 28 |
+
|
| 29 |
+
def analyze(self, image: Image.Image | np.ndarray | str | Path) -> ImageAnalysis:
|
| 30 |
+
pil = self._to_pil(image)
|
| 31 |
+
pil = ImageOps.exif_transpose(pil).convert('RGB')
|
| 32 |
+
notes: list[str] = []
|
| 33 |
+
|
| 34 |
+
face = self.mtcnn(pil)
|
| 35 |
+
if face is None:
|
| 36 |
+
notes.append('No face detected; falling back to center crop for demo continuity.')
|
| 37 |
+
preview = ImageOps.fit(pil, (220, 220))
|
| 38 |
+
face = self.mtcnn(preview)
|
| 39 |
+
if face is None:
|
| 40 |
+
face = self._fallback_tensor(preview)
|
| 41 |
+
notes.append('Used RGB center-crop fallback embedding path.')
|
| 42 |
+
face_detected = False
|
| 43 |
+
else:
|
| 44 |
+
face_detected = True
|
| 45 |
+
else:
|
| 46 |
+
preview = ImageOps.fit(pil, (220, 220))
|
| 47 |
+
face_detected = True
|
| 48 |
+
|
| 49 |
+
with torch.no_grad():
|
| 50 |
+
embedding = self.resnet(face.unsqueeze(0).to(self.device)).cpu().numpy()[0].astype(np.float32)
|
| 51 |
+
|
| 52 |
+
brightness = float(sum(ImageStat.Stat(preview).mean) / 3.0)
|
| 53 |
+
notes.append(f'Average preview brightness: {brightness:.1f}')
|
| 54 |
+
notes.append(f'Embedding device: {self.device.type}')
|
| 55 |
+
return ImageAnalysis(face_detected=face_detected, embedding=embedding, preview=preview, notes=notes)
|
| 56 |
+
|
| 57 |
+
def _to_pil(self, image: Image.Image | np.ndarray | str | Path) -> Image.Image:
|
| 58 |
+
if isinstance(image, Image.Image):
|
| 59 |
+
return image
|
| 60 |
+
if isinstance(image, np.ndarray):
|
| 61 |
+
return Image.fromarray(image.astype(np.uint8))
|
| 62 |
+
return Image.open(image)
|
| 63 |
+
|
| 64 |
+
def _fallback_tensor(self, image: Image.Image) -> torch.Tensor:
|
| 65 |
+
array = np.asarray(ImageOps.fit(image, (160, 160))).astype(np.float32) / 255.0
|
| 66 |
+
tensor = torch.from_numpy(array).permute(2, 0, 1)
|
| 67 |
+
tensor = (tensor - 0.5) / 0.5
|
| 68 |
+
return tensor
|
src/pipeline/video_pipeline.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import cv2
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
from src import config
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class VideoFrameSample:
|
| 14 |
+
timestamp_s: float
|
| 15 |
+
image: Image.Image
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class VideoPipeline:
|
| 19 |
+
def sample_frames(self, video_path: str | Path, max_frames: int | None = None) -> list[VideoFrameSample]:
|
| 20 |
+
max_frames = max_frames or config.DEFAULT_FRAME_SAMPLE_LIMIT
|
| 21 |
+
cap = cv2.VideoCapture(str(video_path))
|
| 22 |
+
if not cap.isOpened():
|
| 23 |
+
raise ValueError('Unable to open uploaded video.')
|
| 24 |
+
|
| 25 |
+
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
| 26 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
| 27 |
+
if total_frames == 0:
|
| 28 |
+
raise ValueError('Uploaded video contains no readable frames.')
|
| 29 |
+
|
| 30 |
+
step = max(total_frames // max_frames, 1)
|
| 31 |
+
samples: list[VideoFrameSample] = []
|
| 32 |
+
frame_idx = 0
|
| 33 |
+
while len(samples) < max_frames:
|
| 34 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
| 35 |
+
ok, frame = cap.read()
|
| 36 |
+
if not ok:
|
| 37 |
+
break
|
| 38 |
+
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 39 |
+
samples.append(VideoFrameSample(timestamp_s=frame_idx / fps, image=Image.fromarray(rgb)))
|
| 40 |
+
frame_idx += step
|
| 41 |
+
cap.release()
|
| 42 |
+
return samples
|
src/protocol/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Protocol adapters and parameter helpers."""
|
src/protocol/elsh_params.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def collision_probability(d: int, delta: int, k: int) -> float:
|
| 7 |
+
denom = math.comb(d, k)
|
| 8 |
+
total = 0.0
|
| 9 |
+
for r in range(0, min(k, delta) + 1, 2):
|
| 10 |
+
total += math.comb(delta, r) * math.comb(d - delta, k - r) / denom
|
| 11 |
+
return total
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def recommended_params(d: int, delta: int, lam: int = 40) -> tuple[int, float, int]:
|
| 15 |
+
k = math.ceil(d / (delta + 1))
|
| 16 |
+
p_delta = collision_probability(d, delta, k)
|
| 17 |
+
p_delta = min(max(p_delta, 1e-9), 1.0 - 1e-9)
|
| 18 |
+
l_min = math.ceil(lam / (-math.log2(1.0 - p_delta)))
|
| 19 |
+
return k, p_delta, l_min
|
src/protocol/fpsi_adapter.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
import os
|
| 6 |
+
import random
|
| 7 |
+
import subprocess
|
| 8 |
+
import time
|
| 9 |
+
from dataclasses import dataclass, asdict
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from sklearn.decomposition import PCA
|
| 15 |
+
from sklearn.preprocessing import normalize
|
| 16 |
+
|
| 17 |
+
from src import config
|
| 18 |
+
from src.protocol.elsh_params import recommended_params
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class MatchResult:
|
| 23 |
+
person: str
|
| 24 |
+
filename: str
|
| 25 |
+
hamming_distance: int
|
| 26 |
+
matched: bool
|
| 27 |
+
score: float
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class ProtocolSummary:
|
| 32 |
+
mode: str
|
| 33 |
+
dim: int
|
| 34 |
+
delta: int
|
| 35 |
+
L: int
|
| 36 |
+
k: int
|
| 37 |
+
p_delta: float
|
| 38 |
+
communication_mb: float
|
| 39 |
+
time_s: float
|
| 40 |
+
gao_feasible: bool
|
| 41 |
+
tar: float
|
| 42 |
+
far: float
|
| 43 |
+
accuracy: float
|
| 44 |
+
gallery_size: int
|
| 45 |
+
query_count: int
|
| 46 |
+
binary_density: float
|
| 47 |
+
notes: list[str]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class FuzzyPSIAdapter:
|
| 51 |
+
def __init__(self, mode: str | None = None) -> None:
|
| 52 |
+
self.mode = mode or config.DEFAULT_MODE
|
| 53 |
+
if self.mode not in config.SUPPORTED_MODES:
|
| 54 |
+
self.mode = "simulation"
|
| 55 |
+
|
| 56 |
+
def binarize(self, features: np.ndarray, target_dim: int) -> tuple[np.ndarray, float]:
|
| 57 |
+
max_components = min(features.shape[0], features.shape[1])
|
| 58 |
+
if target_dim < features.shape[1] and target_dim <= max_components:
|
| 59 |
+
pca = PCA(n_components=target_dim, random_state=42)
|
| 60 |
+
projected = pca.fit_transform(features)
|
| 61 |
+
variance = float(pca.explained_variance_ratio_.sum())
|
| 62 |
+
elif target_dim < features.shape[1]:
|
| 63 |
+
projected = features[:, :target_dim]
|
| 64 |
+
variance = min(1.0, float(target_dim / features.shape[1]))
|
| 65 |
+
else:
|
| 66 |
+
projected = features[:, :target_dim]
|
| 67 |
+
variance = 1.0
|
| 68 |
+
projected = normalize(projected)
|
| 69 |
+
binary_codes = (projected > 0).astype(np.uint8)
|
| 70 |
+
return binary_codes, variance
|
| 71 |
+
|
| 72 |
+
def calibrate_threshold(self, binary_codes: np.ndarray, labels: np.ndarray) -> dict[str, float | int]:
|
| 73 |
+
rng = np.random.default_rng(42)
|
| 74 |
+
label_to_idx: dict[Any, list[int]] = {}
|
| 75 |
+
for i, label in enumerate(labels):
|
| 76 |
+
label_to_idx.setdefault(label, []).append(i)
|
| 77 |
+
|
| 78 |
+
multi_labels = [label for label, idx in label_to_idx.items() if len(idx) >= 2]
|
| 79 |
+
unique_labels = np.array(list(label_to_idx.keys()))
|
| 80 |
+
|
| 81 |
+
genuine_dists: list[int] = []
|
| 82 |
+
impostor_dists: list[int] = []
|
| 83 |
+
for label in multi_labels[:300]:
|
| 84 |
+
indices = label_to_idx[label]
|
| 85 |
+
for i in range(min(len(indices) - 1, 3)):
|
| 86 |
+
genuine_dists.append(int(np.sum(binary_codes[indices[i]] != binary_codes[indices[i + 1]])))
|
| 87 |
+
|
| 88 |
+
for _ in range(max(len(genuine_dists) * 2, 32)):
|
| 89 |
+
l1, l2 = rng.choice(unique_labels, 2, replace=False)
|
| 90 |
+
i1 = rng.choice(label_to_idx[l1])
|
| 91 |
+
i2 = rng.choice(label_to_idx[l2])
|
| 92 |
+
impostor_dists.append(int(np.sum(binary_codes[i1] != binary_codes[i2])))
|
| 93 |
+
|
| 94 |
+
genuine = np.array(genuine_dists if genuine_dists else [0])
|
| 95 |
+
impostor = np.array(impostor_dists if impostor_dists else [binary_codes.shape[1]])
|
| 96 |
+
|
| 97 |
+
best_delta = 0
|
| 98 |
+
best_acc = 0.0
|
| 99 |
+
best_tar = 0.0
|
| 100 |
+
best_far = 1.0
|
| 101 |
+
for delta in range(binary_codes.shape[1]):
|
| 102 |
+
tar = float(np.mean(genuine <= delta))
|
| 103 |
+
far = float(np.mean(impostor <= delta))
|
| 104 |
+
acc = (tar + (1.0 - far)) / 2.0
|
| 105 |
+
if acc > best_acc:
|
| 106 |
+
best_delta = delta
|
| 107 |
+
best_acc = acc
|
| 108 |
+
best_tar = tar
|
| 109 |
+
best_far = far
|
| 110 |
+
|
| 111 |
+
adjusted_delta = max(0, int(best_delta + config.MATCH_THRESHOLD_MARGIN))
|
| 112 |
+
return {
|
| 113 |
+
"delta": adjusted_delta,
|
| 114 |
+
"tar": best_tar,
|
| 115 |
+
"far": best_far,
|
| 116 |
+
"accuracy": best_acc,
|
| 117 |
+
"genuine_mean": float(genuine.mean()),
|
| 118 |
+
"impostor_mean": float(impostor.mean()),
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
def gao_feasible(self, dim: int, delta: int) -> bool:
|
| 122 |
+
return dim > 8 * delta + 8
|
| 123 |
+
|
| 124 |
+
def select_l(self, dim: int, delta: int) -> tuple[int, int, float]:
|
| 125 |
+
k, p_delta, l_min = recommended_params(dim, delta, config.DEFAULT_SECURITY_LAMBDA)
|
| 126 |
+
return l_min, k, p_delta
|
| 127 |
+
|
| 128 |
+
def query_against_gallery(
|
| 129 |
+
self,
|
| 130 |
+
query_feature: np.ndarray,
|
| 131 |
+
gallery_features: np.ndarray,
|
| 132 |
+
gallery_people: np.ndarray,
|
| 133 |
+
gallery_filenames: np.ndarray,
|
| 134 |
+
dim: int,
|
| 135 |
+
calibration_features: np.ndarray | None = None,
|
| 136 |
+
calibration_people: np.ndarray | None = None,
|
| 137 |
+
) -> tuple[MatchResult, ProtocolSummary, dict[str, Any]]:
|
| 138 |
+
calibration_features = gallery_features if calibration_features is None else calibration_features
|
| 139 |
+
calibration_people = gallery_people if calibration_people is None else calibration_people
|
| 140 |
+
|
| 141 |
+
binarization_pool = np.vstack([query_feature.reshape(1, -1), calibration_features])
|
| 142 |
+
pool_binary, variance = self.binarize(binarization_pool, dim)
|
| 143 |
+
query_binary = pool_binary[0]
|
| 144 |
+
calibration_binary = pool_binary[1:]
|
| 145 |
+
|
| 146 |
+
gallery_pool = np.vstack([query_feature.reshape(1, -1), gallery_features])
|
| 147 |
+
gallery_binary = self.binarize(gallery_pool, dim)[0][1:]
|
| 148 |
+
|
| 149 |
+
labels = np.concatenate([np.array(["__query__"], dtype=object), calibration_people.astype(object)])
|
| 150 |
+
threshold_stats = self.calibrate_threshold(pool_binary, labels)
|
| 151 |
+
delta = int(threshold_stats["delta"])
|
| 152 |
+
L, k, p_delta = self.select_l(dim, delta)
|
| 153 |
+
distances = np.sum(gallery_binary != query_binary, axis=1)
|
| 154 |
+
best_idx = int(np.argmin(distances))
|
| 155 |
+
best_dist = int(distances[best_idx])
|
| 156 |
+
matched = best_dist <= delta
|
| 157 |
+
density = float(np.mean(query_binary))
|
| 158 |
+
score = 1.0 - (best_dist / max(dim, 1))
|
| 159 |
+
|
| 160 |
+
if self.mode == "full" and config.FULL_PROTOCOL_ENABLED:
|
| 161 |
+
communication_mb, protocol_time, full_notes = self._run_full_protocol(gallery_binary, query_binary, dim, delta, L)
|
| 162 |
+
notes = ["full protocol mode"] + full_notes
|
| 163 |
+
else:
|
| 164 |
+
communication_mb, protocol_time = self._simulate_protocol_metrics(gallery_binary, query_binary, dim, delta, L)
|
| 165 |
+
notes = ["simulation mode", "full protocol disabled or unavailable"]
|
| 166 |
+
|
| 167 |
+
summary = ProtocolSummary(
|
| 168 |
+
mode=self.mode if self.mode == "simulation" or config.FULL_PROTOCOL_ENABLED else "simulation",
|
| 169 |
+
dim=dim,
|
| 170 |
+
delta=delta,
|
| 171 |
+
L=L,
|
| 172 |
+
k=k,
|
| 173 |
+
p_delta=p_delta,
|
| 174 |
+
communication_mb=communication_mb,
|
| 175 |
+
time_s=protocol_time,
|
| 176 |
+
gao_feasible=self.gao_feasible(dim, delta),
|
| 177 |
+
tar=float(threshold_stats["tar"]),
|
| 178 |
+
far=float(threshold_stats["far"]),
|
| 179 |
+
accuracy=float(threshold_stats["accuracy"]),
|
| 180 |
+
gallery_size=int(len(gallery_features)),
|
| 181 |
+
query_count=1,
|
| 182 |
+
binary_density=density,
|
| 183 |
+
notes=notes,
|
| 184 |
+
)
|
| 185 |
+
match = MatchResult(
|
| 186 |
+
person=str(gallery_people[best_idx]),
|
| 187 |
+
filename=str(gallery_filenames[best_idx]),
|
| 188 |
+
hamming_distance=best_dist,
|
| 189 |
+
matched=matched,
|
| 190 |
+
score=score,
|
| 191 |
+
)
|
| 192 |
+
details = {
|
| 193 |
+
"variance_explained": variance,
|
| 194 |
+
"query_binary": query_binary.tolist(),
|
| 195 |
+
"best_index": best_idx,
|
| 196 |
+
"top5_distances": [int(x) for x in np.sort(distances)[:5]],
|
| 197 |
+
"calibration_size": int(len(calibration_binary)),
|
| 198 |
+
}
|
| 199 |
+
return match, summary, details
|
| 200 |
+
|
| 201 |
+
def _simulate_protocol_metrics(
|
| 202 |
+
self,
|
| 203 |
+
gallery_binary: np.ndarray,
|
| 204 |
+
query_binary: np.ndarray,
|
| 205 |
+
dim: int,
|
| 206 |
+
delta: int,
|
| 207 |
+
L: int,
|
| 208 |
+
) -> tuple[float, float]:
|
| 209 |
+
gallery_size = len(gallery_binary)
|
| 210 |
+
hit_ratio = max(0.02, min(0.35, (delta + 1) / max(dim, 1) * 6.0))
|
| 211 |
+
candidate_count = max(1, int(gallery_size * hit_ratio))
|
| 212 |
+
communication_mb = (L * dim + candidate_count * 16 + 500000) / (1024.0 * 1024.0)
|
| 213 |
+
base_time = 0.05 + candidate_count * 0.0012 + L * 0.001
|
| 214 |
+
return float(communication_mb), float(base_time)
|
| 215 |
+
|
| 216 |
+
def _run_full_protocol(
|
| 217 |
+
self,
|
| 218 |
+
gallery_binary: np.ndarray,
|
| 219 |
+
query_binary: np.ndarray,
|
| 220 |
+
dim: int,
|
| 221 |
+
delta: int,
|
| 222 |
+
L: int,
|
| 223 |
+
) -> tuple[float, float, list[str]]:
|
| 224 |
+
sender_path, receiver_path = self._write_binary_pair(gallery_binary, query_binary, dim)
|
| 225 |
+
build_dir = config.FULL_PROTOCOL_BUILD_DIR
|
| 226 |
+
receiver_bin = build_dir / "fpsi_receiver"
|
| 227 |
+
sender_bin = build_dir / "fpsi_sender"
|
| 228 |
+
if not receiver_bin.exists() or not sender_bin.exists():
|
| 229 |
+
communication_mb, protocol_time = self._simulate_protocol_metrics(gallery_binary, query_binary, dim, delta, L)
|
| 230 |
+
return communication_mb, protocol_time, ["native binaries missing; fell back to simulated metrics"]
|
| 231 |
+
|
| 232 |
+
port = 26000 + random.randint(0, 900)
|
| 233 |
+
recv_cmd = [str(receiver_bin), str(port), str(len(query_binary.reshape(1, -1))), str(dim), str(delta), str(L)]
|
| 234 |
+
send_cmd = [str(sender_bin), "127.0.0.1", str(port), str(len(gallery_binary)), str(dim), str(delta), str(L)]
|
| 235 |
+
|
| 236 |
+
recv_proc = subprocess.Popen(recv_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
| 237 |
+
time.sleep(1.0)
|
| 238 |
+
send_proc = subprocess.Popen(send_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
| 239 |
+
send_out, send_err = send_proc.communicate(timeout=300)
|
| 240 |
+
recv_out, recv_err = recv_proc.communicate(timeout=300)
|
| 241 |
+
comm_mb = 0.0
|
| 242 |
+
total_time = 0.0
|
| 243 |
+
for line in recv_out.splitlines():
|
| 244 |
+
if "Total:" in line:
|
| 245 |
+
parts = line.split()
|
| 246 |
+
for i, token in enumerate(parts):
|
| 247 |
+
if token.endswith("s,"):
|
| 248 |
+
total_time = float(token[:-2])
|
| 249 |
+
elif token == "MB":
|
| 250 |
+
comm_mb = float(parts[i - 1])
|
| 251 |
+
notes = []
|
| 252 |
+
if send_err.strip():
|
| 253 |
+
notes.append(f"sender stderr: {send_err.strip()[:200]}")
|
| 254 |
+
if recv_err.strip():
|
| 255 |
+
notes.append(f"receiver stderr: {recv_err.strip()[:200]}")
|
| 256 |
+
if comm_mb == 0.0 and total_time == 0.0:
|
| 257 |
+
sim_comm, sim_time = self._simulate_protocol_metrics(gallery_binary, query_binary, dim, delta, L)
|
| 258 |
+
return sim_comm, sim_time, notes + ["native run returned no parsable totals; using simulated metrics"]
|
| 259 |
+
return comm_mb, total_time, notes
|
| 260 |
+
|
| 261 |
+
def _write_binary_pair(self, gallery_binary: np.ndarray, query_binary: np.ndarray, dim: int) -> tuple[Path, Path]:
|
| 262 |
+
runtime_dir = config.OUTPUT_DIR / "runtime_inputs"
|
| 263 |
+
runtime_dir.mkdir(parents=True, exist_ok=True)
|
| 264 |
+
sender_path = runtime_dir / f"sender_d{dim}.bin"
|
| 265 |
+
receiver_path = runtime_dir / f"receiver_d{dim}.bin"
|
| 266 |
+
|
| 267 |
+
self._write_binary_dataset(sender_path, gallery_binary)
|
| 268 |
+
self._write_binary_dataset(receiver_path, query_binary.reshape(1, -1))
|
| 269 |
+
return sender_path, receiver_path
|
| 270 |
+
|
| 271 |
+
@staticmethod
|
| 272 |
+
def _write_binary_dataset(path: Path, vectors: np.ndarray) -> None:
|
| 273 |
+
vectors = np.asarray(vectors, dtype=np.uint8)
|
| 274 |
+
n, d = vectors.shape
|
| 275 |
+
with open(path, "wb") as fh:
|
| 276 |
+
fh.write(int(n).to_bytes(4, "little"))
|
| 277 |
+
fh.write(int(d).to_bytes(4, "little"))
|
| 278 |
+
fh.write(vectors.tobytes())
|
| 279 |
+
|
| 280 |
+
def export_summary(self, match: MatchResult, summary: ProtocolSummary, details: dict[str, Any]) -> str:
|
| 281 |
+
payload = {
|
| 282 |
+
"match": asdict(match),
|
| 283 |
+
"summary": asdict(summary),
|
| 284 |
+
"details": details,
|
| 285 |
+
}
|
| 286 |
+
return json.dumps(payload, indent=2)
|
src/protocol/runtime_stub.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
This deployment project defaults to simulation mode. Full native protocol mode can be enabled later by provisioning a built FPSI runtime under runtime/build and setting FPSI_ENABLE_FULL_PROTOCOL=1.
|
src/ui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""UI helpers for the Hugging Face demo."""
|
src/ui/components.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
from src import config
|
| 11 |
+
from src.data.gallery_store import DemoGallery
|
| 12 |
+
from src.pipeline.image_pipeline import ImagePipeline
|
| 13 |
+
from src.protocol.fpsi_adapter import FuzzyPSIAdapter
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
gallery = DemoGallery()
|
| 17 |
+
image_pipeline = ImagePipeline()
|
| 18 |
+
adapter = FuzzyPSIAdapter(config.DEFAULT_MODE)
|
| 19 |
+
|
| 20 |
+
CSS = """
|
| 21 |
+
.hero {padding: 20px 24px; border-radius: 18px; background: linear-gradient(135deg, #102a43 0%, #243b53 50%, #334e68 100%); color: white;}
|
| 22 |
+
.metric-card {padding: 14px 16px; border-radius: 14px; background: #f8fafc; border: 1px solid #d9e2ec;}
|
| 23 |
+
.note-card {padding: 14px 16px; border-radius: 14px; background: #f4f7fb; border-left: 4px solid #486581;}
|
| 24 |
+
.small-muted {color: #627d98; font-size: 0.95rem;}
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _example_paths() -> list[list[str]]:
|
| 29 |
+
return [[path] for _, path in gallery.example_choices()]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _system_status() -> str:
|
| 33 |
+
summary = gallery.summary()
|
| 34 |
+
return json.dumps(
|
| 35 |
+
{
|
| 36 |
+
"mode": adapter.mode,
|
| 37 |
+
"full_protocol_enabled": config.FULL_PROTOCOL_ENABLED,
|
| 38 |
+
"gallery_size": summary["gallery_size"],
|
| 39 |
+
"gallery_feature_dim": summary["dimensions"],
|
| 40 |
+
"supported_dims": list(config.DEFAULT_DIMENSIONS),
|
| 41 |
+
"device": config.FACE_MODEL_DEVICE,
|
| 42 |
+
},
|
| 43 |
+
indent=2,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _protocol_story(dim: int, summary_json: str) -> pd.DataFrame:
|
| 48 |
+
payload = json.loads(summary_json)
|
| 49 |
+
summary = payload["summary"]
|
| 50 |
+
rows = [
|
| 51 |
+
("1. Image frontend", "Uploaded face image is normalized and passed through the demo embedding stack."),
|
| 52 |
+
("2. Binary projection", f"Embedding is binarized to d={dim} bits for Hamming-space matching."),
|
| 53 |
+
("3. Candidate generation", f"E-LSH is parameterized with L={summary['L']} and k={summary['k']}."),
|
| 54 |
+
("4. Exact verification", f"Final decision uses δ={summary['delta']} with backend mode={summary['mode']}."),
|
| 55 |
+
("5. Reviewer metrics", f"Communication={summary['communication_mb']:.3f} MB, latency={summary['time_s']:.3f} s, Gao feasible={summary['gao_feasible']}."),
|
| 56 |
+
]
|
| 57 |
+
return pd.DataFrame(rows, columns=["Stage", "Deployment-facing explanation"])
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def run_image_demo(image: Image.Image, dim: int, mode: str):
|
| 61 |
+
if image is None:
|
| 62 |
+
raise gr.Error("Please upload or capture a face image.")
|
| 63 |
+
|
| 64 |
+
adapter.mode = mode
|
| 65 |
+
analysis = image_pipeline.analyze(image)
|
| 66 |
+
match, summary, details = adapter.query_against_gallery(
|
| 67 |
+
analysis.embedding,
|
| 68 |
+
gallery.features,
|
| 69 |
+
gallery.people,
|
| 70 |
+
gallery.filenames,
|
| 71 |
+
dim,
|
| 72 |
+
calibration_features=gallery.calibration_features,
|
| 73 |
+
calibration_people=gallery.calibration_people,
|
| 74 |
+
)
|
| 75 |
+
gallery_image = Image.open(gallery.gallery_image_path(match.filename)).convert("RGB")
|
| 76 |
+
summary_json = adapter.export_summary(match, summary, details)
|
| 77 |
+
result_md = f"""
|
| 78 |
+
### Match result
|
| 79 |
+
- **Predicted identity:** {match.person}
|
| 80 |
+
- **Matched gallery image:** `{match.filename}`
|
| 81 |
+
- **Hamming distance:** {match.hamming_distance}
|
| 82 |
+
- **Decision:** {'Match accepted' if match.matched else 'No match under current threshold'}
|
| 83 |
+
- **Similarity-style score:** {match.score:.4f}
|
| 84 |
+
|
| 85 |
+
### Protocol metrics
|
| 86 |
+
- **Mode:** {summary.mode}
|
| 87 |
+
- **Dimension:** {summary.dim}
|
| 88 |
+
- **Threshold δ:** {summary.delta}
|
| 89 |
+
- **E-LSH L:** {summary.L}
|
| 90 |
+
- **Communication:** {summary.communication_mb:.3f} MB
|
| 91 |
+
- **Latency:** {summary.time_s:.3f} s
|
| 92 |
+
- **TAR / FAR / Acc:** {summary.tar:.4f} / {summary.far:.4f} / {summary.accuracy:.4f}
|
| 93 |
+
- **Gao feasibility:** {'YES' if summary.gao_feasible else 'NO'}
|
| 94 |
+
"""
|
| 95 |
+
notes = "\n".join(f"- {note}" for note in (analysis.notes + summary.notes))
|
| 96 |
+
metrics_df = pd.DataFrame([
|
| 97 |
+
("binary_density", round(summary.binary_density, 4)),
|
| 98 |
+
("variance_explained", round(details['variance_explained'], 4)),
|
| 99 |
+
("top5_distances", ", ".join(map(str, details['top5_distances']))),
|
| 100 |
+
("gallery_size", summary.gallery_size),
|
| 101 |
+
], columns=["Metric", "Value"])
|
| 102 |
+
return result_md, analysis.preview, gallery_image, summary_json, notes, metrics_df, _protocol_story(dim, summary_json)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def load_example(example_path: str):
|
| 106 |
+
return Image.open(example_path).convert("RGB")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def build_app() -> gr.Blocks:
|
| 110 |
+
with gr.Blocks(title=config.SPACE_TITLE, css=CSS) as demo:
|
| 111 |
+
gr.HTML(
|
| 112 |
+
f"""
|
| 113 |
+
<div class='hero'>
|
| 114 |
+
<h1>{config.SPACE_TITLE}</h1>
|
| 115 |
+
<p>{config.SPACE_SUBTITLE}</p>
|
| 116 |
+
<p>This reviewer-facing demo shows how the research artifact can be surfaced as a deployment-oriented application with image upload, protocol metrics, and a fixed LFW-derived gallery.</p>
|
| 117 |
+
</div>
|
| 118 |
+
"""
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
with gr.Row():
|
| 122 |
+
with gr.Column(scale=2):
|
| 123 |
+
gr.Markdown(
|
| 124 |
+
"""
|
| 125 |
+
### Deployment focus
|
| 126 |
+
- **Image-first MVP** with upload and webcam capture
|
| 127 |
+
- **Dual backend modes**: simulation by default, optional full protocol mode
|
| 128 |
+
- **Fixed LFW-derived reviewer gallery** for stable testing
|
| 129 |
+
- **Protocol explanation panels** to highlight engineering and deployment effort
|
| 130 |
+
"""
|
| 131 |
+
)
|
| 132 |
+
with gr.Column(scale=1):
|
| 133 |
+
gr.Code(_system_status(), language="json", label="System status")
|
| 134 |
+
|
| 135 |
+
with gr.Tabs():
|
| 136 |
+
with gr.Tab("Image test"):
|
| 137 |
+
with gr.Row():
|
| 138 |
+
with gr.Column(scale=1):
|
| 139 |
+
image_input = gr.Image(label="Upload or capture a query face", type="pil", sources=["upload", "webcam"])
|
| 140 |
+
dim_input = gr.Dropdown(choices=list(config.DEFAULT_DIMENSIONS), value=config.DEFAULT_DIM, label="Binary dimension")
|
| 141 |
+
mode_input = gr.Dropdown(choices=list(config.SUPPORTED_MODES), value=config.DEFAULT_MODE, label="Backend mode")
|
| 142 |
+
run_btn = gr.Button("Run FuzzyPSI-hamming demo", variant="primary")
|
| 143 |
+
gr.Examples(examples=_example_paths(), inputs=[image_input], label="Reviewer examples")
|
| 144 |
+
with gr.Column(scale=1):
|
| 145 |
+
preview_output = gr.Image(label="Processed query preview")
|
| 146 |
+
gallery_output = gr.Image(label="Best gallery match")
|
| 147 |
+
with gr.Column(scale=1):
|
| 148 |
+
result_output = gr.Markdown(label="Match result")
|
| 149 |
+
notes_output = gr.Markdown(label="Execution notes")
|
| 150 |
+
|
| 151 |
+
with gr.Row():
|
| 152 |
+
metrics_output = gr.Dataframe(label="Protocol metrics", interactive=False)
|
| 153 |
+
story_output = gr.Dataframe(label="How the pipeline maps to deployment", interactive=False)
|
| 154 |
+
summary_output = gr.Code(label="Detailed JSON summary", language="json")
|
| 155 |
+
|
| 156 |
+
run_btn.click(
|
| 157 |
+
run_image_demo,
|
| 158 |
+
inputs=[image_input, dim_input, mode_input],
|
| 159 |
+
outputs=[result_output, preview_output, gallery_output, summary_output, notes_output, metrics_output, story_output],
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
with gr.Tab("Protocol metrics"):
|
| 163 |
+
gr.Markdown(
|
| 164 |
+
"""
|
| 165 |
+
### Reviewer-visible protocol story
|
| 166 |
+
This deployment keeps the paper’s Hamming-FPSI logic visible through dimension, threshold, communication, latency, and Gao-feasibility metrics. The public Space defaults to simulation mode for reliability, while the project layout preserves a path for native protocol execution.
|
| 167 |
+
"""
|
| 168 |
+
)
|
| 169 |
+
metrics_seed = pd.DataFrame([
|
| 170 |
+
(128, "adaptive", "reported after query", "public default", "best for reliable reviewer experience"),
|
| 171 |
+
(256, "adaptive", "reported after query", "public default", "larger code space and stronger separation"),
|
| 172 |
+
(512, "adaptive", "reported after query", "optional heavy mode", "closest to high-dimensional biometric features"),
|
| 173 |
+
], columns=["d", "δ", "runtime metrics", "recommended mode", "deployment note"])
|
| 174 |
+
gr.Dataframe(value=metrics_seed, interactive=False)
|
| 175 |
+
|
| 176 |
+
with gr.Tab("How it works"):
|
| 177 |
+
gr.Markdown(
|
| 178 |
+
"""
|
| 179 |
+
### End-to-end deployment path
|
| 180 |
+
1. **Frontend intake**: upload or webcam image enters the Gradio service.
|
| 181 |
+
2. **Face embedding**: a deployment-friendly embedding stack converts the image into a dense vector.
|
| 182 |
+
3. **Binary projection**: the adapter ports the artifact’s binarization logic into an interactive service layer.
|
| 183 |
+
4. **E-LSH candidate generation**: deployment metrics expose the recommended `k`, collision probability, and `L` values.
|
| 184 |
+
5. **Exact verification**: the service returns the closest gallery identity together with thresholded Hamming-distance evidence.
|
| 185 |
+
6. **Reviewer instrumentation**: UI cards surface latency, communication, threshold, and Gao infeasibility in the same run.
|
| 186 |
+
"""
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
with gr.Tab("Engineering story"):
|
| 190 |
+
gr.Markdown(
|
| 191 |
+
"""
|
| 192 |
+
### Why this Space shows substantial engineering effort
|
| 193 |
+
- The original release artifact is **script-first** and **feature-file-driven**.
|
| 194 |
+
- This deployment adds a **new image ingestion layer**, **web interaction model**, **demo gallery packaging**, and **dual backend execution abstraction**.
|
| 195 |
+
- The deployment project is isolated from the research artifact so the web stack can evolve independently.
|
| 196 |
+
- The public interface is intentionally framed as a **factory-facing deployment prototype** rather than only a benchmark runner.
|
| 197 |
+
"""
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
with gr.Tab("Video path"):
|
| 201 |
+
gr.Markdown(
|
| 202 |
+
"""
|
| 203 |
+
### Planned short-video support
|
| 204 |
+
The current first release is image-first. The code layout already includes a video pipeline module so that short uploaded clip analysis can be enabled next without restructuring the app.
|
| 205 |
+
"""
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
return demo
|