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 functionsPyQt5: GUI implementationopencv-python: camera-stream processingPillow: image processingnumpy: array and matrix operations
3. System Structure
3.1 Overall structure
I divided the program into three main classes:
AttendanceSystem: main-window classAttendanceTab: attendance-checking tabManagementTab: 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.
