Build a Real-Time Face Recognition System

Detect and identify faces from your webcam using InsightFace and DuckDB.

Introduction

30 Second Summary

Your phone unlocks when it sees your face. Your photos app groups pictures by who's in them. Face recognition is everywhere, and today you'll build one yourself.

In this project, you will build a real-time face recognition system that uses your Mac's webcam to detect and identify people. Your system will automatically remember new faces by storing them in a database and recognize them whenever they reappear on camera.

What You'll Build

A Python application that captures live webcam video, detects faces using the InsightFace deep learning library, matches them against stored face embeddings using cosine similarity, and saves cropped face screenshots in a DuckDB database.

By the end of this project, you'll have:

  • A real-time face detection system that draws live bounding boxes around faces on your webcam feed.
  • A DuckDB database storing cropped face screenshots and 512-dimensional embedding vectors for every detected person.
  • An automatic recognition pipeline that identifies returning faces by comparing their embeddings against stored records.
  • A face gallery viewer that queries your database and displays all stored faces in a visual grid (Secret Mission).

Are there any prerequisites?

No prior experience with computer vision or machine learning is required. This guide includes full setup instructions for every tool you need, including Python 3, VS Code, and all Python packages.

Before We Start

Before We Start...

Set Up Your Development Environment

Before you can detect a single face, your Mac needs the right tools installed. This step gets your environment ready so you can focus on building the actual face recognition system.

You will install Python 3, a code editor, and five Python packages that power every part of this project.

In this step, get ready to:
  • Install Python 3 and VS Code.
  • Create your project folder and install the required Python packages.
  • Verify all packages work and download the face detection model.
Install Python 3 and VS Code

Your face recognition system is written in Python and edited in VS Code. Let's make sure both are installed.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if Python 3 is already installed by running this command:
python3 --version

I see a version number

Python 3 is ready. Move on to checking VS Code below.

Command not found

You need to install Python 3. The easiest way on macOS is through Homebrew, a package manager for macOS.

  • Install Homebrew by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

What is Homebrew?

Homebrew is a package manager that lets you install developer tools from the command line. Think of it as an app store for your terminal.

  • The installer will ask for your password and confirmation. Follow the prompts to complete the installation.
  • Once Homebrew finishes, install Python 3 by running:
brew install python
  • Verify Python installed correctly by running:
python3 --version

You should see a version number like 3.13.x printed in your terminal.

Seeing 'brew: command not found'?

If Homebrew installed but the brew command is not recognized, you may need to add it to your PATH. Look for instructions at the end of the Homebrew installer output. It usually tells you to run two commands starting with echo and eval.

Help me fix Homebrew not being found after installation

Now let's check for VS Code, the code editor you will use to write your Python scripts.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and check if it appears in the results.

I found VS Code

VS Code is installed and ready. Open it now and keep it running.

I don't have VS Code

  • Open your browser and go to https://code.visualstudio.com.
  • Click the download button for macOS.
  • Open the downloaded .zip file and drag Visual Studio Code into your Applications folder.
  • Open VS Code from your Applications folder.
Create your project and install packages

Your project needs its own folder and a dedicated Python environment. This keeps your face recognition packages separate from anything else on your Mac.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir face-recognition
cd face-recognition
  • Confirm the folder was created by running:
ls

The output should be empty because the folder is brand new. That is expected.

Next, you will create a virtual environment. This is an isolated copy of Python that only your project uses.

  • Create a virtual environment by running this command:
python3 -m venv venv
  • Activate the virtual environment by running:
source venv/bin/activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment is a self-contained Python installation inside your project folder. Any packages you install while it is active only exist inside this environment.

This prevents conflicts between projects that might need different versions of the same package.

Don't see (venv) in your prompt?

Make sure you ran source venv/bin/activate (not just venv/bin/activate without source). Also confirm you are still inside the face-recognition folder.

Help me activate my Python virtual environment

Now you will install the five packages your project needs. Each one handles a different part of the face recognition pipeline.

  • Install all required packages by running this command:
pip install insightface==0.7.3 onnxruntime==1.25.0 opencv-python==4.13.0.92 duckdb==1.5.1 numpy

What are these packages?

  • insightface is the face detection and recognition library. It provides the AI model that finds faces and creates embeddings.
  • onnxruntime is the engine that runs the AI model on your CPU.
  • opencv-python handles webcam capture, image processing, and drawing bounding boxes.
  • numpy provides the math operations for comparing face embeddings.
  • duckdb is the database that stores face screenshots and embedding vectors.

The installation may take a few minutes. You should see a series of Successfully installed messages when it finishes.

Seeing installation errors?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If it disappeared, run source venv/bin/activate again before retrying the install command.

If you see a permissions error, do not use sudo. Instead, check that you are inside your face-recognition folder and that the virtual environment is active.

Help me troubleshoot pip install errors in my virtual environment

Verify everything works

You have five packages installed, but you have not confirmed they actually work together yet. This verification script imports every package and loads the InsightFace face detection model.

The model is called buffalo_l and it auto-downloads the first time you run the script. The download is about 326 MB, so it may take a minute.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the face-recognition folder on your Desktop, then click Open.
  • Create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save it as verify_setup.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Copy the following code into verify_setup.py:
# Quick verification script to test all packages are installed correctly

print("Checking packages...")

import cv2
print(f"  OpenCV version: {cv2.__version__}")

import numpy as np
print(f"  NumPy version: {np.__version__}")

import insightface
print(f"  InsightFace version: {insightface.__version__}")

import onnxruntime
print(f"  ONNX Runtime version: {onnxruntime.__version__}")

import duckdb
print(f"  DuckDB version: {duckdb.__version__}")

print("\nAll packages installed successfully!")

# Test InsightFace model loading (this downloads buffalo_l on first run)
print("\nLoading InsightFace model (buffalo_l)...")
print("This may take a minute on first run as it downloads ~326 MB...")
from insightface.app import FaceAnalysis
app = FaceAnalysis(providers=["CPUExecutionProvider"])
app.prepare(ctx_id=-1, det_size=(640, 640))
print("Model loaded successfully!")

# Test DuckDB
con = duckdb.connect(":memory:")
con.execute("SELECT 'DuckDB is working!' AS status").show()
con.close()

print("\nSetup complete! You are ready to build.")

What does this script do?

  • It imports each of the five packages and prints their version numbers. If any import fails, you will see an error pointing to the missing package.
  • It loads the buffalo_l model from InsightFace. This model includes both face detection and face recognition. On first run, it automatically downloads about 326 MB of model files.
  • It runs a quick DuckDB query in memory to confirm the database engine works.
  • Save verify_setup.py.
  • Go back to your Terminal. Make sure you are in the face-recognition folder with (venv) active.
  • Run the verification script:
python verify_setup.py

What should I see?

The script prints each package name and version number. After that, it downloads the buffalo_l model (this only happens once). Finally, you should see:

Model loaded successfully!
┌──────────────────────┐
│       status         │
│       varchar        │
├──────────────────────┤
│ DuckDB is working!   │
└──────────────────────┘

Setup complete! You are ready to build.

Seeing an import error?

If the script fails on a specific import (like ModuleNotFoundError: No module named 'insightface'), check that your virtual environment is active. You should see (venv) in your prompt.

If the model download stalls, check your internet connection and try again. The download is a one-time event of about 326 MB.

Help me fix errors when running verify_setup.py

Awesome, I've got everything!

Double check you have saved verify_setup.py and that the script printed all version numbers plus the success messages.

I'd like to double check the full code

# Quick verification script to test all packages are installed correctly

print("Checking packages...")

import cv2
print(f"  OpenCV version: {cv2.__version__}")

import numpy as np
print(f"  NumPy version: {np.__version__}")

import insightface
print(f"  InsightFace version: {insightface.__version__}")

import onnxruntime
print(f"  ONNX Runtime version: {onnxruntime.__version__}")

import duckdb
print(f"  DuckDB version: {duckdb.__version__}")

print("\nAll packages installed successfully!")

# Test InsightFace model loading (this downloads buffalo_l on first run)
print("\nLoading InsightFace model (buffalo_l)...")
print("This may take a minute on first run as it downloads ~326 MB...")
from insightface.app import FaceAnalysis
app = FaceAnalysis(providers=["CPUExecutionProvider"])
app.prepare(ctx_id=-1, det_size=(640, 640))
print("Model loaded successfully!")

# Test DuckDB
con = duckdb.connect(":memory:")
con.execute("SELECT 'DuckDB is working!' AS status").show()
con.close()

print("\nSetup complete! You are ready to build.")

Your development environment is fully set up. Every package is installed, and the face detection model is downloaded and ready. Next, you will write the code that opens your webcam and starts detecting faces in real time.

Detect Faces from Your Webcam

Your development environment is set up and every package is installed. Now it's time to bring your webcam to life.

In this step, you'll write a Python script that opens your webcam, runs each frame through InsightFace's face detection model, and draws green bounding boxes around every face it finds.

In this step, get ready to:
  • Open your Mac's webcam feed using OpenCV.
  • Detect faces in real time with InsightFace's buffalo_l model.
  • Draw green bounding boxes around every detected face.
Open your webcam with OpenCV

OpenCV can capture video from your webcam and display it frame by frame. Each frame is a single image. By reading frames in a loop, you get a live video feed.

  • In VS Code's Explorer sidebar, right-click on an empty area.
  • Select New File.
  • Type face_recognition.py and press Enter.
  • Paste the following code into face_recognition.py:
import cv2

def main():
    # Open webcam
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Error: Could not open webcam.")
        return

    print("Face recognition started. Press 'q' to quit.")

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Error: Could not read frame.")
            break

        # Show the frame
        cv2.imshow("Face Recognition", frame)

        # Quit on 'q' key
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    # Cleanup
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

What does this code do?

  • cv2.VideoCapture(0) connects to your Mac's built-in webcam. The 0 means the first available camera.
  • The while True loop reads one frame at a time using cap.read(). Each frame is a regular image stored as a NumPy array.
  • cv2.imshow() opens a window and displays the current frame. The window title is Face Recognition.
  • cv2.waitKey(1) & 0xFF == ord("q") checks if you pressed the q key. If so, the loop breaks and the script cleans up.
  • Save face_recognition.py.
  • Run the script by entering this command in your terminal:
python face_recognition.py

Seeing a camera permission dialog?

macOS asks for camera access the first time a program tries to use it. Click Allow to grant access.

If you accidentally clicked Don't Allow, open System Settings, then Privacy & Security, then Camera, and enable access for your terminal app.

You should see a new window open with your live webcam feed. Press q to close the window and stop the script.

Webcam not opening?

  • Make sure no other app (like FaceTime or Zoom) is using your camera. Only one app can access the webcam at a time.
  • If you see Error: Could not open webcam., your terminal might not have camera permissions. Check System Settings > Privacy & Security > Camera.
  • My webcam isn't opening in OpenCV. How do I fix this?
Add face detection with InsightFace

Your webcam feed is running. Now you'll add InsightFace to analyze each frame and detect any faces in it. InsightFace's FaceAnalysis class loads the buffalo_l model, which handles both detection and recognition.

  • In face_recognition.py, add these two import lines at the top of the file, right below import cv2:
import insightface
from insightface.app import FaceAnalysis
  • Inside the main() function, find the comment # Open webcam. Paste the following code above it:
    # Initialize InsightFace
    app = FaceAnalysis(providers=["CPUExecutionProvider"])
    app.prepare(ctx_id=-1, det_size=(640, 640))
  • Inside the while True: loop, find the comment # Show the frame. Paste the following code above it:
        # Detect faces in the frame
        faces = app.get(frame)

What do these additions do?

  • FaceAnalysis(providers=["CPUExecutionProvider"]) creates an InsightFace analyzer that runs on your CPU.
  • app.prepare(ctx_id=-1, det_size=(640, 640)) loads the buffalo_l model into memory. The det_size sets the detection resolution to 640x640 pixels.
  • app.get(frame) analyzes a single webcam frame and returns a list of detected face objects. Each face object contains a bounding box, an embedding vector, and other attributes.
  • Save face_recognition.py and run it again:
python face_recognition.py

The webcam feed looks the same visually, but you'll notice it runs noticeably slower. InsightFace is now analyzing every single frame for faces behind the scenes. Press q to close.

Seeing an error when the model loads?

  • If you see a file-not-found error, the buffalo_l model may not have downloaded properly in Step 1. Run python verify_setup.py again to trigger the download.
  • If you see onnxruntime errors, make sure you installed onnxruntime (not onnxruntime-gpu). Reinstall it by running pip install onnxruntime==1.25.0.
  • InsightFace is giving me an error when loading the model. How do I fix this?
Draw bounding boxes on detected faces

InsightFace is detecting faces, but you can't see the results yet. Each detected face comes with a bounding box. This is a set of four coordinates that define a rectangle around the face.

  • In face_recognition.py, find the line faces = app.get(frame) inside the while True: loop. Paste the following code below it, before the # Show the frame comment:
        for face in faces:
            bbox = face.bbox
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            # Draw bounding box
            cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)

What does this code do?

  • face.bbox returns four numbers. These are the x and y coordinates of the top-left corner and the bottom-right corner of the face.
  • [int(coord) for coord in bbox] converts the coordinates from decimals to whole numbers. OpenCV requires integer pixel positions for drawing.
  • cv2.rectangle() draws a rectangle on the frame. (0, 255, 0) is the color green in BGR format. 2 is the line thickness in pixels.
  • Save face_recognition.py and run it again:
python face_recognition.py

You should see green rectangles appearing around every face in the webcam feed. Try moving your face around to watch the boxes follow you in real time.

Don't see any bounding boxes?

  • Make sure your face is clearly visible and well-lit. InsightFace needs decent lighting to detect faces.
  • Check that the bounding box code is inside the for face in faces: loop, and that the loop comes before the cv2.imshow() call. Drawing after imshow means the boxes won't appear.
  • Verify there are no indentation errors. The for face in faces: line should be at the same indentation level as the # Detect faces comment (two levels deep, inside main() and while True).
  • My face detection bounding boxes aren't showing up. How do I debug this?

Awesome, I've got everything!

Make sure you've saved face_recognition.py before moving on to the next step.

I'd like to double check the full code

Here is the complete face_recognition.py file as it should look at the end of this step.

import cv2
import insightface
from insightface.app import FaceAnalysis

def main():
    # Initialize InsightFace
    app = FaceAnalysis(providers=["CPUExecutionProvider"])
    app.prepare(ctx_id=-1, det_size=(640, 640))

    # Open webcam
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Error: Could not open webcam.")
        return

    print("Face recognition started. Press 'q' to quit.")

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Error: Could not read frame.")
            break

        # Detect faces in the frame
        faces = app.get(frame)

        for face in faces:
            bbox = face.bbox
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            # Draw bounding box
            cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)

        # Show the frame
        cv2.imshow("Face Recognition", frame)

        # Quit on 'q' key
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    # Cleanup
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

Your webcam can now detect faces and draw boxes around them in real time. Next up, you'll store every detected face in a DuckDB database so you can track and recognize people across sessions.

Store Face Data in DuckDB

In the last step, you got your webcam detecting faces in real time with green bounding boxes drawn around them. That's exciting, but the detections vanish the moment each frame is gone.

For a real recognition system, you need to save those faces somewhere persistent. In this step, you'll set up a DuckDB database to store cropped face screenshots and embedding vectors for every detected person.

In this step, get ready to:
  • Create a DuckDB database with a faces table for storing face data.
  • Crop detected faces from each frame and save them to the database.
  • Query the database to verify that face screenshots and embeddings are being stored.
Create the DuckDB database and faces table

DuckDB is an embedded database that runs directly inside your Python script. There's no server to install or configure.

Why DuckDB instead of SQLite?

DuckDB supports native FLOAT[512] array columns, which is perfect for storing 512-dimensional face embedding vectors. SQLite would require workarounds like serializing arrays to JSON text. DuckDB also handles BLOB columns natively, so you can store raw image bytes directly.

Your faces table needs to store five things for each detected person: an ID, a name, timestamps for when they were first and last seen, a cropped face image, and a 512-number embedding vector.

  • In face_recognition.py, add two new imports at the top of your file, below the existing insightface imports:
import duckdb
from datetime import datetime
  • Below all your import lines, add the setup_database function by copying this code:
# ----- Database Setup -----

def setup_database(db_path):
    """Create and return a DuckDB connection with the faces table."""
    con = duckdb.connect(db_path)
    con.execute("""
        CREATE TABLE IF NOT EXISTS faces (
            id INTEGER,
            name VARCHAR,
            first_seen TIMESTAMP,
            last_seen TIMESTAMP,
            face_image BLOB,
            embedding FLOAT[512]
        )
    """)
    return con

What does this code do?

  • duckdb.connect(db_path) creates (or opens) a database file on disk. All your face data persists between script runs.
  • CREATE TABLE IF NOT EXISTS ensures the table is created on first run but doesn't overwrite existing data on future runs.
  • face_image BLOB stores raw PNG image bytes. embedding FLOAT[512] stores the 512-dimensional face fingerprint as a fixed-size array of numbers.
  • Save the file.

You won't see a change yet. The next chunk uses this function to connect to the database inside your main loop.

Crop and save detected faces

When InsightFace detects a face, it gives you bounding box coordinates. You need a function that uses those coordinates to cut out just the face region from the full frame.

  • Below the setup_database function, add the crop_face function:
# ----- Crop Face from Frame -----

def crop_face(frame, bbox):
    """Crop the face region from the frame using bounding box coordinates."""
    x1, y1, x2, y2 = [int(coord) for coord in bbox]
    h, w = frame.shape[:2]
    # Clamp coordinates to stay within the frame boundaries
    x1 = max(0, x1)
    y1 = max(0, y1)
    x2 = min(w, x2)
    y2 = min(h, y2)
    return frame[y1:y2, x1:x2]

What does this code do?

  • The bounding box gives you four coordinates: the top-left corner (x1, y1) and the bottom-right corner (x2, y2) of the face rectangle.
  • The max(0, ...) and min(w, ...) lines clamp the coordinates so they never go outside the frame. A face near the edge of the camera could produce coordinates that extend beyond the image.
  • frame[y1:y2, x1:x2] uses NumPy array slicing to extract just the face pixels from the full frame.
  • Save the file.

You won't see a change yet. The next chunk adds the database insert functions that use these cropped images.

Now you need two helper functions: one to figure out what ID to assign to a new person, and one to insert a face record into the database.

  • Between the setup_database function and the crop_face function, add these two database helper functions:
def get_next_id(con):
    """Get the next available person ID."""
    result = con.execute("SELECT COALESCE(MAX(id), 0) FROM faces").fetchone()
    return result[0] + 1


def insert_face(con, person_id, name, face_image_bytes, embedding):
    """Insert a new face record into the database."""
    now = datetime.now()
    embedding_list = embedding.tolist()
    con.execute(
        "INSERT INTO faces VALUES (?, ?, ?, ?, ?, ?)",
        [person_id, name, now, now, face_image_bytes, embedding_list]
    )

What does this code do?

  • get_next_id queries the highest existing ID in the database and returns the next number. If the table is empty, COALESCE(MAX(id), 0) falls back to 0, so the first person gets ID 1.
  • insert_face stores a complete face record: the person's ID, name, current timestamp for both first_seen and last_seen, the cropped face image as raw bytes, and the embedding as a Python list.
  • The ? placeholders in the SQL query prevent injection attacks. DuckDB automatically maps Python bytes to BLOB and Python lists to arrays.
  • Save the file.

You won't see a change yet. The next chunk wires everything together inside your main loop.

Now it's time to connect the database and face cropping logic into your main application loop. For each detected face, you'll crop it, encode it as a PNG, and insert it into DuckDB.

  • In your main() function, find these lines that initialize InsightFace:
    app = FaceAnalysis(providers=["CPUExecutionProvider"])
    app.prepare(ctx_id=-1, det_size=(640, 640))
  • Directly below those two lines, add the DuckDB initialization:
    # Initialize DuckDB
    db_path = "faces.duckdb"
    con = setup_database(db_path)
  • Next, find the for face in faces: loop inside the while True: block. Replace the entire for loop with this updated version that crops and stores each face:
        for face in faces:
            bbox = face.bbox
            embedding = face.normed_embedding

            # Store every detected face (recognition comes in the next step)
            person_id = get_next_id(con)
            name = f"Person {person_id}"

            # Crop and encode face image
            face_crop = crop_face(frame, bbox)
            if face_crop.size > 0:
                success, encoded = cv2.imencode(".png", face_crop)
                if success:
                    face_bytes = encoded.tobytes()
                    insert_face(con, person_id, name, face_bytes, embedding)

            # Draw bounding box
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)

What does this code do?

  • face.normed_embedding grabs the L2-normalized 512-dimensional embedding vector for each face. You must use normed_embedding instead of embedding for accurate similarity calculations later.
  • cv2.imencode(".png", face_crop) converts the cropped face pixels into PNG-formatted bytes that can be stored in the database.
  • Right now, every detected face in every frame gets stored as a new person. You'll fix this in the next step by adding recognition logic that checks whether a face already exists before enrolling it.
  • Finally, find the cleanup section at the bottom of main() (after the while True: loop). Add con.close() and a database path message so the cleanup looks like this:
    # Cleanup
    cap.release()
    cv2.destroyAllWindows()
    con.close()
    print("Face detection stopped.")
    print(f"Database saved to: {db_path}")
  • Save face_recognition.py.

Not sure where each piece goes?

The database initialization (db_path and con) goes inside main() right after the InsightFace setup, before the webcam opens. The updated for loop replaces your existing one entirely. Use the double-check tab below to compare your full file.

Help me figure out where to put the database code in my main function.

Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

I'd like to double check the full code

Here is the complete face_recognition.py as it should look at the end of this substep.

import cv2
import numpy as np
import insightface
from insightface.app import FaceAnalysis
import duckdb
from datetime import datetime


# ----- Database Setup -----

def setup_database(db_path):
    """Create and return a DuckDB connection with the faces table."""
    con = duckdb.connect(db_path)
    con.execute("""
        CREATE TABLE IF NOT EXISTS faces (
            id INTEGER,
            name VARCHAR,
            first_seen TIMESTAMP,
            last_seen TIMESTAMP,
            face_image BLOB,
            embedding FLOAT[512]
        )
    """)
    return con


def get_next_id(con):
    """Get the next available person ID."""
    result = con.execute("SELECT COALESCE(MAX(id), 0) FROM faces").fetchone()
    return result[0] + 1


def insert_face(con, person_id, name, face_image_bytes, embedding):
    """Insert a new face record into the database."""
    now = datetime.now()
    embedding_list = embedding.tolist()
    con.execute(
        "INSERT INTO faces VALUES (?, ?, ?, ?, ?, ?)",
        [person_id, name, now, now, face_image_bytes, embedding_list]
    )


# ----- Crop Face from Frame -----

def crop_face(frame, bbox):
    """Crop the face region from the frame using bounding box coordinates."""
    x1, y1, x2, y2 = [int(coord) for coord in bbox]
    h, w = frame.shape[:2]
    x1 = max(0, x1)
    y1 = max(0, y1)
    x2 = min(w, x2)
    y2 = min(h, y2)
    return frame[y1:y2, x1:x2]


# ----- Main Application -----

def main():
    # Initialize InsightFace
    app = FaceAnalysis(providers=["CPUExecutionProvider"])
    app.prepare(ctx_id=-1, det_size=(640, 640))

    # Initialize DuckDB
    db_path = "faces.duckdb"
    con = setup_database(db_path)

    # Open webcam
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Error: Could not open webcam.")
        return

    print("Face detection started. Press 'q' to quit.")

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Error: Could not read frame.")
            break

        # Detect faces in the frame
        faces = app.get(frame)

        for face in faces:
            bbox = face.bbox
            embedding = face.normed_embedding

            # Store every detected face (recognition comes in the next step)
            person_id = get_next_id(con)
            name = f"Person {person_id}"

            # Crop and encode face image
            face_crop = crop_face(frame, bbox)
            if face_crop.size > 0:
                success, encoded = cv2.imencode(".png", face_crop)
                if success:
                    face_bytes = encoded.tobytes()
                    insert_face(con, person_id, name, face_bytes, embedding)

            # Draw bounding box
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)

        # Show the frame
        cv2.imshow("Face Recognition", frame)

        # Quit on 'q' key
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    # Cleanup
    cap.release()
    cv2.destroyAllWindows()
    con.close()
    print("Face detection stopped.")
    print(f"Database saved to: {db_path}")


if __name__ == "__main__":
    main()
Query and verify your stored faces

Before moving on, you need to confirm that face data is actually being written to the database. You'll run the script for a few seconds, then query the database to check.

  • First, delete the old database file (if it exists from previous runs) so you start fresh. Run this in your terminal:
rm -f faces.duckdb
  • Run your face recognition script:
python3 face_recognition.py
  • Position your face in front of the webcam for about 3-5 seconds.
  • Press q to quit the video window.

You should see the message Database saved to: faces.duckdb in your terminal.

Script crashes or no faces detected?

  • If you see a NameError for setup_database or crop_face, make sure your helper functions are defined above the main() function, not inside it.
  • If no bounding boxes appear, check that your face is well-lit and clearly visible to the webcam.
  • If you see an ImportError for duckdb, confirm your virtual environment is activated (you should see (venv) in your terminal prompt).

My face recognition script is crashing when I try to store faces in DuckDB.

Now query the database to see what was stored.

  • Run this Python command in your terminal to count and inspect the stored faces:
python3 -c "
import duckdb
con = duckdb.connect('faces.duckdb')
print('Total faces stored:', con.execute('SELECT COUNT(*) FROM faces').fetchone()[0])
con.execute('SELECT id, name, first_seen, length(face_image) as img_bytes FROM faces LIMIT 5').show()
con.close()
"

You should see a count of stored faces (likely dozens, since every frame stores a new entry right now) and a small table showing face IDs, names, timestamps, and image sizes in bytes.

Why are there so many rows?

Right now, the script stores every detected face from every frame as a brand new person. If you ran it for 3 seconds at 10 frames per second, that's 30 rows for the same face. This is expected.

In the next step, you'll add cosine similarity matching so the system recognizes returning faces and only stores genuinely new people.

Seeing 0 faces stored?

  • Make sure you ran the script with your face visible to the webcam for at least a few seconds before pressing q.
  • Check that the faces.duckdb file exists in your project folder by running ls. If it's missing, the script may have crashed before saving.

My DuckDB query shows 0 faces even though I ran the script with my face visible.

Your database is working and filling up with face data. Next up, you'll add the brains of the system. You'll use cosine similarity to compare face embeddings so the system can recognize people it has already seen.

Recognize Faces with Cosine Similarity

Your system can detect faces and store them in DuckDB. But right now, it treats every face as a brand new person. It has no way to check whether it has already seen someone before.

In this step, you will give your system the ability to recognize people. You will use cosine similarity to compare face embeddings and decide if a detected face matches someone already in the database.

In this step, get ready to:
  • Implement a cosine similarity function to compare face embeddings.
  • Build recognition logic that matches detected faces against stored embeddings in your database.
  • Display identity labels and color-coded bounding boxes on the live webcam feed.
Understand embeddings and cosine similarity

To identify who a face belongs to, your system needs a way to compare faces mathematically. InsightFace converts each detected face into a set of 512 numbers called an embedding.

What is a face embedding?

An embedding is like a mathematical fingerprint for a face. InsightFace analyzes a face and produces 512 numbers that capture its unique features (eye spacing, nose shape, jawline, etc.).

Two photos of the same person produce similar embeddings. Two different people produce very different embeddings.

To measure how similar two embeddings are, you use cosine similarity. It calculates the angle between two vectors and returns a score from -1 (completely different) to 1 (identical).

Why cosine similarity?

The ArcFace model behind InsightFace's buffalo_l was specifically designed to use cosine similarity for comparison. The official ArcFace paper states: "During testing, the score is computed by the Cosine Distance of two feature vectors."

The embeddings are L2-normalized, which means cosine similarity gives the most accurate and intuitive results for this model.

  • In face_recognition.py, add the following function after your import statements and before the setup_database function:
# ----- Cosine Similarity Function -----

def cosine_similarity(embedding1, embedding2):
    """Calculate cosine similarity between two face embeddings."""
    dot_product = np.dot(embedding1, embedding2)
    norm1 = np.linalg.norm(embedding1)
    norm2 = np.linalg.norm(embedding2)
    if norm1 == 0 or norm2 == 0:
        return 0.0
    return dot_product / (norm1 * norm2)

What does this code do?

  • np.dot(embedding1, embedding2) calculates the dot product, which measures how much two vectors point in the same direction.
  • np.linalg.norm() calculates the magnitude (length) of each vector, used to normalize the result.
  • The zero-check prevents division by zero if an embedding is all zeros.
  • The final result is a score between -1 and 1. A score close to 1 means the two faces are very likely the same person.
  • Save the file. You won't see a visible change yet. The next substep builds the functions that call this one.
Build the recognition logic

Your cosine similarity function can compare two embeddings. Now you need helper functions that load all known faces from DuckDB, find the best match for a new face, and update records when someone is re-seen.

  • In face_recognition.py, add the following three functions after your insert_face function:
def get_all_embeddings(con):
    """Load all stored embeddings and names from the database."""
    result = con.execute("SELECT id, name, embedding FROM faces").fetchall()
    persons = []
    for row in result:
        person_id = row[0]
        name = row[1]
        embedding = np.array(row[2], dtype=np.float32)
        persons.append({"id": person_id, "name": name, "embedding": embedding})
    return persons


def update_last_seen(con, person_id):
    """Update the last_seen timestamp for a person."""
    now = datetime.now()
    con.execute(
        "UPDATE faces SET last_seen = ? WHERE id = ?",
        [now, person_id]
    )


# ----- Face Recognition Logic -----

def find_match(embedding, known_persons, threshold=0.5):
    """Compare embedding against known persons using cosine similarity."""
    best_match = None
    best_score = -1

    for person in known_persons:
        score = cosine_similarity(embedding, person["embedding"])
        if score > best_score:
            best_score = score
            best_match = person

    if best_score >= threshold:
        return best_match, best_score
    return None, best_score

What do these functions do?

  • get_all_embeddings() queries every face record from DuckDB and converts each stored embedding list back into a NumPy array. It returns a list of dictionaries with each person's ID, name, and embedding.
  • update_last_seen() updates the last_seen timestamp for a recognized person, so you can track when they were last on camera.
  • find_match() loops through all known persons and finds the one with the highest cosine similarity score. If that score is 0.5 or higher, the face is a match. Otherwise, it returns None to signal a new person.

Why 0.5 as the threshold?

A threshold of 0.5 is a good starting point. It catches most true matches while avoiding false positives. Practitioners report that thresholds above 0.52 make false positives virtually non-existent across large datasets.

If your system is confusing two different people, raise the threshold. If it keeps re-enrolling the same person, lower it.

  • Save the file. You won't see a change yet. The next substep wires these functions into your main loop.
Display identity labels on the live feed

Your helper functions are ready. Now you need to update the main loop to use them. For every detected face, the loop will compare its embedding against all stored faces, decide whether it is a match or a new person, and draw a labeled bounding box.

Why use normed_embedding?

Always use face.normed_embedding instead of face.embedding. The raw embedding gives unreliable similarity scores (values like 700 instead of a clean -1 to 1 range). The normed version is L2-normalized, which is required for accurate cosine similarity.

  • In your main() function, find the line faces = app.get(frame) inside the while True loop.
  • Delete your existing for face in faces: loop and everything inside it.
  • Replace it with the following code, directly below the faces = app.get(frame) line:
        # Load known persons from database
        known_persons = get_all_embeddings(con)

        for face in faces:
            bbox = face.bbox
            embedding = face.normed_embedding

            # Try to find a match
            match, score = find_match(embedding, known_persons)

            if match is not None:
                # Recognized face
                name = match["name"]
                color = (0, 255, 0)  # Green
                update_last_seen(con, match["id"])
            else:
                # New face - enroll automatically
                person_id = get_next_id(con)
                name = f"Person {person_id}"
                color = (0, 255, 255)  # Yellow

                # Crop and encode face image
                face_crop = crop_face(frame, bbox)
                if face_crop.size > 0:
                    success, encoded = cv2.imencode(".png", face_crop)
                    if success:
                        face_bytes = encoded.tobytes()
                        insert_face(con, person_id, name, face_bytes, embedding)

                        # Add to known persons so we don't re-enroll this frame
                        known_persons.append({
                            "id": person_id,
                            "name": name,
                            "embedding": embedding
                        })

            # Draw bounding box
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

            # Draw name label
            label = f"{name} ({score:.2f})" if match else name
            cv2.putText(
                frame, label, (x1, y1 - 10),
                cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2
            )

What does this code do?

  • get_all_embeddings(con) loads every known person from DuckDB at the start of each frame, so matching stays up to date.
  • find_match() compares the current face's embedding against all stored embeddings. If the best score is 0.5 or higher, it returns the matching person.
  • If a match is found, the bounding box is green and the person's name and score appear as a label. The last_seen timestamp is updated.
  • If no match is found, the system automatically enrolls the face as a new person (yellow bounding box). It crops the face, encodes it as a PNG, and stores it in DuckDB.
  • The new person is appended to known_persons immediately so the same face is not re-enrolled if it appears again in the same frame.
  • Save face_recognition.py.
  • Delete your old database file before testing. This prevents leftover face data from conflicting with the new recognition logic. Run this in your terminal:
rm -f faces.duckdb
  • Run your face recognition script by running:
python face_recognition.py
  • Look at the webcam window. You should see a yellow bounding box appear around your face with a label like Person 1 the first time it detects you.
  • Stay on camera for a moment. The bounding box should turn green on subsequent frames, showing your name and a similarity score (e.g. Person 1 (0.78)).
  • Press q to quit when you are done testing.

Not seeing colored boxes or labels?

  • If the boxes are always yellow and never turn green, check that your cosine_similarity function is defined before find_match in the file. find_match calls cosine_similarity, so it must come after it.
  • If you see no boxes at all, make sure you kept the cv2.imshow("Face Recognition", frame) line below your for loop. It should not have been deleted when you replaced the loop body.
  • If you get a NameError for get_all_embeddings or find_match, make sure you added the new functions from the previous substep and saved the file.
  • My recognition system isn't matching faces correctly. What should I check?

Awesome, I've got everything!

Double check you have saved face_recognition.py and confirmed that bounding boxes and labels appear on your webcam feed.

I'd like to double check the full code

Here is the complete face_recognition.py as it should look at the end of this step.

import cv2
import numpy as np
import insightface
from insightface.app import FaceAnalysis
import duckdb
from datetime import datetime


# ----- Cosine Similarity Function -----

def cosine_similarity(embedding1, embedding2):
    """Calculate cosine similarity between two face embeddings."""
    dot_product = np.dot(embedding1, embedding2)
    norm1 = np.linalg.norm(embedding1)
    norm2 = np.linalg.norm(embedding2)
    if norm1 == 0 or norm2 == 0:
        return 0.0
    return dot_product / (norm1 * norm2)


# ----- Database Setup -----

def setup_database(db_path):
    """Create and return a DuckDB connection with the faces table."""
    con = duckdb.connect(db_path)
    con.execute("""
        CREATE TABLE IF NOT EXISTS faces (
            id INTEGER,
            name VARCHAR,
            first_seen TIMESTAMP,
            last_seen TIMESTAMP,
            face_image BLOB,
            embedding FLOAT[512]
        )
    """)
    return con


def get_all_embeddings(con):
    """Load all stored embeddings and names from the database."""
    result = con.execute("SELECT id, name, embedding FROM faces").fetchall()
    persons = []
    for row in result:
        person_id = row[0]
        name = row[1]
        embedding = np.array(row[2], dtype=np.float32)
        persons.append({"id": person_id, "name": name, "embedding": embedding})
    return persons


def get_next_id(con):
    """Get the next available person ID."""
    result = con.execute("SELECT COALESCE(MAX(id), 0) FROM faces").fetchone()
    return result[0] + 1


def insert_face(con, person_id, name, face_image_bytes, embedding):
    """Insert a new face record into the database."""
    now = datetime.now()
    embedding_list = embedding.tolist()
    con.execute(
        "INSERT INTO faces VALUES (?, ?, ?, ?, ?, ?)",
        [person_id, name, now, now, face_image_bytes, embedding_list]
    )


def update_last_seen(con, person_id):
    """Update the last_seen timestamp for a person."""
    now = datetime.now()
    con.execute(
        "UPDATE faces SET last_seen = ? WHERE id = ?",
        [now, person_id]
    )


# ----- Face Recognition Logic -----

def find_match(embedding, known_persons, threshold=0.5):
    """Compare embedding against known persons using cosine similarity."""
    best_match = None
    best_score = -1

    for person in known_persons:
        score = cosine_similarity(embedding, person["embedding"])
        if score > best_score:
            best_score = score
            best_match = person

    if best_score >= threshold:
        return best_match, best_score
    return None, best_score


# ----- Crop Face from Frame -----

def crop_face(frame, bbox):
    """Crop the face region from the frame using bounding box coordinates."""
    x1, y1, x2, y2 = [int(coord) for coord in bbox]
    h, w = frame.shape[:2]
    x1 = max(0, x1)
    y1 = max(0, y1)
    x2 = min(w, x2)
    y2 = min(h, y2)
    return frame[y1:y2, x1:x2]


# ----- Main Application -----

def main():
    # Initialize InsightFace
    app = FaceAnalysis(providers=["CPUExecutionProvider"])
    app.prepare(ctx_id=-1, det_size=(640, 640))

    # Initialize DuckDB
    db_path = "faces.duckdb"
    con = setup_database(db_path)

    # Open webcam
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Error: Could not open webcam.")
        return

    print("Face recognition started. Press 'q' to quit.")

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Error: Could not read frame.")
            break

        # Detect faces in the frame
        faces = app.get(frame)

        # Load known persons from database
        known_persons = get_all_embeddings(con)

        for face in faces:
            bbox = face.bbox
            embedding = face.normed_embedding

            # Try to find a match
            match, score = find_match(embedding, known_persons)

            if match is not None:
                # Recognized face
                name = match["name"]
                color = (0, 255, 0)  # Green
                update_last_seen(con, match["id"])
            else:
                # New face - enroll automatically
                person_id = get_next_id(con)
                name = f"Person {person_id}"
                color = (0, 255, 255)  # Yellow

                # Crop and encode face image
                face_crop = crop_face(frame, bbox)
                if face_crop.size > 0:
                    success, encoded = cv2.imencode(".png", face_crop)
                    if success:
                        face_bytes = encoded.tobytes()
                        insert_face(con, person_id, name, face_bytes, embedding)

                        # Add to known persons so we don't re-enroll this frame
                        known_persons.append({
                            "id": person_id,
                            "name": name,
                            "embedding": embedding
                        })

            # Draw bounding box
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

            # Draw name label
            label = f"{name} ({score:.2f})" if match else name
            cv2.putText(
                frame, label, (x1, y1 - 10),
                cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2
            )

        # Show the frame
        cv2.imshow("Face Recognition", frame)

        # Quit on 'q' key
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    # Cleanup
    cap.release()
    cv2.destroyAllWindows()
    con.close()
    print("Face recognition stopped.")
    print(f"Database saved to: {db_path}")


if __name__ == "__main__":
    main()

Your system can now detect, recognize, and store faces automatically. Next up, you will assemble everything into a polished final script and test it with multiple people.

Put It All Together

In the last step, you built the recognition engine that compares face embeddings using cosine similarity and labels each person on the live feed.

Now it is time to combine every piece into one polished Python script. You will assemble the webcam capture, face detection, database storage, and recognition logic into a single file, then run it end to end with multiple people.

In this step, get ready to:
  • Assemble all components into a single final script with an architecture diagram.
  • Test the system with multiple people on camera and verify recognition works.
  • Inspect your DuckDB database to see every stored person and their timestamps.
Combine all components into the final script

Throughout the previous steps you built each piece of the face recognition system separately. Now you will bring them together into one clean file called face_recognition.py.

  • Create a new file called face_recognition.py inside your project folder.
  • Copy the following ASCII architecture diagram and imports into the top of the file:
# ============================================================
# Real-Time Face Recognition System with InsightFace + DuckDB
# ============================================================
#
# ARCHITECTURE:
#
#   +-------------------+
#   |    Mac Webcam     |
#   +--------+----------+
#            |
#            | video frames
#            v
#   +--------+----------+
#   |  OpenCV Capture   |
#   |  (cv2.VideoCapture)|
#   +--------+----------+
#            |
#            | each frame
#            v
#   +--------+----------+
#   |   InsightFace     |
#   |   FaceAnalysis    |
#   |   (buffalo_l)     |
#   +--------+----------+
#            |
#            | faces[] with bounding boxes + embeddings
#            v
#   +--------+----------+       +-------------------+
#   | Cosine Similarity | <---> |     DuckDB        |
#   | Matching Engine   |       | (faces.duckdb)    |
#   +--------+----------+       | - face images     |
#            |                  | - embeddings      |
#            | match results    | - names/timestamps|
#            v                  +-------------------+
#   +--------+----------+
#   |  OpenCV Drawing   |
#   |  (bounding boxes  |
#   |   + name labels)  |
#   +--------+----------+
#            |
#            v
#   +--------+----------+
#   |   Display Window  |
#   +-------------------+
#
# ============================================================

import cv2
import numpy as np
import insightface
from insightface.app import FaceAnalysis
import duckdb
from datetime import datetime

What does this code do?

  • The ASCII diagram maps the entire data flow of your application, from webcam input all the way to the display window. It is a helpful reference when you come back to this code later.
  • The six imports pull in every library you need: cv2 for webcam and drawing, numpy for math, insightface for face detection and embeddings, duckdb for storage, and datetime for timestamps.
  • Below the imports, add all of the helper functions you built in previous steps by copying the following code:
# ----- Cosine Similarity Function -----

def cosine_similarity(embedding1, embedding2):
    """Calculate cosine similarity between two face embeddings."""
    dot_product = np.dot(embedding1, embedding2)
    norm1 = np.linalg.norm(embedding1)
    norm2 = np.linalg.norm(embedding2)
    if norm1 == 0 or norm2 == 0:
        return 0.0
    return dot_product / (norm1 * norm2)


# ----- Database Setup -----

def setup_database(db_path):
    """Create and return a DuckDB connection with the faces table."""
    con = duckdb.connect(db_path)
    con.execute("""
        CREATE TABLE IF NOT EXISTS faces (
            id INTEGER,
            name VARCHAR,
            first_seen TIMESTAMP,
            last_seen TIMESTAMP,
            face_image BLOB,
            embedding FLOAT[512]
        )
    """)
    return con


def get_all_embeddings(con):
    """Load all stored embeddings and names from the database."""
    result = con.execute("SELECT id, name, embedding FROM faces").fetchall()
    persons = []
    for row in result:
        person_id = row[0]
        name = row[1]
        embedding = np.array(row[2], dtype=np.float32)
        persons.append({"id": person_id, "name": name, "embedding": embedding})
    return persons


def get_next_id(con):
    """Get the next available person ID."""
    result = con.execute("SELECT COALESCE(MAX(id), 0) FROM faces").fetchone()
    return result[0] + 1


def insert_face(con, person_id, name, face_image_bytes, embedding):
    """Insert a new face record into the database."""
    now = datetime.now()
    embedding_list = embedding.tolist()
    con.execute(
        "INSERT INTO faces VALUES (?, ?, ?, ?, ?, ?)",
        [person_id, name, now, now, face_image_bytes, embedding_list]
    )


def update_last_seen(con, person_id):
    """Update the last_seen timestamp for a person."""
    now = datetime.now()
    con.execute(
        "UPDATE faces SET last_seen = ? WHERE id = ?",
        [now, person_id]
    )


# ----- Face Recognition Logic -----

def find_match(embedding, known_persons, threshold=0.5):
    """Compare embedding against known persons using cosine similarity."""
    best_match = None
    best_score = -1

    for person in known_persons:
        score = cosine_similarity(embedding, person["embedding"])
        if score > best_score:
            best_score = score
            best_match = person

    if best_score >= threshold:
        return best_match, best_score
    return None, best_score


# ----- Crop Face from Frame -----

def crop_face(frame, bbox):
    """Crop the face region from the frame using bounding box coordinates."""
    x1, y1, x2, y2 = [int(coord) for coord in bbox]
    h, w = frame.shape[:2]
    x1 = max(0, x1)
    y1 = max(0, y1)
    x2 = min(w, x2)
    y2 = min(h, y2)
    return frame[y1:y2, x1:x2]

What does this code do?

These are the same functions you built across Steps 2 through 4, now collected in one place. Here is a quick refresher on each group:

  • cosine_similarity compares two 512-dimensional face embeddings and returns a score between -1 and 1.
  • The five database functions handle creating the faces table, loading stored embeddings, generating new IDs, inserting new face records, and updating timestamps.
  • find_match loops through all known persons and returns the best match above the 0.5 threshold.
  • crop_face extracts the face region from a video frame using bounding box coordinates, clamped to stay within frame boundaries.

The main() function is where all of these pieces come together. It initializes the model and database, opens the webcam, and runs the detection-recognition-drawing loop.

  • Add the main() function and entry point at the bottom of face_recognition.py by copying the following code:
# ----- Main Application -----

def main():
    # Initialize InsightFace
    app = FaceAnalysis(providers=["CPUExecutionProvider"])
    app.prepare(ctx_id=-1, det_size=(640, 640))

    # Initialize DuckDB
    db_path = "faces.duckdb"
    con = setup_database(db_path)

    # Open webcam
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Error: Could not open webcam.")
        return

    print("Face recognition started. Press 'q' to quit.")

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Error: Could not read frame.")
            break

        # Detect faces in the frame
        faces = app.get(frame)

        # Load known persons from database
        known_persons = get_all_embeddings(con)

        for face in faces:
            bbox = face.bbox
            embedding = face.normed_embedding

            # Try to find a match
            match, score = find_match(embedding, known_persons)

            if match is not None:
                # Recognized face
                name = match["name"]
                color = (0, 255, 0)  # Green
                update_last_seen(con, match["id"])
            else:
                # New face - enroll automatically
                person_id = get_next_id(con)
                name = f"Person {person_id}"
                color = (0, 255, 255)  # Yellow

                # Crop and encode face image
                face_crop = crop_face(frame, bbox)
                if face_crop.size > 0:
                    success, encoded = cv2.imencode(".png", face_crop)
                    if success:
                        face_bytes = encoded.tobytes()
                        insert_face(con, person_id, name, face_bytes, embedding)

                        # Add to known persons so we don't re-enroll this frame
                        known_persons.append({
                            "id": person_id,
                            "name": name,
                            "embedding": embedding
                        })

            # Draw bounding box
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

            # Draw name label
            label = f"{name} ({score:.2f})" if match else name
            cv2.putText(
                frame, label, (x1, y1 - 10),
                cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2
            )

        # Show the frame
        cv2.imshow("Face Recognition", frame)

        # Quit on 'q' key
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    # Cleanup
    cap.release()
    cv2.destroyAllWindows()
    con.close()
    print("Face recognition stopped.")
    print(f"Database saved to: {db_path}")


if __name__ == "__main__":
    main()

What does this code do?

This is the orchestration loop that drives the entire application. Here is what each section handles:

  • Lines 3-5 initialize InsightFace with the buffalo_l model on CPU and set up the DuckDB database connection.
  • The while True loop reads one frame at a time from the webcam, runs face detection, and iterates through every detected face.
  • For each face, it grabs the normed_embedding and calls find_match(). Recognized faces get a green bounding box. New faces are auto-enrolled with a yellow box.
  • The known_persons.append() call after enrolling a new face prevents the same person from being enrolled multiple times within a single frame.
  • Pressing q breaks the loop. The cleanup section releases the webcam, closes the display window, and saves the database.
  • Save face_recognition.py.

Seeing a syntax error when you save?

  • Make sure the indentation is consistent throughout the file. Python is whitespace-sensitive, so mixing tabs and spaces causes errors.
  • Check that every opening parenthesis, bracket, and triple-quote has a matching closing one. A missing closing """ on the SQL string in setup_database is a common miss.
  • Double check the full code using the tab below to compare against your file.

Help me find the syntax error in my face_recognition.py file.

Awesome, I've got everything!

Double check you have saved face_recognition.py before moving on.

I'd like to double check the full code

# ============================================================
# Real-Time Face Recognition System with InsightFace + DuckDB
# ============================================================
#
# ARCHITECTURE:
#
#   +-------------------+
#   |    Mac Webcam     |
#   +--------+----------+
#            |
#            | video frames
#            v
#   +--------+----------+
#   |  OpenCV Capture   |
#   |  (cv2.VideoCapture)|
#   +--------+----------+
#            |
#            | each frame
#            v
#   +--------+----------+
#   |   InsightFace     |
#   |   FaceAnalysis    |
#   |   (buffalo_l)     |
#   +--------+----------+
#            |
#            | faces[] with bounding boxes + embeddings
#            v
#   +--------+----------+       +-------------------+
#   | Cosine Similarity | <---> |     DuckDB        |
#   | Matching Engine   |       | (faces.duckdb)    |
#   +--------+----------+       | - face images     |
#            |                  | - embeddings      |
#            | match results    | - names/timestamps|
#            v                  +-------------------+
#   +--------+----------+
#   |  OpenCV Drawing   |
#   |  (bounding boxes  |
#   |   + name labels)  |
#   +--------+----------+
#            |
#            v
#   +--------+----------+
#   |   Display Window  |
#   +-------------------+
#
# ============================================================

import cv2
import numpy as np
import insightface
from insightface.app import FaceAnalysis
import duckdb
from datetime import datetime


# ----- Cosine Similarity Function -----

def cosine_similarity(embedding1, embedding2):
    """Calculate cosine similarity between two face embeddings."""
    dot_product = np.dot(embedding1, embedding2)
    norm1 = np.linalg.norm(embedding1)
    norm2 = np.linalg.norm(embedding2)
    if norm1 == 0 or norm2 == 0:
        return 0.0
    return dot_product / (norm1 * norm2)


# ----- Database Setup -----

def setup_database(db_path):
    """Create and return a DuckDB connection with the faces table."""
    con = duckdb.connect(db_path)
    con.execute("""
        CREATE TABLE IF NOT EXISTS faces (
            id INTEGER,
            name VARCHAR,
            first_seen TIMESTAMP,
            last_seen TIMESTAMP,
            face_image BLOB,
            embedding FLOAT[512]
        )
    """)
    return con


def get_all_embeddings(con):
    """Load all stored embeddings and names from the database."""
    result = con.execute("SELECT id, name, embedding FROM faces").fetchall()
    persons = []
    for row in result:
        person_id = row[0]
        name = row[1]
        embedding = np.array(row[2], dtype=np.float32)
        persons.append({"id": person_id, "name": name, "embedding": embedding})
    return persons


def get_next_id(con):
    """Get the next available person ID."""
    result = con.execute("SELECT COALESCE(MAX(id), 0) FROM faces").fetchone()
    return result[0] + 1


def insert_face(con, person_id, name, face_image_bytes, embedding):
    """Insert a new face record into the database."""
    now = datetime.now()
    embedding_list = embedding.tolist()
    con.execute(
        "INSERT INTO faces VALUES (?, ?, ?, ?, ?, ?)",
        [person_id, name, now, now, face_image_bytes, embedding_list]
    )


def update_last_seen(con, person_id):
    """Update the last_seen timestamp for a person."""
    now = datetime.now()
    con.execute(
        "UPDATE faces SET last_seen = ? WHERE id = ?",
        [now, person_id]
    )


# ----- Face Recognition Logic -----

def find_match(embedding, known_persons, threshold=0.5):
    """Compare embedding against known persons using cosine similarity."""
    best_match = None
    best_score = -1

    for person in known_persons:
        score = cosine_similarity(embedding, person["embedding"])
        if score > best_score:
            best_score = score
            best_match = person

    if best_score >= threshold:
        return best_match, best_score
    return None, best_score


# ----- Crop Face from Frame -----

def crop_face(frame, bbox):
    """Crop the face region from the frame using bounding box coordinates."""
    x1, y1, x2, y2 = [int(coord) for coord in bbox]
    h, w = frame.shape[:2]
    x1 = max(0, x1)
    y1 = max(0, y1)
    x2 = min(w, x2)
    y2 = min(h, y2)
    return frame[y1:y2, x1:x2]


# ----- Main Application -----

def main():
    # Initialize InsightFace
    app = FaceAnalysis(providers=["CPUExecutionProvider"])
    app.prepare(ctx_id=-1, det_size=(640, 640))

    # Initialize DuckDB
    db_path = "faces.duckdb"
    con = setup_database(db_path)

    # Open webcam
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Error: Could not open webcam.")
        return

    print("Face recognition started. Press 'q' to quit.")

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Error: Could not read frame.")
            break

        # Detect faces in the frame
        faces = app.get(frame)

        # Load known persons from database
        known_persons = get_all_embeddings(con)

        for face in faces:
            bbox = face.bbox
            embedding = face.normed_embedding

            # Try to find a match
            match, score = find_match(embedding, known_persons)

            if match is not None:
                # Recognized face
                name = match["name"]
                color = (0, 255, 0)  # Green
                update_last_seen(con, match["id"])
            else:
                # New face - enroll automatically
                person_id = get_next_id(con)
                name = f"Person {person_id}"
                color = (0, 255, 255)  # Yellow

                # Crop and encode face image
                face_crop = crop_face(frame, bbox)
                if face_crop.size > 0:
                    success, encoded = cv2.imencode(".png", face_crop)
                    if success:
                        face_bytes = encoded.tobytes()
                        insert_face(con, person_id, name, face_bytes, embedding)

                        # Add to known persons so we don't re-enroll this frame
                        known_persons.append({
                            "id": person_id,
                            "name": name,
                            "embedding": embedding
                        })

            # Draw bounding box
            x1, y1, x2, y2 = [int(coord) for coord in bbox]
            cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

            # Draw name label
            label = f"{name} ({score:.2f})" if match else name
            cv2.putText(
                frame, label, (x1, y1 - 10),
                cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2
            )

        # Show the frame
        cv2.imshow("Face Recognition", frame)

        # Quit on 'q' key
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    # Cleanup
    cap.release()
    cv2.destroyAllWindows()
    con.close()
    print("Face recognition stopped.")
    print(f"Database saved to: {db_path}")


if __name__ == "__main__":
    main()
Test with multiple people

Your script is ready. Time to see it in action with real faces on camera.

  • If you have an old faces.duckdb file from earlier testing, delete it so you start fresh.
  • Run the face recognition system by running this command:
python3 face_recognition.py

What should I see?

A window titled "Face Recognition" opens showing your webcam feed. When your face appears, a yellow bounding box with the label "Person 1" appears around it. Yellow means this is a newly enrolled face.

After the first frame, the box turns green and shows a similarity score like "Person 1 (0.87)". Green means the system recognized you from the database.

What about the macOS camera permission?

The first time you run this, macOS shows a permission dialog asking whether Terminal (or VS Code) can access your camera. Click Allow. If you accidentally denied it, open System Settings, go to Privacy & Security, then Camera, and toggle access on for Terminal.

  • Test recognition with multiple people by having someone else step in front of the camera.

The new person gets a yellow box labeled "Person 2". On subsequent frames, their box turns green with a similarity score. Your face still shows as "Person 1" with a green box.

  • Have the second person step away from the camera, then step back in.

They should be recognized as "Person 2" with a green box. The system pulls their stored embedding from the database and matches it.

  • Press q to quit the application when you are done testing.

Webcam not opening or showing a black screen?

  • Make sure no other application (FaceTime, Photo Booth, Zoom) is using your camera. Only one app can access the webcam at a time.
  • Check that you granted camera permission to Terminal or VS Code in System Settings > Privacy & Security > Camera.
  • If the window opens but detection is very slow, that is normal for the first few frames while the model warms up. Performance should stabilize to 5-15 FPS on most Macs.

Help me troubleshoot my webcam not working with OpenCV.

Faces not being detected?

  • Make sure your face is well-lit and clearly visible to the camera. The detection model works best with frontal faces in good lighting.
  • Verify that your virtual environment is activated and all packages are installed. Run verify_setup.py again to confirm.

Help me figure out why InsightFace is not detecting my face.

Inspect your database

Your face recognition system stored every person it detected in faces.duckdb. Let's query the database to see exactly who was recorded and when.

  • Print a summary table of all stored persons by running this command:
python3 -c "
import duckdb
con = duckdb.connect('faces.duckdb', read_only=True)
print('--- Stored Faces ---')
con.execute('SELECT id, name, first_seen, last_seen FROM faces ORDER BY id').show()
con.close()
"

What should I see?

A formatted table showing one row per person. Each row has the person's id, name (like "Person 1"), first_seen timestamp from when they were first enrolled, and last_seen timestamp from the most recent time the system recognized them.

If you tested with two people, you should see two rows. The last_seen time should be later than first_seen if the system recognized that person more than once.

  • Verify the total count of stored face records by running:
python3 -c "
import duckdb
con = duckdb.connect('faces.duckdb', read_only=True)
result = con.execute('SELECT COUNT(*) AS total_faces FROM faces').fetchone()
print(f'Total face records: {result[0]}')
con.close()
"

The count should match the number of unique people the system detected during your test.

Seeing zero faces or duplicate entries?

  • If you see zero rows, make sure you are running the query from the same folder where faces.duckdb was created. Run ls to confirm the file exists in your current directory.
  • If you see many duplicate entries for the same person, the similarity threshold of 0.5 may be too high for your setup. You can lower it to 0.4 in the find_match() function, delete faces.duckdb, and run the script again.

Help me understand why my database has unexpected results.

Pro tip: tuning the threshold

The 0.5 cosine similarity threshold is a starting point. If you find the system creating duplicate entries for the same person, try lowering the threshold to 0.4. If it incorrectly matches different people as the same person, raise it to 0.6. Values above 0.52 make false positives extremely rare.

Secret mission

Build a Face Gallery Viewer

Your face recognition system stores every face it detects, but you have no way to browse them visually. Build a separate script that queries your DuckDB database and displays every stored face in a clean grid, complete with names and timestamps.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Python virtual environment in your project folder.
  • DuckDB database file (faces.duckdb) storing face images and embeddings.
  • InsightFace buffalo_l model files (~326 MB, stored in ~/.insightface/models/).
  • Python packages installed via pip (insightface, onnxruntime, opencv-python, numpy, duckdb).
  • Project folder containing your Python scripts (face_recognition.py, verify_setup.py, and optionally view_gallery.py).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with your face recognition system.

  • Your project folder, database file, and virtual environment will remain on disk.
  • The buffalo_l model files stay cached in ~/.insightface/models/ so you won't need to re-download them.
  • Nothing is running in the background. The system only uses resources while the script is active.

Pause - I'll come back to this later

Free up some disk space while keeping your project files intact so you can return later.

  • Deactivate your virtual environment by running deactivate in your terminal.
  • Your faces.duckdb file and Python scripts will stay in your project folder.
  • The buffalo_l model files (~326 MB) will remain cached. You can delete them from ~/.insightface/models/ to free space. They will re-download automatically the next time you run the script.
  • To resume later, navigate to your project folder, activate the virtual environment, and run your script again.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete your entire project folder. This removes your Python scripts, virtual environment, and the faces.duckdb database file.
  • Delete the cached InsightFace models by removing the ~/.insightface/ folder.
  • Optionally, uninstall Homebrew-managed Python if you no longer need it for other projects.
  • Revoke webcam permissions for Terminal or VS Code in System Settings > Privacy & Security > Camera if you no longer want those apps to have camera access.

Mission accomplished

That's a Wrap!

Nice work! You just built a real-time face recognition system from scratch using InsightFace, OpenCV, and DuckDB.

You learned how to:

  • Detected faces in real time from your webcam using InsightFace's buffalo_l model and drew live bounding boxes with identity labels on each face.
  • Extracted 512-dimensional face embeddings and matched faces using cosine similarity to automatically enroll and recognize people without any manual input.
  • Stored cropped face screenshots and embedding vectors in a DuckDB database for persistent face recognition across sessions.
  • Built a face gallery viewer that queries all stored faces from DuckDB and displays them in a visual grid.

Ready to quiz yourself?