redc007 commited on
Commit
0d4913c
·
verified ·
1 Parent(s): 1de2153

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +454 -0
app.py ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ FastAPI Attendance System - Production Ready
4
+ Run with: uvicorn main:app --host 0.0.0.0 --port 5001 --workers 4
5
+ """
6
+
7
+ import base64
8
+ import io
9
+ import os
10
+ import sys
11
+ from datetime import datetime
12
+ from typing import Optional
13
+
14
+ import cv2
15
+ import numpy as np
16
+ import face_recognition
17
+ import mysql.connector
18
+ from fastapi import FastAPI, File, Form, UploadFile, HTTPException, status
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from fastapi.responses import JSONResponse
21
+ from PIL import Image
22
+ from pydantic import BaseModel
23
+ from mysql.connector import Error
24
+ from werkzeug.utils import secure_filename
25
+
26
+ # Import the core logic function
27
+ from attendance_marking import markAttendance
28
+
29
+ # --- Configuration ---
30
+ DB_CONFIG = {
31
+ 'user': 'root',
32
+ 'password': 'redclaws',
33
+ 'host': 'localhost',
34
+ 'database': 'NewAttn',
35
+ }
36
+
37
+ PATH_KNOWN_DATA = 'known_data'
38
+ PATH_TRAINING_IMAGES = 'Training_images'
39
+ FACE_DISTANCE_THRESHOLD = 0.5
40
+
41
+ # Ensure directories exist
42
+ os.makedirs(PATH_KNOWN_DATA, exist_ok=True)
43
+ os.makedirs(PATH_TRAINING_IMAGES, exist_ok=True)
44
+
45
+ # --- Data Models ---
46
+ class AttendanceRequest(BaseModel):
47
+ image: str # Base64 encoded image
48
+
49
+ class AttendanceResponse(BaseModel):
50
+ status: str
51
+ message: str
52
+ emp_id: Optional[str] = None
53
+ distance: Optional[str] = None
54
+
55
+ class RegistrationResponse(BaseModel):
56
+ status: str
57
+ message: str
58
+ emp_id: Optional[str] = None
59
+
60
+ # --- Helper Functions ---
61
+ def load_known_data():
62
+ """Loads encodings and emp_ids from the known_data directory."""
63
+ try:
64
+ encodings = np.load(
65
+ os.path.join(PATH_KNOWN_DATA, 'known_encodings.npy'),
66
+ allow_pickle=True
67
+ )
68
+ with open(os.path.join(PATH_KNOWN_DATA, 'known_names.txt'), 'r') as f:
69
+ emp_ids = [line.strip() for line in f.readlines()]
70
+ print(f"✅ Loaded {len(encodings)} faces.")
71
+ return encodings, emp_ids
72
+ except FileNotFoundError:
73
+ print("⚠️ No existing known data found. Starting fresh.")
74
+ return np.array([]), []
75
+ def findEncodings(images):
76
+ encodeList = []
77
+ for img in images:
78
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
79
+ face_encodings = face_recognition.face_encodings(img)
80
+ if face_encodings:
81
+ encodeList.append(face_encodings[0])
82
+ else:
83
+ print("Warning: No face found in one of the training images!")
84
+ return encodeList
85
+
86
+ # Function to mark attendance using MySQL
87
+ def markAttendance(emp_id):
88
+ now = datetime.now()
89
+ dtString = now.strftime('%H:%M:%S')
90
+ dateString = now.strftime('%Y-%m-%d')
91
+
92
+ cursor = None
93
+ try:
94
+ record_count=0
95
+ # cursor = db_conn.cursor()
96
+
97
+ # # ✅ FIXED: Use COUNT(*) to get the number of matching records
98
+ # check_query = """
99
+ # SELECT COUNT(*)
100
+ # FROM dailylog
101
+ # WHERE emp_id = %s AND date = %s
102
+ # """
103
+ # cursor.execute(check_query, (emp_id, dateString))
104
+
105
+ # result = cursor.fetchone()
106
+ # record_count = result[0] if result else 0
107
+ # print(f"DEBUG: emp_id={emp_id}, date={dateString}, count={record_count}")
108
+
109
+ if record_count == 0:
110
+ # insert_query = """
111
+ # INSERT INTO dailylog (emp_id, date, punch_in)
112
+ # VALUES (%s, %s, %s)
113
+ # """
114
+ # cursor.execute(insert_query, (emp_id, dateString, dtString))
115
+ # db_conn.commit()
116
+ return True , "new"
117
+ else:
118
+ return False , "duplicate"
119
+
120
+ except Error as e:
121
+ # print(f"DB error: {e}")
122
+ # db_conn.rollback()
123
+ return False , f"error: {e}"
124
+ # def get_db_connection():
125
+ # """Get a database connection."""
126
+ # try:
127
+ # return mysql.connector.connect(**DB_CONFIG)
128
+ # except Error as e:
129
+ # print(f"❌ Database connection error: {e}")
130
+ # raise HTTPException(
131
+ # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
132
+ # detail=f"Database connection failed: {str(e)}"
133
+ # )
134
+
135
+ # --- Initialize FastAPI App ---
136
+ app = FastAPI(
137
+ title="Facial Attendance System API",
138
+ description="Production-ready facial recognition attendance system",
139
+ version="1.0.0"
140
+ )
141
+
142
+ # Configure CORS
143
+ app.add_middleware(
144
+ CORSMiddleware,
145
+ allow_origins=["*"], # In production, replace with specific origins
146
+ allow_credentials=True,
147
+ allow_methods=["*"],
148
+ allow_headers=["*"],
149
+ )
150
+
151
+ # Global variables for encodings (loaded at startup)
152
+ encodeListKnown = np.array([])
153
+ classNames = []
154
+
155
+ # --- Startup Event ---
156
+ @app.on_event("startup")
157
+ async def startup_event():
158
+ """Load known face encodings on startup."""
159
+ global encodeListKnown, classNames
160
+ encodeListKnown, classNames = load_known_data()
161
+ print("🚀 FastAPI Attendance System Started")
162
+
163
+ # --- Health Check Endpoint ---
164
+ @app.get("/health")
165
+ async def health_check():
166
+ """Health check endpoint."""
167
+ return {
168
+ "status": "healthy",
169
+ "timestamp": datetime.now().isoformat(),
170
+ "registered_employees": len(classNames)
171
+ }
172
+
173
+ # --- Attendance Marking Endpoint ---
174
+ @app.post("/api/mark_attendance", response_model=AttendanceResponse)
175
+ async def mark_attendance_api(request: AttendanceRequest):
176
+ """
177
+ Mark attendance using facial recognition.
178
+
179
+ Args:
180
+ request: AttendanceRequest containing base64 encoded image
181
+
182
+ Returns:
183
+ AttendanceResponse with status and details
184
+ """
185
+ if not request.image:
186
+ raise HTTPException(
187
+ status_code=status.HTTP_400_BAD_REQUEST,
188
+ detail="No image data provided"
189
+ )
190
+
191
+ #db_conn = None
192
+ try:
193
+ # 1. Decode and Convert Image
194
+ image_bytes = base64.b64decode(request.image)
195
+ img = Image.open(io.BytesIO(image_bytes)).convert('RGB')
196
+ img_np = np.array(img)
197
+ img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
198
+
199
+ # 2. Process Image (resize for faster detection)
200
+ imgS = cv2.resize(img_bgr, (0, 0), None, 0.25, 0.25)
201
+ imgS = cv2.cvtColor(imgS, cv2.COLOR_BGR2RGB)
202
+
203
+ # 3. Detect faces
204
+ facesCurFrame = face_recognition.face_locations(imgS)
205
+ encodesCurFrame = face_recognition.face_encodings(imgS, facesCurFrame)
206
+
207
+ if not facesCurFrame:
208
+ return AttendanceResponse(
209
+ status="failure",
210
+ message="No face detected in the image."
211
+ )
212
+
213
+ # 4. Match face
214
+ encodeFace = encodesCurFrame[0]
215
+ matches = face_recognition.compare_faces(encodeListKnown, encodeFace)
216
+ faceDis = face_recognition.face_distance(encodeListKnown, encodeFace)
217
+
218
+ if len(faceDis) == 0:
219
+ return AttendanceResponse(
220
+ status="failure",
221
+ message="No registered employees in the system."
222
+ )
223
+
224
+ matchIndex = np.argmin(faceDis)
225
+ min_distance = faceDis[matchIndex]
226
+
227
+ # 5. Check threshold and mark attendance
228
+ if matches[matchIndex] and min_distance < FACE_DISTANCE_THRESHOLD:
229
+ employee_id = classNames[matchIndex].upper()
230
+
231
+ # Get DB connection
232
+ # db_conn = get_db_connection()
233
+
234
+ # Mark attendance
235
+ was_marked, status_msg = markAttendance(employee_id)
236
+
237
+ if was_marked:
238
+ return AttendanceResponse(
239
+ status="success",
240
+ message=f"Attendance marked for {employee_id}.",
241
+ emp_id=employee_id,
242
+ distance=f"{min_distance:.2f}"
243
+ )
244
+ elif status_msg == "duplicate":
245
+ return AttendanceResponse(
246
+ status="info",
247
+ message=f"{employee_id} already logged today.",
248
+ emp_id=employee_id,
249
+ distance=f"{min_distance:.2f}"
250
+ )
251
+ else:
252
+ return AttendanceResponse(
253
+ status="error",
254
+ message=f"Failed to mark attendance: {status_msg}"
255
+ )
256
+ else:
257
+ return AttendanceResponse(
258
+ status="failure",
259
+ message=f"Unknown person detected. Min Distance: {min_distance:.2f}"
260
+ )
261
+
262
+ except Error as err:
263
+ print(f"❌ Database Error: {db_err}")
264
+ raise HTTPException(
265
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
266
+ detail=f"Database error: {str(db_err)}"
267
+ )
268
+ except Exception as e:
269
+ print(f"❌ Exception: {e}")
270
+ raise HTTPException(
271
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
272
+ detail=f"Internal server error: {str(e)}"
273
+ )
274
+ # finally:
275
+ # if db_conn is not None and db_conn.is_connected():
276
+ # db_conn.close()
277
+
278
+ # --- Employee Registration Endpoint ---
279
+ @app.post("/api/register_employee", response_model=RegistrationResponse)
280
+ async def register_employee(
281
+ emp_id: str = Form(...),
282
+ image: UploadFile = File(...)
283
+ ):
284
+ """
285
+ Register a new employee with facial recognition.
286
+
287
+ Args:
288
+ emp_id: Employee ID
289
+ image: Employee face image file
290
+
291
+ Returns:
292
+ RegistrationResponse with status and details
293
+ """
294
+ global encodeListKnown, classNames
295
+
296
+ db_conn = None
297
+ cursor = None
298
+ image_path = None
299
+
300
+ try:
301
+ # Validate inputs
302
+ if not emp_id or not image:
303
+ raise HTTPException(
304
+ status_code=status.HTTP_400_BAD_REQUEST,
305
+ detail="Employee ID and image are required."
306
+ )
307
+
308
+ # Sanitize employee ID
309
+ emp_id = emp_id.strip().upper()
310
+
311
+ # Check if employee already exists
312
+ if emp_id in classNames:
313
+ raise HTTPException(
314
+ status_code=status.HTTP_400_BAD_REQUEST,
315
+ detail=f"Employee {emp_id} already exists in the system."
316
+ )
317
+
318
+ # Validate image file
319
+ if not image.content_type.startswith('image/'):
320
+ raise HTTPException(
321
+ status_code=status.HTTP_400_BAD_REQUEST,
322
+ detail="Invalid file type. Please upload an image."
323
+ )
324
+
325
+ # Save the image
326
+ filename = secure_filename(f"{emp_id}.jpg")
327
+ image_path = os.path.join(PATH_TRAINING_IMAGES, filename)
328
+
329
+ # Read and save image
330
+ contents = await image.read()
331
+ with open(image_path, 'wb') as f:
332
+ f.write(contents)
333
+
334
+ # Load and process the image
335
+ img = cv2.imread(image_path)
336
+ if img is None:
337
+ if os.path.exists(image_path):
338
+ os.remove(image_path)
339
+ raise HTTPException(
340
+ status_code=status.HTTP_400_BAD_REQUEST,
341
+ detail="Failed to read the uploaded image."
342
+ )
343
+
344
+ # Convert to RGB for face_recognition
345
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
346
+
347
+ # Detect faces and generate encoding
348
+ face_encodings = face_recognition.face_encodings(img_rgb)
349
+
350
+ if not face_encodings:
351
+ os.remove(image_path)
352
+ raise HTTPException(
353
+ status_code=status.HTTP_400_BAD_REQUEST,
354
+ detail="No face detected in the image. Please upload a clear face photo."
355
+ )
356
+
357
+ if len(face_encodings) > 1:
358
+ os.remove(image_path)
359
+ raise HTTPException(
360
+ status_code=status.HTTP_400_BAD_REQUEST,
361
+ detail="Multiple faces detected. Please upload an image with only one face."
362
+ )
363
+
364
+ # Get the encoding
365
+ new_encoding = face_encodings[0]
366
+
367
+ # Update global arrays
368
+ if len(encodeListKnown) == 0:
369
+ encodeListKnown = np.array([new_encoding])
370
+ else:
371
+ encodeListKnown = np.vstack([encodeListKnown, new_encoding])
372
+
373
+ classNames.append(emp_id)
374
+
375
+ # Save updated encodings and names
376
+ np.save(
377
+ os.path.join(PATH_KNOWN_DATA, 'known_encodings.npy'),
378
+ encodeListKnown
379
+ )
380
+
381
+ with open(os.path.join(PATH_KNOWN_DATA, 'known_names.txt'), 'w') as f:
382
+ for name in classNames:
383
+ f.write(f"{name}\n")
384
+
385
+ # Insert into database
386
+ # db_conn = get_db_connection()
387
+ # cursor = db_conn.cursor()
388
+
389
+ # query = """
390
+ # INSERT INTO employees (emp_id, name, email, department)
391
+ # VALUES (%s, %s, %s, %s)
392
+ # """
393
+ # cursor.execute(query, (emp_id, "DUMMY", "dummy@mail.com", "DummyDept"))
394
+ # db_conn.commit()
395
+
396
+ print(f"✅ Successfully registered employee: {emp_id}")
397
+
398
+ return RegistrationResponse(
399
+ status="success",
400
+ message=f"Employee {emp_id} registered successfully!",
401
+ emp_id=emp_id
402
+ )
403
+
404
+ except HTTPException:
405
+ # Re-raise HTTP exceptions
406
+ raise
407
+ except Exception as e:
408
+ print(f"❌ Error during registration: {e}")
409
+ if db_conn:
410
+ db_conn.rollback()
411
+ if image_path and os.path.exists(image_path):
412
+ os.remove(image_path)
413
+ raise HTTPException(
414
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
415
+ detail=f"Registration failed: {str(e)}"
416
+ )
417
+ # finally:
418
+ # if cursor:
419
+ # cursor.close()
420
+ # if db_conn is not None and db_conn.is_connected():
421
+ # db_conn.close()
422
+
423
+ # --- Get Registered Employees Endpoint ---
424
+ @app.get("/api/employees")
425
+ async def get_employees():
426
+ """Get list of all registered employees."""
427
+ return {
428
+ "status": "success",
429
+ "count": len(classNames),
430
+ "employees": classNames
431
+ }
432
+
433
+ # --- Root Endpoint ---
434
+ @app.get("/")
435
+ async def root():
436
+ """Root endpoint."""
437
+ return {
438
+ "message": "Facial Attendance System API",
439
+ "version": "1.0.0",
440
+ "docs": "/docs",
441
+ "health": "/health"
442
+ }
443
+
444
+
445
+ if __name__ == "__main__":
446
+ import uvicorn
447
+ uvicorn.run(
448
+ "app:app",
449
+ host="0.0.0.0",
450
+ port=5001,
451
+ reload=False, # Set to False in production
452
+ workers=1 # Increase for production (e.g., 4)
453
+
454
+ )