Back to Blog

Building a Face-Recognition Attendance System with Python

Build Log6 min
Sungbin Kim

1. Project Overview

This is a face-recognition attendance system built with Python's face_recognition library and PyQt5. I wanted to process attendance automatically with a camera rather than a QR code. While building it, I learned that the surrounding features—UI, exception handling, and bulk registration—mattered even more than face recognition itself.

Features

  • Real-time face recognition and attendance checking
  • Registering and managing new faces
  • Saving and exporting attendance records
  • An intuitive graphical user interface
  • Bulk face registration

2. Development Environment

pip install face_recognition
pip install PyQt5
pip install opencv-python
pip install numpy
pip install Pillow

The main libraries are:

  • face_recognition: core face-recognition functions
  • PyQt5: GUI implementation
  • opencv-python: camera-stream processing
  • Pillow: image processing
  • numpy: array and matrix operations

3. System Structure

3.1 Overall structure

I divided the program into three main classes:

  1. AttendanceSystem: main-window class
  2. AttendanceTab: attendance-checking tab
  3. ManagementTab: face-management tab

Each class has a relatively focused responsibility.

3.2 AttendanceSystem

This class owns the main window and the overall UI style.

class AttendanceSystem(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.showMaximized()  # Start maximized

    def initUI(self):
        self.setWindowTitle('Face Recognition Attendance System')
        self.setStyleSheet("""
            QMainWindow { background-color: #f0f0f0; }
            QPushButton {
                background-color: #2196F3;
                color: white;
                border: none;
                padding: 12px 24px;
                border-radius: 6px;
                font-size: 14px;
                min-width: 120px;
            }
        """)

3.3 AttendanceTab

This is the core class for attendance. It handles the live camera and face-recognition loop.

3.3.1 Initialization and UI

class AttendanceTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.initUI()
        self.camera = None
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_frame)
        self.is_running = False
        self.frame_count = 0
        self.known_face_encodings = []
        self.known_face_names = []
        self.load_known_faces()
        self.present_students = set()

3.3.2 Real-time recognition

Frame processing and recognition run in this method. I process every third frame and resize it to one quarter of the original size to improve speed.

