Skip to content

QR Code Attendance System

QR codes turn any phone or printout into a scannable ID — which is why they run everything from event check-ins to classroom attendance. This project wires together two libraries: qrcode to generate a code for each user, and OpenCV to scan it through a webcam and stamp the time. You’ll build the generate-and-scan core, then turn the toy into a usable system: a database of records, duplicate-scan prevention, a live camera overlay that shows the decoded ID, and CSV export for reports.

You will leave understanding:

  • How QR codes encode arbitrary text and how qrcode renders them.
  • The OpenCV capture loop: open camera → read frame → detect → release.
  • Why the naive version can’t tell who’s already checked in (and how to fix it).
  • How to move from “popup says scanned” to a real attendance log.
  • Python 3.6 or above.
  • A text editor or IDE.
  • pip install qrcode[pil] opencv-python.
  • A working webcam.
  • Tkinter (bundled with Python).
  1. Create a folder named qr-attendance.
  2. Inside it, create qr_code_attendance_system.py.
  3. Install dependencies: pip install qrcode[pil] opencv-python.
qr_code_attendance_system.py pch.viewSource
qr_code_attendance_system.py
"""
QR Code Attendance System

A Python application that generates QR codes for users and scans them to mark attendance. Features include:
- Generating QR codes for user identification.
- Scanning QR codes to mark attendance.
"""

import qrcode
import cv2
from tkinter import Tk, Label, Entry, Button, messagebox
from datetime import datetime


class QRCodeAttendanceSystem:
    def __init__(self, root):
        self.root = root
        self.root.title("QR Code Attendance System")

        Label(root, text="Enter User ID:").grid(row=0, column=0, padx=10, pady=10)
        self.user_id_entry = Entry(root, width=30)
        self.user_id_entry.grid(row=0, column=1, padx=10, pady=10)

        Button(root, text="Generate QR Code", command=self.generate_qr_code).grid(row=1, column=0, columnspan=2, pady=10)
        Button(root, text="Scan QR Code", command=self.scan_qr_code).grid(row=2, column=0, columnspan=2, pady=10)

    def generate_qr_code(self):
        """Generate a QR code for the entered user ID."""
        user_id = self.user_id_entry.get()
        if not user_id:
            messagebox.showerror("Error", "Please enter a User ID.")
            return

        qr = qrcode.QRCode(version=1, box_size=10, border=5)
        qr.add_data(user_id)
        qr.make(fit=True)

        img = qr.make_image(fill="black", back_color="white")
        img_path = f"{user_id}_qr.png"
        img.save(img_path)

        messagebox.showinfo("Success", f"QR Code generated and saved as {img_path}.")

    def scan_qr_code(self):
        """Scan a QR code to mark attendance."""
        cap = cv2.VideoCapture(0)
        detector = cv2.QRCodeDetector()

        while True:
            ret, frame = cap.read()
            if not ret:
                break

            data, bbox, _ = detector.detectAndDecode(frame)
            if data:
                timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                messagebox.showinfo("Attendance Marked", f"User ID: {data}\nTime: {timestamp}")
                break

            show_or_save("QR Code Scanner", frame)
            if wait_or_skip(1) & 0xFF == ord("q"):
                break

        cap.release()
        cv2.destroyAllWindows() if "--show" in __import__("sys").argv else None


def main():
    root = Tk()
    app = QRCodeAttendanceSystem(root)
    root.mainloop()


def wait_or_skip(delay=0):
    """cv2.waitKey needs a window; without one it raises. Skip it instead."""
    import sys

    if "--show" in sys.argv:
        return cv2.waitKey(delay)
    return -1


def show_or_save(title, image, _counter=[0]):
    """Display the frame, or write it to a file when no window is available.

    Headless OpenCV has no GUI at all, and even a full build cannot open a
    window over SSH or inside a container. Falling back to a file keeps the
    project runnable everywhere and leaves something to look at afterwards.
    """
    import os
    import re as _re

    if "--show" in __import__("sys").argv:
        cv2.imshow(title, image)
        return None
    _counter[0] += 1
    stem = _re.sub(r"\W+", "_", title).strip("_").lower() or "frame"
    name = f"{stem}.png" if _counter[0] == 1 else f"{stem}_{_counter[0]}.png"
    cv2.imwrite(name, image)
    print(f"saved {name}  ({os.path.getsize(name):,} bytes)")
    return name


if __name__ == "__main__":
    main()
command
C:\Users\Your Name\qr-attendance> python qr_code_attendance_system.py
# Enter a User ID and "Generate QR Code" -> saves <id>_qr.png
# "Scan QR Code" opens the webcam; show the code to mark attendance.

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid
qr_code_attendance_system.py
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(user_id)
qr.make(fit=True)
img = qr.make_image(fill="black", back_color="white")
img.save(f"{user_id}_qr.png")

