An interactive display that uses a two-way mirror and facial recognition to match a user's face with a well-known figure from a selected category (scientists, engineers, or entrepreneurs). When a user steps up and activates the system, a webcam captures their face, computes a facial embedding, finds the closest visual match from a precomputed database, and displays the result on a monitor behind the mirror.
python -m model.dataset_builder- You only need to build the image dataset and precompute embeddings once locally unless you make changes to the base image dataset
python -m model.embed_dataset- Re-run this step any time you add or remove images from the database
python main.py- Webcam index defaults to
0to use your computers default camera - Press
ESCat any point to cancel
The matcher relies on two artifacts in model/data/: embeddings.npy (NumPy array of face embeddings) and names.json (parallel list of names — the i-th name corresponds to row i of the array). Both are regenerated from profiles.json + raw_images/ by model/embed_dataset.py, so the workflow for adding/removing people is:
- Add or remove images. Drop new images into model/data/raw_images/ as
<person_id>.jpg, or delete the ones you no longer want.<person_id>should be a stable, ASCII-safe slug. - Update model/data/profiles.json. Each entry is keyed by
person_idand must include at minimum anameand theimage_pathpointing at the file you just added. Other fields used by the UI (role,position,research_areas,summary,category,profile_url) should be filled in if you want them to appear on the result screen. Categories arescientist,engineer, orentrepreneurand are what the front-end filter buttons key off of. - Re-run the embedder before the new entries are picked up:
This loads every profile, runs InsightFace's
python -m model.embed_dataset
buffalo_lmodel on each image, and rewritesembeddings.npy+names.jsontogether. Anyone whose image is missing or has no detectable face is printed in theSkipped:list at the end — fix those and re-run if you care about them.
If instead you want to re-scrape the source sites from scratch (Perimeter Institute, iQuIST, Quantum Insider CTOs), run python -m model.dataset_builder first — that rewrites profiles.json and re-downloads images — then run python -m model.embed_dataset to rebuild the embeddings. embeddings.npy and names.json should never be edited by hand; always regenerate them via embed_dataset.py so they stay in sync.
The hosted version keeps the same high-level flow (idle -> category_select -> camera -> inference -> output) but removes the Raspberry Pi hardware dependencies:
- The browser asks for webcam permission and captures 10 JPEG frames over 5 seconds.
- Category selection is handled by clickable cards instead of ESP32 Bluetooth buttons.
- FastAPI exposes a stateless
POST /api/matchendpoint that accepts JSON withcategoryand base64 JPEGframes. - The backend reuses the existing InsightFace embeddings in
model/data/and serves result images from/images.
Run the backend locally:
uvicorn display.server:app --host 0.0.0.0 --port 8000Run the frontend locally:
cd display/frontend
npm run startFor deployment, host display/frontend on Vercel and host the FastAPI backend on Render or Railway. The backend start command is:
uvicorn display.server:app --host 0.0.0.0 --port $PORTSet QM_CORS_ORIGINS on the backend to your Vercel origin, for example:
QM_CORS_ORIGINS=https://your-project.vercel.appConfigure the deployed frontend API target by editing display/frontend/public/config.js before deploying:
window.__QM_API_BASE_URL__ = "https://your-backend.onrender.com";When config.js is empty, local Angular dev automatically calls http://localhost:8000, and production falls back to the same origin.
Once you have connected your Raspberry Pi to your monitor and hooked up a keyboard through the USB-port, try running this command to see if an instance of chromium will pop up. Make sure that you have configured wifi properly and you have installed all the necessary dependencies:
sudo apt update
sudo apt install xorg chromiumCommand to run your server
uvicorn display.server:app --host 0.0.0.0 --port 8000Build the frontend before kiosk/server runs
cd display/frontend
npm run buildThe recommended way to start the full kiosk (xrandr setup, frontend build, uvicorn server, and Chromium in kiosk mode) is via start-kiosk.sh:
xinit ./start-kiosk.sh -- :0xinit boots a minimal X server on display :0 and runs start-kiosk.sh as the X client. The script handles:
- Activating the project
venvif present - Rotating
HDMI-2and disablingHDMI-1viaxrandr - Disabling screen blanking / DPMS and hiding the cursor with
unclutter - Building the React frontend (
npm run buildin display/frontend/) - Starting the FastAPI backend (
uvicorn server:appon port8000) - Waiting for the server to come up, then launching Chromium in
--kioskmode pointed at the splash page
Logs are appended to /tmp/kiosk.log.
If you only want a bare-bones launch without the helper script:
xinit /bin/bash -c "chromium-browser --kiosk http://localhost:8000" -- :0Validation mode lets you demo the pipeline end-to-end with a known expected match. The webcam preview and thumbs-up gesture detection still run normally, but the frame fed into the face model is replaced with a noisy version of a reference image from model/data/raw_images/. See display/validation_loop.py for the noise pipeline.
Launch via start-kiosk.sh with the --validate flag and the figure's name (matched against <name>.jpg in the raw images directory, with spaces and underscores interchangeable):
xinit ./start-kiosk.sh --validate "Avery Broderick" -- :0Noise level defaults to harsh; override with the VALIDATE_NOISE env var (mild or harsh):
VALIDATE_NOISE=mild xinit ./start-kiosk.sh --validate "Avery Broderick" -- :0You can also run validation mode without the kiosk wrapper by exporting VALIDATE_NAME before starting the server:
export VALIDATE_NAME="Avery Broderick"
export VALIDATE_NOISE=harsh # optional: mild | harsh
uvicorn display.server:app --host 0.0.0.0 --port 8000Unset VALIDATE_NAME (or omit --validate) to return to normal operation.
- Raspberry Pi 4 Model B (4GB RAM) — runs the backend pipeline
- Logitech C270 HD Webcam — captures user face images via USB
- ESP32 WROOM-32 — handles button input, foot pedal, and LED control; communicates with the Pi over Bluetooth
- 21.5" IPS Monitor — displays the matched result behind the mirror
- WS2812B LED Strip — visual feedback during activation and result display
- 18x24" Tempered Two-Way Mirror Glass (70% reflective)
- Thin Film Foot Pressure Sensor — triggers system activation
- 1TB USB External Storage — stores database images, embeddings, and model files