def update_frame(self):
    ret, frame = self.camera.read()
    if ret:
        process_this_frame = self.frame_count % 3 == 0
        self.frame_count += 1

        if process_this_frame:
            height, width = frame.shape[:2]
            small_frame = cv2.resize(frame, (width//4, height//4))
            rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)

            try:
                face_locations = face_recognition.face_locations(
                    rgb_small_frame, model="hog"
                )
                if face_locations:
                    face_encodings = face_recognition.face_encodings(
                        rgb_small_frame, face_locations
                    )
                    for (top, right, bottom, left), face_encoding in zip(
                        face_locations, face_encodings
                    ):
                        if self.known_face_encodings:
                            face_distances = face_recognition.face_distance(
                                self.known_face_encodings, face_encoding
                            )
                            best_match_index = np.argmin(face_distances)
                            if face_distances[best_match_index] < 0.6:
                                name = self.known_face_names[best_match_index]
                                self.record_attendance(name)
                                self.show_notification(name)
            except Exception as e:
                print(f"Recognition error: {str(e)}")

3.3.3 Recording attendance

The attendance method saves a record and updates the interface. It also prevents duplicate records during the same session.

def record_attendance(self, name):
    try:
        current_time = datetime.datetime.now()
        time_string = current_time.strftime('%H:%M:%S')
        if name in self.present_students:
            return

        faces_dir = "faces"
        image_path = os.path.join(faces_dir, f"{name}.jpg")
        if os.path.exists(image_path):
            pixmap = QPixmap(image_path)
            pixmap = pixmap.scaled(
                250, 250, Qt.KeepAspectRatio, Qt.SmoothTransformation
            )
            self.recent_face_label.setPixmap(pixmap)
            self.status_label.setText(f'✓ Attendance confirmed for {name}.')
            self.update_attendance_table(name, time_string, pixmap)
            self.save_attendance_record(name, time_string)
            self.present_students.add(name)
            self.update_absent_list()
    except Exception as e:
        print(f"Error recording attendance: {str(e)}")

3.4 ManagementTab

This class handles face registration and management. I implemented both single registration and bulk registration.

3.4.1 Registering one face

def register_face(self):
    try:
        file_name, _ = QFileDialog.getOpenFileName(
            self, "Choose a face image", "",
            "Image files (*.jpg *.jpeg *.png)"
        )
        if file_name:
            name, ok = QInputDialog.getText(
                self, 'Enter a name', 'Enter the name to register:'
            )
            if ok and name:
                faces_dir = "faces"
                if not os.path.exists(faces_dir):
                    os.makedirs(faces_dir)
                new_path = os.path.join(faces_dir, f"{name}.jpg")
                counter = 1
                while os.path.exists(new_path):
                    new_path = os.path.join(faces_dir, f"{name}_{counter}.jpg")
                    counter += 1
                image = Image.open(file_name).convert('RGB')
                image.save(new_path, 'JPEG', quality=95)
                self.known_faces.append((name, new_path))
                self.update_face_grid()
                QMessageBox.information(
                    self, 'Registration complete', f'Face registered for {name}.'
                )
    except Exception as e:
        QMessageBox.warning(self, 'Error', f'Registration failed: {str(e)}')

3.4.2 Bulk registration

Bulk registration walks through a folder, skips duplicates, checks whether a face can be detected, and reports successful, skipped, and failed files.

def bulk_register_faces(self):
    try:
        folder_path = QFileDialog.getExistingDirectory(
            self, "Choose a folder containing images"
        )
        if folder_path:
            success_count = 0
            skip_count = 0
            fail_count = 0
            error_files = []
            valid_extensions = ('.jpg', '.jpeg', '.png')

            for filename in os.listdir(folder_path):
                if filename.lower().endswith(valid_extensions):
                    try:
                        file_path = os.path.join(folder_path, filename)
                        name = os.path.splitext(filename)[0]
                        existing_path = os.path.join(faces_dir, f"{name}.jpg")
                        if os.path.exists(existing_path):
                            skip_count += 1
                            continue
                        image = face_recognition.load_image_file(file_path)
                        face_locations = face_recognition.face_locations(image)
                        if face_locations:
                            img = Image.open(file_path).convert('RGB')
                            img.save(new_path, 'JPEG', quality=95)
                            success_count += 1
                        else:
                            fail_count += 1
                            error_files.append(f"{filename} (no face detected)")
                    except Exception as e:
                        fail_count += 1
                        error_files.append(f"{filename} (error: {str(e)})")
            result_message = (
                f"Registration complete:\n\n"
                f"Success: {success_count}\n"
                f"Skipped: {skip_count}\n"
                f"Failed: {fail_count}"
            )
            QMessageBox.information(self, 'Bulk registration complete', result_message)
    except Exception as e:
        QMessageBox.warning(self, 'Error', f'Bulk registration failed: {str(e)}')

3.5 What I Fixed Because It Was Slow

At first I fed every frame straight into face_recognition and the camera moved like a slideshow. So I ran recognition on every third frame, and shrank that frame to a quarter of its width and height before passing it in. I also used HOG detection instead of CNN. I was running this on a laptop CPU, so I chose frame rate over accuracy — and paid for it in missed faces at an angle or in low light.

Moving recognition onto its own thread so the UI stopped freezing happened here too. I did not touch memory management at all.

3.6 Errors That Actually Happened

Failing to open the camera was the most common one. cv2.VideoCapture(0) is not always the built-in camera depending on the laptop, so I try a few indexes from 0 and use whichever opens.

Registering a face under an existing name used to overwrite the old file; I fixed that. A failed recognition is not an error but a normal state, so it just shows "not recognised" on screen and moves on.

4. How to Use It

The first time, you register faces in the management tab. After that you press Start Attendance in the attendance tab, the camera opens, and any registered face that comes into view gets recorded with a timestamp. Records export to CSV.

5. What I Am Not Happy With

I never measured the recognition rate properly. I stopped at "seems to work," so I have no numbers for how often it fails by lighting or angle.

The code also sits almost entirely in one file. Camera, recognition, storage and UI are tangled together, so changing one thing meant reading all of it.

The full source is on GitHub.

What bothered me most was not performance. It was that the system has to keep photographs of faces on hand. My classmates' faces were sitting as files in a folder on my laptop the whole time, and that never stopped feeling wrong. So the next time I solved the same problem, I did it with QR codes.