A QR code is just text encoded as a grid. version sets capacity (1 = smallest), box_size is pixels per module, border is the quiet zone around it. fit=True auto-grows the version if the data doesn’t fit. Here the encoded data is simply the user ID.

qr_code_attendance_system.py
cap = cv2.VideoCapture(0)          # 0 = default webcam
detector = cv2.QRCodeDetector()

VideoCapture(0) grabs the first camera. QRCodeDetector is OpenCV’s built-in QR reader — no extra library needed.

qr_code_attendance_system.py
while True:
    ret, frame = cap.read()
    if not ret:
        break
    data, bbox, _ = detector.detectAndDecode(frame)
    if data:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        messagebox.showinfo("Attendance Marked", f"User ID: {data}\nTime: {timestamp}")
        break
    cv2.imshow("QR Code Scanner", frame)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break
cap.release()
cv2.destroyAllWindows()

Each loop reads a frame, tries to decode a QR code, and on success stamps the time and stops. cv2.imshow shows the live feed; waitKey(1) keeps it responsive and lets q quit. Always release() the camera and destroyAllWindows() — skip it and the webcam stays locked.

The Core Problem: Nothing Is Actually Stored

Section titled “The Core Problem: Nothing Is Actually Stored”

The naive version pops a message and forgets. A real system needs a record. Add SQLite:

db.py
import sqlite3
 
conn = sqlite3.connect("attendance.db")
conn.execute("""CREATE TABLE IF NOT EXISTS attendance (
    user_id TEXT, timestamp TEXT
)""")
 
def mark(user_id):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    conn.execute("INSERT INTO attendance VALUES (?, ?)", (user_id, ts))
    conn.commit()

Note ? placeholders — never f-string user input into SQL (injection risk).

A code lingering in view marks attendance dozens of times. Check today’s log first:

dedupe.py
def already_marked_today(user_id):
    today = datetime.now().strftime("%Y-%m-%d")
    row = conn.execute(
        "SELECT 1 FROM attendance WHERE user_id=? AND timestamp LIKE ?",
        (user_id, f"{today}%")).fetchone()
    return row is not None

Mark only if not already_marked_today(data).

Show the detected box and ID on the video so the user knows it worked:

overlay.py
if data and bbox is not None:
    pts = bbox.astype(int).reshape(-1, 2)
    for i in range(len(pts)):
        cv2.line(frame, tuple(pts[i]), tuple(pts[(i + 1) % len(pts)]), (0, 255, 0), 2)
    cv2.putText(frame, data, tuple(pts[0]), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)

For reports, dump the table:

export.py
import csv
def export():
    rows = conn.execute("SELECT user_id, timestamp FROM attendance").fetchall()
    with open("attendance.csv", "w", newline="") as f:
        csv.writer(f).writerows([("User ID", "Time"), *rows])
ProblemCauseFix
Camera stays on / lockedForgot release()Always cap.release() + destroyAllWindows()
Same person marked many timesNo duplicate checkSkip if already marked today
Attendance vanishes on exitNothing persistedStore in SQLite/CSV
Black window, no feedWrong camera index or in-useTry VideoCapture(1); close other apps
QR won’t scanToo small / poor lighting / glareLarger box_size, better light, hold steady
SQL breaks on odd IDsString-formatted queryUse ? parameterized queries
  1. User registry — generate codes for a whole roster from a CSV.
  2. Check-in / check-out — track both, compute hours present.
  3. Web dashboard — live attendance via Flask (see Simple Blog with Flask).
  4. Encrypted payloads — sign the QR data so codes can’t be forged.
  5. Email/SMS confirmation — notify on successful check-in.
  6. Photo capture — save a webcam snapshot with each scan.
  7. Mobile scanning — a companion phone app posting to your API.
  • Events & conferences — ticketed entry and session check-in.
  • Schools & offices — class and workplace attendance.
  • Gyms & memberships — access logging.
  • Inventory & logistics — scanning items in and out.
  • QR generation & decoding — encoding data, the qrcode and OpenCV APIs.
  • Computer vision basics — the capture/detect/release loop.
  • Database design — records, queries, and safe parameterization.
  • Data integrity — duplicate prevention and exports.
  • Persist scans to SQLite with duplicate prevention.
  • Add a live overlay showing the decoded ID.
  • Support check-in/check-out and CSV export.
  • Generate codes for a whole roster at once.

You combined qrcode and OpenCV into a scan-to-log attendance system, then closed the gap that makes the naive version a toy: it now remembers, refuses duplicates, and exports reports. Generate-then-scan plus a database is the exact recipe behind ticketing, access control, and inventory systems everywhere. Full source on GitHub. Explore more computer-vision projects on Python Central Hub.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading