diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d11aba --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Python cache files +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# Virtual environment +venv/ +env/ +.venv/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log + +# Database files +*.db +*.sqlite + +# Environment variables +.env.local +.env.development +.env.production \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc92619 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Biometric Attendance System - Microservices Architecture + +This repository contains a biometric attendance system that has been refactored from a monolithic architecture to a microservices-based approach. + +## Architecture Overview + +The system has been transformed from a single monolithic file (`app_display.py` - 865 lines) into a modular microservices architecture with 5 focused services: + +### Microservices + +1. **`main_app.py`** (408 lines) - Main Application Entry Point + - Orchestrates all services + - Provides the complete GUI interface using Tkinter + - Handles service dependency injection + - Maintains the original user experience + +2. **`database_service.py`** (185 lines) - Database Operations Service + - Database connection and initialization + - Admin management (authentication, CRUD operations) + - Employee management (registration, retrieval) + - Attendance operations (check-in/out, records) + - Department management + +3. **`face_recognition_service.py`** (128 lines) - Face Recognition Service + - Camera operations and image capture + - Face detection and location identification + - Face encoding generation and comparison + - Image processing and storage + - Camera preview window management + +4. **`admin_service.py`** (107 lines) - Admin Management Service + - Admin authentication and password management + - Dashboard data aggregation + - Employee registration workflow + - Department validation + +5. **`attendance_service.py`** (143 lines) - Attendance Operations Service + - Attendance marking (check-in/check-out) + - Face validation for attendance + - Attendance records retrieval and filtering + - Excel export functionality + - Failed attempt logging + +## Usage + +### Running the Application + +To run the new microservices-based application: + +```bash +python3 main_app.py +``` + +### Legacy Version + +The original monolithic version is still available in `app_display.py` for reference, but the new microservices architecture is recommended for all use cases. + +## Benefits of Microservices Architecture + +- **Separation of Concerns**: Each service has a single, well-defined responsibility +- **Maintainability**: Easier to modify and extend individual components +- **Testability**: Services can be tested independently +- **Modularity**: Clear interfaces between services +- **Scalability**: Individual services can be optimized or replaced as needed + +## Dependencies + +The application requires the following Python packages: +- `tkinter` (GUI framework) +- `opencv-python` (cv2 - computer vision operations) +- `face_recognition` (face detection and recognition) +- `mysql-connector-python` (database connectivity) +- `numpy` (numerical operations) +- `PIL/Pillow` (image processing) +- `tkcalendar` (date picker widget) +- `bcrypt` (password hashing) +- `pandas` (data manipulation) +- `matplotlib` (plotting and visualization) +- `python-dotenv` (environment variable management) + +## Environment Setup + +1. Install required dependencies: +```bash +pip install opencv-python face_recognition mysql-connector-python numpy Pillow tkcalendar bcrypt pandas matplotlib python-dotenv +``` + +2. Set up your database configuration in a `.env` file: +``` +host=your_database_host +user=your_database_user +password=your_database_password +database=your_database_name +``` + +3. Run the application: +```bash +python3 main_app.py +``` + +## File Structure + +``` +. +├── main_app.py # Main application entry point +├── database_service.py # Database operations service +├── face_recognition_service.py # Face recognition service +├── admin_service.py # Admin management service +├── attendance_service.py # Attendance operations service +├── app_display.py # Legacy monolithic version +├── .env # Environment configuration +├── .gitignore # Git ignore rules +└── README.md # This file +``` + +## Refactoring Summary + +- **Original**: 1 file, 865 lines, monolithic architecture +- **Refactored**: 5 services, 971 lines total, microservices architecture +- **Increase**: +106 lines (12% increase for much better architecture) + +The slight increase in total lines is due to proper class structures, comprehensive documentation, better error handling, and clear separation of concerns. This is a worthwhile trade-off for the significant improvements in maintainability and modularity. \ No newline at end of file diff --git a/admin_service.py b/admin_service.py new file mode 100644 index 0000000..9c7686a --- /dev/null +++ b/admin_service.py @@ -0,0 +1,107 @@ +""" +Admin Service Module +Handles admin authentication, dashboard, and management operations +""" +import bcrypt +import mysql.connector +from tkinter import messagebox + +class InvalidDepartmentSelectionError(Exception): + pass + +class AdminService: + def __init__(self, database_service): + self.db = database_service + + def authenticate_admin(self, username, password): + """Authenticate admin user""" + try: + admin_record = self.db.get_admin_by_username(username) + if admin_record: + stored_password = admin_record[2] # password is at index 2 + if bcrypt.checkpw(password.encode('utf-8'), stored_password): + return True, "Login successful" + else: + return False, "Invalid password" + else: + return False, "Admin not found" + except Exception as e: + return False, f"Authentication error: {str(e)}" + + def add_new_admin(self, username, password): + """Add new admin user""" + if not username or not password: + return False, "Please fill all fields." + + try: + self.db.add_admin(username, password) + return True, "New admin added successfully." + except mysql.connector.IntegrityError: + return False, "Username already exists." + except Exception as e: + return False, f"Error adding admin: {str(e)}" + + def change_admin_password(self, current_username, current_password, new_password): + """Change admin password""" + if not new_password: + return False, "Please enter a new password." + + # Verify current credentials + is_authenticated, message = self.authenticate_admin(current_username, current_password) + if not is_authenticated: + return False, "Current password is incorrect." + + try: + self.db.update_admin_password(current_username, new_password) + return True, "Password changed successfully." + except Exception as e: + return False, f"Error changing password: {str(e)}" + + def get_dashboard_data(self): + """Get dashboard data including total employees, attendance rate, and department data""" + try: + total_employees = self.db.get_total_employees() + attendance_rate = self.db.get_attendance_rate() + attendance_by_department = self.db.get_attendance_by_department() + + attendance_data = [ + (dept, attendance_count, (attendance_count / employee_count) * 100 if employee_count > 0 else 0) + for dept, employee_count, attendance_count in attendance_by_department + ] + return total_employees, attendance_rate, attendance_data + except mysql.connector.Error as err: + print(f"Error: {err}") + return 0, 0, [] + + def get_all_departments(self): + """Get all active departments""" + return self.db.get_all_departments() + + def register_employee(self, emp_id, emp_name, emp_department, emp_designation, face_encoding): + """Register new employee""" + if not emp_id or not emp_name or not emp_department or not emp_designation: + return False, "Please fill all the fields." + + try: + emp_dept_id = self.db.get_department_id_by_name(emp_department) + if emp_dept_id is None: + return False, "Invalid department selected." + + encoding_data = face_encoding.tobytes() + self.db.add_employee(emp_id, emp_name, emp_dept_id, emp_designation, encoding_data) + return True, "Employee registered successfully." + except mysql.connector.IntegrityError: + return False, "Employee ID already exists." + except Exception as e: + return False, f"Error registering employee: {str(e)}" + + def validate_department_selection(self, selected_department): + """Validate if the selected department is valid""" + if selected_department == "Select Department" or not selected_department: + raise InvalidDepartmentSelectionError("Please select a valid department.") + + departments = self.get_all_departments() + if selected_department not in departments: + raise InvalidDepartmentSelectionError("Invalid department selected.") + + return True \ No newline at end of file diff --git a/app_display.py b/app_display.py index d4cb1e0..4f72df2 100644 --- a/app_display.py +++ b/app_display.py @@ -1,3 +1,19 @@ +""" +LEGACY MONOLITHIC VERSION - app_display.py +=========================================== +This is the original monolithic implementation of the Biometric Attendance System. + +For the new microservices architecture, please use: +- main_app.py (main application entry point) +- database_service.py (database operations) +- face_recognition_service.py (face recognition logic) +- admin_service.py (admin management) +- attendance_service.py (attendance operations) + +The microservices architecture provides better separation of concerns, +easier maintenance, and more modular code structure. +=========================================== +""" import tkinter as tk import os import sys @@ -17,8 +33,6 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from dotenv import load_dotenv -from refactored_project import database - load_dotenv() host = os.getenv("host") diff --git a/attendance_service.py b/attendance_service.py new file mode 100644 index 0000000..7bb6799 --- /dev/null +++ b/attendance_service.py @@ -0,0 +1,143 @@ +""" +Attendance Service Module +Handles attendance marking and viewing operations +""" +import os +import pandas as pd +import face_recognition +from datetime import datetime, timedelta +from tkinter import messagebox, filedialog + +class AttendanceService: + def __init__(self, database_service, face_recognition_service): + self.db = database_service + self.face_service = face_recognition_service + + def mark_attendance(self, emp_id, is_markin=True): + """Mark attendance for an employee""" + if not emp_id: + return False, "Employee ID is required." + + # Process image for attendance + frame, face_encoding, error = self.face_service.process_attendance_image(emp_id) + if error: + return False, error + + # Get employee record + employee_record = self.db.get_employee_by_id(emp_id) + if not employee_record: + return False, "Employee not found." + + name = employee_record[0] + known_face_encoding = employee_record[1] + + # Validate face + is_match, face_distance = self.face_service.validate_employee_face(face_encoding, known_face_encoding) + + if is_match: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + date_today = datetime.now().strftime("%Y-%m-%d") + + if is_markin: + return self._handle_checkin(emp_id, name, timestamp, frame, face_encoding) + else: + return self._handle_checkout(emp_id, name, timestamp, date_today, frame, face_encoding) + else: + # Save failed attempt image + self._save_failed_attempt(emp_id, frame, face_encoding) + return False, f"Face recognition failed. Distance: {face_distance:.3f}" + + def _handle_checkin(self, emp_id, name, timestamp, frame, face_encoding): + """Handle employee check-in""" + date_today = datetime.now().strftime("%Y-%m-%d") + + # Check if already checked in today + existing_record = self.db.check_attendance_exists(emp_id, date_today) + if existing_record: + return False, "Attendance already marked for today." + + # Save image and add attendance record + output_dir = "C:\\Users\\kumar\\Desktop\\attend" + output_dir = os.path.join(output_dir, date_today) + + image_path = self.face_service.save_face_image(frame, face_recognition.face_locations(frame), emp_id, output_dir) + transaction_id = self.db.add_attendance_in(emp_id, timestamp, image_path) + + return True, f"Checked in {name} at {timestamp}" + + def _handle_checkout(self, emp_id, name, timestamp, date_today, frame, face_encoding): + """Handle employee check-out""" + # Check if there's a check-in record for today + existing_record = self.db.check_attendance_exists(emp_id, date_today) + if not existing_record: + return False, "No check-in record found for today." + + # Save checkout image + output_dir_out = "C:\\Users\\kumar\\Desktop\\attend" + output_dir_out = os.path.join(output_dir_out, date_today) + + image_path = self.face_service.save_face_image(frame, face_recognition.face_locations(frame), f"{emp_id}_out", output_dir_out) + self.db.update_attendance_out(emp_id, timestamp, image_path, date_today) + + return True, f"Checked out {name} at {timestamp}" + + def _save_failed_attempt(self, emp_id, frame, face_encoding): + """Save failed attendance attempt image""" + output_dir = "C:\\Users\\kumar\\Desktop\\failed_attempts" + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + output_dir = os.path.join(output_dir, emp_id) + + self.face_service.save_face_image(frame, face_recognition.face_locations(frame), timestamp, output_dir) + + def get_attendance_records(self, selected_department, start_date, end_date): + """Get attendance records for a department within date range""" + try: + start_datetime = datetime.combine(start_date, datetime.min.time()) + end_datetime = datetime.combine(end_date, datetime.min.time()) + timedelta(days=1) + + export_records = self.db.get_attendance_records(selected_department, start_datetime, end_datetime) + + # Generate full date range for display + attendance_display_data = [] + full_date_range = [start_datetime + timedelta(days=i) for i in range((end_datetime - start_datetime).days)] + + for single_date in full_date_range: + date_str = single_date.strftime('%Y-%m-%d') + daily_records = self.db.get_daily_attendance_records(selected_department, single_date.date()) + + if daily_records: + for record in daily_records: + emp_id, emp_name, checkin_time, checkout_time, worktime = record + + checkin_display = checkin_time.strftime('%H:%M:%S') if checkin_time else "N/A" + checkout_display = checkout_time.strftime('%H:%M:%S') if checkout_time else "N/A" + worktime_display = str(worktime) if worktime else "N/A" + + attendance_display_data.append((date_str, emp_id, emp_name, checkin_display, checkout_display, worktime_display)) + else: + attendance_display_data.append((date_str, "No records", "", "", "", "")) + + return attendance_display_data, export_records + + except Exception as e: + messagebox.showerror("Error", f"Failed to fetch attendance data: {str(e)}") + return [], [] + + def export_attendance_to_excel(self, records): + """Export attendance records to Excel file""" + if not records: + messagebox.showerror("Error", "No data available to export.") + return False + + df = pd.DataFrame(records, columns=['Date', 'Employee ID', 'Name', 'Checkin Time', 'Checkout Time', 'Worked For']) + + file_path = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx")]) + if file_path: + try: + df.to_excel(file_path, index=False) + messagebox.showinfo("Success", f"Attendance data has been exported to {file_path}") + return True + except Exception as e: + messagebox.showerror("Error", f"Failed to export data: {str(e)}") + return False + return False \ No newline at end of file diff --git a/database_service.py b/database_service.py new file mode 100644 index 0000000..9981d4f --- /dev/null +++ b/database_service.py @@ -0,0 +1,185 @@ +""" +Database Service Module +Handles all database operations for the Biometric Attendance System +""" +import mysql.connector +import os +from dotenv import load_dotenv +import bcrypt + +class DatabaseService: + def __init__(self): + load_dotenv() + self.host = os.getenv("host") + self.user = os.getenv("user") + self.password = os.getenv("password") + self.database = os.getenv("database") + self.conn = None + self.cursor = None + self.connect() + self.initialize_tables() + + def connect(self): + """Establish database connection""" + self.conn = mysql.connector.connect( + host=self.host, + user=self.user, + password=self.password, + database=self.database + ) + self.cursor = self.conn.cursor() + + def initialize_tables(self): + """Create necessary tables if they don't exist""" + # Create admins table + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS admins ( + id INT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(255) UNIQUE, + password VARCHAR(255) + ) + ''') + + # Check if default admin exists, if not create one + self.cursor.execute("SELECT * FROM admins") + if self.cursor.fetchone() is None: + hashed_password = bcrypt.hashpw("admin123".encode('utf-8'), bcrypt.gensalt()) + self.cursor.execute("INSERT INTO admins (username, password) VALUES (%s, %s)", ('admin', hashed_password)) + self.conn.commit() + + def get_admin_by_username(self, username): + """Get admin by username""" + self.cursor.execute("SELECT * FROM admins WHERE username=%s", (username,)) + return self.cursor.fetchone() + + def add_admin(self, username, password): + """Add new admin""" + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + self.cursor.execute("INSERT INTO admins (username, password) VALUES (%s, %s)", (username, hashed_password)) + self.conn.commit() + + def update_admin_password(self, username, new_password): + """Update admin password""" + hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()) + self.cursor.execute("UPDATE admins SET password=%s WHERE username=%s", (hashed_password, username)) + self.conn.commit() + + def get_employee_by_id(self, emp_id): + """Get employee by ID""" + self.cursor.execute("SELECT em_employee_name, em_employee_face_encoding FROM employee_master WHERE em_employee_id=%s", (emp_id,)) + return self.cursor.fetchone() + + def add_employee(self, emp_id, emp_name, emp_dept_id, emp_designation, face_encoding): + """Add new employee""" + self.cursor.execute(""" + INSERT INTO employee_master + (em_employee_id, em_employee_name, em_employee_dept, em_employee_designation, em_employee_face_encoding, em_employee_active) + VALUES (%s, %s, %s, %s, %s, %s) + """, (emp_id, emp_name, emp_dept_id, emp_designation, face_encoding, 1)) + self.conn.commit() + + def get_department_id_by_name(self, dept_name): + """Get department ID by name""" + self.cursor.execute("SELECT dm_dept_id FROM department_master WHERE dm_dept_desc=%s", (dept_name,)) + result = self.cursor.fetchone() + return result[0] if result else None + + def get_all_departments(self): + """Get all active departments""" + self.cursor.execute("SELECT dm_dept_desc FROM department_master WHERE dm_dept_active = 1") + return [row[0] for row in self.cursor.fetchall()] + + def get_total_employees(self): + """Get total number of active employees""" + self.cursor.execute("SELECT COUNT(*) FROM employee_master WHERE em_employee_active = 1") + return self.cursor.fetchone()[0] + + def get_attendance_rate(self): + """Get today's attendance rate""" + self.cursor.execute(""" + SELECT + (SELECT COUNT(DISTINCT et_employee_id) + FROM employee_transactions + WHERE DATE(et_employee_in_time) = CURRENT_DATE) / + (SELECT COUNT(*) + FROM employee_master + WHERE em_employee_active = 1) * 100 AS attendance_rate; + """) + result = self.cursor.fetchone()[0] + return result or 0 + + def get_attendance_by_department(self): + """Get attendance data by department""" + self.cursor.execute(""" + SELECT + dm.dm_dept_desc AS department_desc, + COUNT(em.em_employee_id) AS employee_count, + COUNT(DISTINCT et.et_employee_id) AS attendance_count + FROM + department_master dm + LEFT JOIN + employee_master em ON dm.dm_dept_id = em.em_employee_dept AND em.em_employee_active = 1 + LEFT JOIN + employee_transactions et ON em.em_employee_id = et.et_employee_id + AND DATE(et.et_employee_in_time) = CURRENT_DATE + WHERE + dm.dm_dept_active = 1 + GROUP BY + dm.dm_dept_desc; + """) + return self.cursor.fetchall() + + def check_attendance_exists(self, emp_id, date_today): + """Check if attendance already exists for employee today""" + self.cursor.execute(""" + SELECT * FROM employee_transactions + WHERE et_employee_id=%s AND DATE(et_employee_in_time)=%s AND DATE(et_employee_out_time)=%s + """, (emp_id, date_today, date_today)) + return self.cursor.fetchone() + + def add_attendance_in(self, emp_id, timestamp, image_path): + """Add check-in attendance""" + self.cursor.execute(""" + INSERT INTO employee_transactions (et_employee_id, et_employee_in_time, et_employee_in_imgpth) + VALUES (%s, %s, %s) + """, (emp_id, timestamp, image_path)) + self.conn.commit() + return self.cursor.lastrowid + + def update_attendance_out(self, emp_id, timestamp, image_path, date_today): + """Update check-out attendance""" + self.cursor.execute(""" + UPDATE employee_transactions + SET et_employee_out_time=%s, et_employee_out_imgpth=%s, et_worktime = TIMEDIFF(et_employee_out_time, et_employee_in_time) + WHERE et_employee_id=%s AND DATE(et_employee_in_time)=%s AND et_employee_out_time IS NULL + """, (timestamp, image_path, emp_id, date_today)) + self.conn.commit() + + def get_attendance_records(self, department, start_datetime, end_datetime): + """Get attendance records for a department within date range""" + self.cursor.execute(''' + SELECT DATE(et.et_employee_in_time), et.et_employee_id, em.em_employee_name, et.et_employee_in_time, et.et_employee_out_time, et.et_worktime + FROM employee_transactions et + JOIN employee_master em ON et.et_employee_id = em.em_employee_id + JOIN department_master d ON em.em_employee_dept = d.dm_dept_id + WHERE d.dm_dept_desc = %s AND et.et_employee_in_time >= %s AND et.et_employee_in_time < %s; + ''', (department, start_datetime, end_datetime)) + return self.cursor.fetchall() + + def get_daily_attendance_records(self, department, single_date): + """Get daily attendance records for a department""" + self.cursor.execute(''' + SELECT et.et_employee_id, em.em_employee_name, et.et_employee_in_time, et.et_employee_out_time, et.et_worktime + FROM employee_transactions et + JOIN employee_master em ON et.et_employee_id = em.em_employee_id + JOIN department_master d ON em.em_employee_dept = d.dm_dept_id + WHERE d.dm_dept_desc = %s AND DATE(et.et_employee_in_time) = %s; + ''', (department, single_date)) + return self.cursor.fetchall() + + def close(self): + """Close database connection""" + if self.cursor: + self.cursor.close() + if self.conn: + self.conn.close() \ No newline at end of file diff --git a/face_recognition_service.py b/face_recognition_service.py new file mode 100644 index 0000000..ed1c2d8 --- /dev/null +++ b/face_recognition_service.py @@ -0,0 +1,128 @@ +""" +Face Recognition Service Module +Handles all face detection, encoding, and recognition operations +""" +import cv2 +import face_recognition +import numpy as np +import os +from datetime import datetime + +class FaceRecognitionService: + def __init__(self): + self.FACE_DISTANCE_THRESHOLD = 0.6 + + def capture_image_from_camera(self): + """Capture image from camera""" + cap = cv2.VideoCapture(0) + ret, frame = cap.read() + cap.release() + return ret, frame + + def detect_faces(self, frame): + """Detect faces in the frame and return face locations""" + small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25) + rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) + face_locations = face_recognition.face_locations(rgb_small_frame) + return face_locations, rgb_small_frame + + def encode_face(self, rgb_frame, face_locations): + """Generate face encoding from detected face""" + if len(face_locations) == 0: + return None, "No face detected in the image." + elif len(face_locations) > 1: + return None, "Multiple faces detected. Ensure only one face is in the frame." + + face_encodings = face_recognition.face_encodings(rgb_frame, face_locations) + if len(face_encodings) > 0: + return face_encodings[0], None + return None, "Could not encode face." + + def compare_faces(self, known_face_encoding, face_encoding): + """Compare face encodings and determine if they match""" + if known_face_encoding is None or face_encoding is None: + return False, 1.0 + + face_distance = face_recognition.face_distance([known_face_encoding], face_encoding)[0] + matches = face_distance <= self.FACE_DISTANCE_THRESHOLD + return matches, face_distance + + def extract_and_save_face(self, frame, face_locations, emp_id, output_dir): + """Extract face from frame and save to directory""" + if len(face_locations) > 0: + y1, x2, y2, x1 = face_locations[0] + face_image = frame[y1*4:y2*4, x1*4:x2*4] + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + image_path = os.path.join(output_dir, f"{emp_id}.jpg") + cv2.imwrite(image_path, face_image) + return image_path + return None + + def save_face_image(self, frame, face_loc, id, output_dir): + """Save face image for attendance tracking""" + y1, x2, y2, x1 = face_loc[0], face_loc[1], face_loc[2], face_loc[3] + face_image = frame[y1*4:y2*4, x1*4:x2*4] + + # Display captured image briefly + cv2.imshow("Captured image", frame) + cv2.waitKey(2000) + cv2.destroyAllWindows() + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + image_path = os.path.join(output_dir, f"{id}.jpg") + cv2.imwrite(image_path, face_image) + return image_path + + def process_attendance_image(self, emp_id): + """Process image for attendance marking""" + cap = cv2.VideoCapture(0) + ret, frame = cap.read() + cap.release() + + if not ret: + return None, None, "Failed to capture image." + + face_locations, rgb_small_frame = self.detect_faces(frame) + + if len(face_locations) == 0: + return None, None, "No face detected." + + face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations) + + if len(face_encodings) > 0: + face_encoding = face_encodings[0] + return frame, face_encoding, None + + return None, None, "Could not encode face." + + def validate_employee_face(self, face_encoding, known_face_encoding_bytes): + """Validate employee face against stored encoding""" + if known_face_encoding_bytes is None: + return False, 1.0 + + known_face_encoding = np.frombuffer(known_face_encoding_bytes) + return self.compare_faces(known_face_encoding, face_encoding) + + def create_camera_window_update_function(self, lmain, cap, capture_window): + """Create update function for camera preview window""" + def update_frame(): + ret, frame = cap.read() + if ret: + cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) + from PIL import Image, ImageTk + img = Image.fromarray(cv2image) + imgtk = ImageTk.PhotoImage(image=img) + lmain.imgtk = imgtk + lmain.configure(image=imgtk) + lmain.after(10, update_frame) + else: + cap.release() + capture_window.destroy() + from tkinter import messagebox + messagebox.showerror("Error", "Failed to access camera.") + return update_frame \ No newline at end of file diff --git a/main_app.py b/main_app.py new file mode 100644 index 0000000..6ac0a39 --- /dev/null +++ b/main_app.py @@ -0,0 +1,408 @@ +""" +Main Application File +Biometric Attendance System - Microservices Architecture +This file orchestrates all the services and provides the GUI interface +""" +import tkinter as tk +import os +import sys +import cv2 +import numpy as np +from tkinter import messagebox, ttk +from PIL import Image, ImageTk +from tkcalendar import DateEntry +from datetime import datetime, timedelta +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + +# Import our microservices +from database_service import DatabaseService +from face_recognition_service import FaceRecognitionService +from admin_service import AdminService, InvalidDepartmentSelectionError +from attendance_service import AttendanceService + +class BiometricAttendanceApp: + def __init__(self): + # Initialize services + self.db_service = DatabaseService() + self.face_service = FaceRecognitionService() + self.admin_service = AdminService(self.db_service) + self.attendance_service = AttendanceService(self.db_service, self.face_service) + + # Initialize GUI + self.root = tk.Tk() + self.root.title("Biometric Attendance System") + self.root.geometry("800x600") + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + + # Create frames + self.setup_frames() + self.setup_main_frame() + self.setup_admin_login_frame() + self.setup_attendance_frame() + + # Show initial frame + self.show_frame(self.main_frame) + + def setup_frames(self): + """Setup main container frames""" + self.main_frame = tk.Frame(self.root) + self.admin_login_frame = tk.Frame(self.root) + self.admin_dashboard_frame = tk.Frame(self.root) + self.attendance_frame = tk.Frame(self.root) + + for frame in (self.main_frame, self.admin_login_frame, self.admin_dashboard_frame, self.attendance_frame): + frame.grid(row=0, column=0, sticky="nsew") + + def show_frame(self, frame): + """Show specified frame""" + frame.tkraise() + + def on_closing(self): + """Handle application closing""" + if messagebox.askokcancel("Quit", "Do you want to quit?"): + try: + self.db_service.close() + except: + pass + self.root.destroy() + sys.exit() + + def setup_main_frame(self): + """Setup main welcome frame""" + for widget in self.main_frame.winfo_children(): + widget.destroy() + + tk.Label(self.main_frame, text="Biometric Attendance System", font=("Arial", 24)).pack(pady=50) + + btn_admin = tk.Button(self.main_frame, text="Admin Panel", font=("Arial", 16), + command=lambda: self.show_frame(self.admin_login_frame)) + btn_admin.pack(pady=20) + + btn_attendance = tk.Button(self.main_frame, text="Mark Attendance", font=("Arial", 16), + command=lambda: self.show_frame(self.attendance_frame)) + btn_attendance.pack(pady=20) + + def setup_admin_login_frame(self): + """Setup admin login frame""" + for widget in self.admin_login_frame.winfo_children(): + widget.destroy() + + tk.Label(self.admin_login_frame, text="Admin Login", font=("Arial", 20)).pack(pady=20) + + tk.Label(self.admin_login_frame, text="Username:").pack(pady=5) + self.admin_username_entry = tk.Entry(self.admin_login_frame) + self.admin_username_entry.pack(pady=5) + + tk.Label(self.admin_login_frame, text="Password:").pack(pady=5) + self.admin_password_entry = tk.Entry(self.admin_login_frame, show="*") + self.admin_password_entry.pack(pady=5) + + btn_login = tk.Button(self.admin_login_frame, text="Login", command=self.admin_login) + btn_login.pack(pady=10) + + btn_back = tk.Button(self.admin_login_frame, text="Back", command=lambda: self.show_frame(self.main_frame)) + btn_back.pack(pady=10) + + def admin_login(self): + """Handle admin login""" + username = self.admin_username_entry.get() + password = self.admin_password_entry.get() + + is_authenticated, message = self.admin_service.authenticate_admin(username, password) + + if is_authenticated: + self.current_admin = username + self.setup_admin_dashboard() + self.show_frame(self.admin_dashboard_frame) + self.admin_username_entry.delete(0, tk.END) + self.admin_password_entry.delete(0, tk.END) + else: + messagebox.showerror("Login Failed", message) + + def setup_admin_dashboard(self): + """Setup admin dashboard frame""" + for widget in self.admin_dashboard_frame.winfo_children(): + widget.destroy() + + tk.Label(self.admin_dashboard_frame, text="Admin Dashboard", font=("Arial", 20)).pack(pady=20) + + # Dashboard data + total_employees, attendance_rate, attendance_data = self.admin_service.get_dashboard_data() + + # Display dashboard info + info_frame = tk.Frame(self.admin_dashboard_frame) + info_frame.pack(pady=10) + + tk.Label(info_frame, text=f"Total Employees: {total_employees}", font=("Arial", 12)).pack() + tk.Label(info_frame, text=f"Today's Attendance Rate: {attendance_rate:.1f}%", font=("Arial", 12)).pack() + + # Dashboard plot + if attendance_data: + self.create_dashboard_plot(attendance_data) + + # Admin menu buttons + btn_frame = tk.Frame(self.admin_dashboard_frame) + btn_frame.pack(pady=20) + + tk.Button(btn_frame, text="View Attendance", command=self.setup_view_attendance_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Register Employee", command=self.setup_registration_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Add Admin", command=self.setup_add_admin_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Change Password", command=self.setup_change_password_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Logout", command=lambda: self.show_frame(self.main_frame)).pack(side=tk.LEFT, padx=5) + + def create_dashboard_plot(self, attendance_data): + """Create attendance dashboard plot""" + departments = [data[0] for data in attendance_data] + attendance_percentages = [data[2] for data in attendance_data] + + fig, ax = plt.subplots(figsize=(8, 4)) + ax.bar(departments, attendance_percentages) + ax.set_ylabel('Attendance Percentage') + ax.set_title('Department-wise Attendance Today') + ax.set_ylim(0, 100) + + canvas = FigureCanvasTkAgg(fig, self.admin_dashboard_frame) + canvas.draw() + canvas.get_tk_widget().pack() + + def setup_view_attendance_frame(self): + """Setup view attendance frame""" + view_window = tk.Toplevel(self.root) + view_window.title("View Attendance") + view_window.geometry("800x600") + + tk.Label(view_window, text="View Attendance Records", font=("Arial", 16)).pack(pady=10) + + # Department selection + tk.Label(view_window, text="Select Department:").pack() + departments = self.admin_service.get_all_departments() + department_var = tk.StringVar(value="Select Department") + department_combo = ttk.Combobox(view_window, textvariable=department_var, values=departments, state="readonly") + department_combo.pack(pady=5) + + # Date selection + date_frame = tk.Frame(view_window) + date_frame.pack(pady=10) + + tk.Label(date_frame, text="From:").grid(row=0, column=0, padx=5) + start_date = DateEntry(date_frame) + start_date.grid(row=0, column=1, padx=5) + + tk.Label(date_frame, text="To:").grid(row=0, column=2, padx=5) + end_date = DateEntry(date_frame) + end_date.grid(row=0, column=3, padx=5) + + # Display button + def display_attendance(): + selected_department = department_var.get() + try: + self.admin_service.validate_department_selection(selected_department) + attendance_data, export_records = self.attendance_service.get_attendance_records( + selected_department, start_date.get(), end_date.get()) + + # Create treeview for display + tree = ttk.Treeview(view_window, columns=('Date', 'EmpID', 'Name', 'CheckIn', 'CheckOut', 'WorkTime'), show='headings') + + for col in tree['columns']: + tree.heading(col, text=col) + tree.column(col, width=100) + + for record in attendance_data: + tree.insert('', tk.END, values=record) + + tree.pack(fill=tk.BOTH, expand=True, pady=10) + + # Export button + tk.Button(view_window, text="Export to Excel", + command=lambda: self.attendance_service.export_attendance_to_excel(export_records)).pack(pady=5) + + except InvalidDepartmentSelectionError as e: + messagebox.showerror("Error", str(e)) + + tk.Button(view_window, text="Display Attendance", command=display_attendance).pack(pady=10) + + def setup_registration_frame(self): + """Setup employee registration frame""" + reg_window = tk.Toplevel(self.root) + reg_window.title("Register Employee") + reg_window.geometry("400x500") + + tk.Label(reg_window, text="Employee Registration", font=("Arial", 16)).pack(pady=10) + + # Form fields + tk.Label(reg_window, text="Employee ID:").pack() + emp_id_entry = tk.Entry(reg_window) + emp_id_entry.pack(pady=5) + + tk.Label(reg_window, text="Name:").pack() + name_entry = tk.Entry(reg_window) + name_entry.pack(pady=5) + + tk.Label(reg_window, text="Department:").pack() + departments = self.admin_service.get_all_departments() + dept_var = tk.StringVar(value="Select Department") + dept_combo = ttk.Combobox(reg_window, textvariable=dept_var, values=departments, state="readonly") + dept_combo.pack(pady=5) + + tk.Label(reg_window, text="Designation:").pack() + designation_entry = tk.Entry(reg_window) + designation_entry.pack(pady=5) + + def capture_image(): + """Capture and process employee image""" + capture_window = tk.Toplevel(reg_window) + capture_window.title("Capture Image") + + lmain = tk.Label(capture_window) + lmain.pack() + + cap = cv2.VideoCapture(0) + + update_frame = self.face_service.create_camera_window_update_function(lmain, cap, capture_window) + + def capture_and_save(): + ret, frame = cap.read() + if ret: + cap.release() + capture_window.destroy() + + face_locations, rgb_frame = self.face_service.detect_faces(frame) + face_encoding, error = self.face_service.encode_face(rgb_frame, face_locations) + + if error: + messagebox.showerror("Error", error) + return + + emp_id = emp_id_entry.get() + emp_name = name_entry.get() + emp_department = dept_var.get() + emp_designation = designation_entry.get() + + # Save face image + output_dir = "C:\\Users\\kumar\\Desktop\\register" + self.face_service.extract_and_save_face(frame, face_locations, emp_id, output_dir) + + # Register employee + success, message = self.admin_service.register_employee( + emp_id, emp_name, emp_department, emp_designation, face_encoding) + + if success: + messagebox.showinfo("Success", message) + reg_window.destroy() + else: + messagebox.showerror("Error", message) + + tk.Button(capture_window, text="Capture", command=capture_and_save).pack() + + def on_closing(): + cap.release() + capture_window.destroy() + + capture_window.protocol("WM_DELETE_WINDOW", on_closing) + update_frame() + + tk.Button(reg_window, text="Capture Image", command=capture_image).pack(pady=20) + + def setup_add_admin_frame(self): + """Setup add admin frame""" + admin_window = tk.Toplevel(self.root) + admin_window.title("Add Admin") + admin_window.geometry("300x200") + + tk.Label(admin_window, text="Add New Admin", font=("Arial", 16)).pack(pady=10) + + tk.Label(admin_window, text="Username:").pack() + username_entry = tk.Entry(admin_window) + username_entry.pack(pady=5) + + tk.Label(admin_window, text="Password:").pack() + password_entry = tk.Entry(admin_window, show="*") + password_entry.pack(pady=5) + + def save_admin(): + username = username_entry.get() + password = password_entry.get() + + success, message = self.admin_service.add_new_admin(username, password) + + if success: + messagebox.showinfo("Success", message) + admin_window.destroy() + else: + messagebox.showerror("Error", message) + + tk.Button(admin_window, text="Save", command=save_admin).pack(pady=10) + + def setup_change_password_frame(self): + """Setup change password frame""" + pwd_window = tk.Toplevel(self.root) + pwd_window.title("Change Password") + pwd_window.geometry("300x250") + + tk.Label(pwd_window, text="Change Admin Password", font=("Arial", 16)).pack(pady=10) + + tk.Label(pwd_window, text="Current Password:").pack() + current_pwd_entry = tk.Entry(pwd_window, show="*") + current_pwd_entry.pack(pady=5) + + tk.Label(pwd_window, text="New Password:").pack() + new_pwd_entry = tk.Entry(pwd_window, show="*") + new_pwd_entry.pack(pady=5) + + def change_password(): + current_pwd = current_pwd_entry.get() + new_pwd = new_pwd_entry.get() + + success, message = self.admin_service.change_admin_password( + self.current_admin, current_pwd, new_pwd) + + if success: + messagebox.showinfo("Success", message) + pwd_window.destroy() + else: + messagebox.showerror("Error", message) + + tk.Button(pwd_window, text="Change Password", command=change_password).pack(pady=10) + + def setup_attendance_frame(self): + """Setup attendance marking frame""" + for widget in self.attendance_frame.winfo_children(): + widget.destroy() + + tk.Label(self.attendance_frame, text="Mark Attendance", font=("Arial", 20)).pack(pady=20) + + tk.Label(self.attendance_frame, text="Employee ID:").pack(pady=10) + self.employee_id_entry = tk.Entry(self.attendance_frame) + self.employee_id_entry.pack(pady=5) + + def mark_attendance(is_markin=True): + emp_id = self.employee_id_entry.get().strip() + success, message = self.attendance_service.mark_attendance(emp_id, is_markin) + + if success: + messagebox.showinfo("Attendance", message) + else: + messagebox.showerror("Error", message) + + self.employee_id_entry.delete(0, tk.END) + + btn_mark_in = tk.Button(self.attendance_frame, text="Mark In", + command=lambda: mark_attendance(is_markin=True)) + btn_mark_in.pack(pady=10) + + btn_mark_out = tk.Button(self.attendance_frame, text="Mark Out", + command=lambda: mark_attendance(is_markin=False)) + btn_mark_out.pack(pady=10) + + btn_back = tk.Button(self.attendance_frame, text="Back to Main Screen", + command=lambda: [self.employee_id_entry.delete(0, tk.END), self.show_frame(self.main_frame)]) + btn_back.pack(pady=10) + + def run(self): + """Start the application""" + self.root.mainloop() + +if __name__ == "__main__": + app = BiometricAttendanceApp() + app.run() \ No newline at end of file