PrashanthB461 commited on
Commit
e5c009b
Β·
verified Β·
1 Parent(s): 64ead65

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +399 -313
app.py CHANGED
@@ -18,7 +18,6 @@ import pickle
18
  import cv2
19
  import gradio as gr
20
  import numpy as np
21
- import pandas as pd
22
  from PIL import Image
23
  import requests
24
  from dotenv import load_dotenv
@@ -47,6 +46,12 @@ SF_CREDENTIALS = {
47
  "domain": os.getenv("SF_DOMAIN", "login")
48
  }
49
 
 
 
 
 
 
 
50
  # --- SALESFORCE CONNECTION ---
51
 
52
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
@@ -59,22 +64,22 @@ def connect_to_salesforce() -> Optional[Salesforce]:
59
  return sf
60
  except Exception as e:
61
  logger.error(f"❌ Salesforce connection failed: {e}")
62
- raise
63
 
64
  # --- CORE LOGIC ---
65
 
66
  class AttendanceSystem:
67
  """
68
- Manages all backend logic for the face recognition attendance system.
69
  """
70
  def __init__(self):
71
  # State Management
72
  self.processing_thread = None
73
  self.is_processing = threading.Event()
74
- self.frame_queue = queue.Queue(maxsize=10)
75
- self.error_message = None
76
- self.last_processed_frame = None # Holds the final frame after processing
77
- self.final_log = None # Holds the final log after processing
78
 
79
  # Data Storage
80
  self.known_face_embeddings: List[np.ndarray] = []
@@ -83,15 +88,10 @@ class AttendanceSystem:
83
  self.next_worker_id: int = 1
84
 
85
  # Session Tracking
86
- self.last_recognition_time: Dict[str, float] = {}
87
- self.recognition_cooldown = 5 # seconds
88
  self.session_log: List[str] = []
89
- self.today_attendance: Dict[str, bool] = {} # Track attendance for today
90
- self.last_detected_faces: Dict[str, float] = {} # Track last detection time per worker
91
-
92
- # Performance optimization
93
- self.frame_skip = 2 # Process every 3rd frame for faster processing
94
- self.frame_counter = 0
95
 
96
  # Initialize
97
  self.sf = connect_to_salesforce()
@@ -102,106 +102,134 @@ class AttendanceSystem:
102
  os.makedirs("data/faces", exist_ok=True)
103
 
104
  def load_worker_data(self):
105
- logger.info("Loading worker data...")
106
- if self.sf:
107
- try:
108
  workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
109
- if not workers:
110
- self._load_local_worker_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  return
112
-
113
- temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
114
- for worker in workers:
115
- if worker.get('Face_Embedding__c'):
116
- temp_embeddings.append(np.array(json.loads(worker['Face_Embedding__c'])))
117
- temp_names.append(worker['Name'])
118
- temp_ids.append(worker['Worker_ID__c'])
119
- try:
120
- worker_num = int(worker['Worker_ID__c'][1:])
121
- if worker_num > max_id:
122
- max_id = worker_num
123
- except (ValueError, TypeError):
124
- continue
125
-
126
- self.known_face_embeddings = temp_embeddings
127
- self.known_face_names = temp_names
128
- self.known_face_ids = temp_ids
129
- self.next_worker_id = max_id + 1
130
- self.save_local_worker_data()
131
- logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from Salesforce.")
132
- except Exception as e:
133
- logger.error(f"❌ Error loading from Salesforce: {e}. Attempting local load.")
134
- self._load_local_worker_data()
135
- else:
136
- logger.warning("Salesforce not connected. Loading from local cache.")
137
  self._load_local_worker_data()
138
 
139
  def _load_local_worker_data(self):
140
  try:
141
  if os.path.exists("data/workers.pkl"):
142
- with open("data/workers.pkl", "rb") as f: data = pickle.load(f)
 
143
  self.known_face_embeddings = data.get("embeddings", [])
144
  self.known_face_names = data.get("names", [])
145
  self.known_face_ids = data.get("ids", [])
146
  self.next_worker_id = data.get("next_id", 1)
147
  logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from local cache.")
148
  except Exception as e:
149
- logger.error(f"❌ Error loading local data: {e}")
150
 
151
  def save_local_worker_data(self):
152
  try:
153
- worker_data = {"embeddings": self.known_face_embeddings, "names": self.known_face_names, "ids": self.known_face_ids, "next_id": self.next_worker_id}
154
- with open("data/workers.pkl", "wb") as f: pickle.dump(worker_data, f)
 
 
 
 
 
 
155
  except Exception as e:
156
- logger.error(f"❌ Error saving local worker data: {e}")
157
 
158
  # --- Registration and Attendance ---
159
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
160
  if image is None or not name.strip():
161
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
 
162
  try:
163
  image_array = np.array(image)
164
- # Analyze face quality before registration
165
- analysis = DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
166
- if analysis[0]['face_confidence'] < 0.95:
167
- return "❌ Face not clear enough for registration!", self.get_registered_workers_info()
168
 
169
- embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  if self._is_duplicate_face(embedding):
171
- return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
172
 
173
  worker_id = f"W{self.next_worker_id:04d}"
174
  name = name.strip().title()
175
  self._add_worker_to_system(worker_id, name, embedding, image_array)
176
- self.save_local_worker_data()
177
- self.load_worker_data()
178
  return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
179
- except ValueError:
180
- return "❌ No face detected in the image!", self.get_registered_workers_info()
181
  except Exception as e:
182
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
183
 
184
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
185
  try:
186
- # Check face quality before auto-registration
187
- analysis = DeepFace.analyze(img_path=face_image, actions=['emotion'], enforce_detection=False)
188
- if analysis[0]['face_confidence'] < 0.95:
 
 
 
 
 
189
  return None
190
-
191
- embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
192
- if self._is_duplicate_face(embedding):
 
 
 
 
 
 
 
193
  return None
194
-
195
  worker_id = f"W{self.next_worker_id:04d}"
196
  worker_name = f"Worker {self.next_worker_id}"
197
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
198
- self.save_local_worker_data()
199
- log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
200
- self.session_log.append(log_msg)
201
- logger.info(log_msg)
202
  return worker_id, worker_name
203
  except Exception as e:
204
- logger.error(f"❌ Auto-registration error: {e}")
205
  return None
206
 
207
  def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
@@ -209,358 +237,416 @@ class AttendanceSystem:
209
  self.known_face_names.append(name)
210
  self.known_face_ids.append(worker_id)
211
  self.next_worker_id += 1
 
 
212
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
213
  face_pil.save(f"data/faces/{worker_id}.jpg")
214
- caption = self._get_image_caption(face_pil)
215
- if self.sf:
216
- try:
217
- worker_record = self.sf.Worker__c.create({'Name': name, 'Worker_ID__c': worker_id, 'Face_Embedding__c': json.dumps(embedding), 'Image_Caption__c': caption})
218
- image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
219
- if image_url: self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
220
- logger.info(f"βœ… Worker {worker_id} synced to Salesforce.")
221
- except Exception as e:
222
- logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
  def _is_duplicate_face(self, embedding: List[float], threshold: float = 10.0) -> bool:
225
- if not self.known_face_embeddings: return False
226
- distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
 
 
 
 
 
 
227
  return min(distances) < threshold
228
 
229
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
230
  today_str = date.today().isoformat()
231
 
232
- # Check if already marked today
233
  if worker_id in self.today_attendance:
234
  return False
235
 
236
- # Check cooldown period
237
- current_time = time.time()
238
- if worker_id in self.last_recognition_time and (current_time - self.last_recognition_time[worker_id] < self.recognition_cooldown):
239
- return False
240
-
241
- # Mark attendance
242
- current_time_dt = datetime.now()
243
- if self.sf:
244
- try:
245
- self.sf.Attendance__c.create({
246
- 'Worker_ID__c': worker_id,
247
- 'Name__c': worker_name,
248
- 'Date__c': today_str,
249
- 'Timestamp__c': current_time_dt.isoformat(),
250
- 'Status__c': "Present"
251
- })
252
- except Exception as e:
253
- logger.error(f"❌ Error saving attendance to Salesforce: {e}")
254
-
255
  self.today_attendance[worker_id] = True
256
- self.last_recognition_time[worker_id] = current_time
257
- log_msg = f"βœ… [{current_time_dt.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
258
- self.session_log.append(log_msg)
 
 
 
 
 
 
 
 
259
  return True
260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  # --- Video Processing ---
262
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
263
- """
264
- Process a single video frame with optimizations for speed and accuracy.
265
- """
266
  try:
267
- # Skip frames for faster processing
268
- self.frame_counter += 1
269
- if self.frame_counter % (self.frame_skip + 1) != 0:
 
270
  return frame
271
-
272
- # Resize frame for faster processing (keeping aspect ratio)
 
 
273
  height, width = frame.shape[:2]
274
- new_width = 640
275
- new_height = int((new_width / width) * height)
276
- small_frame = cv2.resize(frame, (new_width, new_height))
277
-
278
  face_objs = DeepFace.extract_faces(
279
- img_path=small_frame,
280
- detector_backend='opencv',
281
  enforce_detection=False,
282
- align=True
283
  )
284
 
285
  for face_obj in face_objs:
286
- confidence = face_obj['confidence']
287
- if confidence < 0.95: # Higher confidence threshold
288
  continue
289
-
 
290
  facial_area = face_obj['facial_area']
291
- x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
292
-
293
- # Scale coordinates back to original frame size
294
- x = int(x * width / new_width)
295
- y = int(y * height / new_height)
296
- w = int(w * width / new_width)
297
- h = int(h * height / new_height)
298
 
 
299
  face_image = frame[y:y+h, x:x+w]
300
- if face_image.size == 0:
301
- continue
302
-
303
- # Only process faces that haven't been detected recently
304
- current_time = time.time()
305
- face_key = f"{x}_{y}_{w}_{h}"
306
- if face_key in self.last_detected_faces and (current_time - self.last_detected_faces[face_key] < 2.0):
307
  continue
308
- self.last_detected_faces[face_key] = current_time
309
-
310
- embedding = DeepFace.represent(
311
- img_path=face_image,
312
- model_name='Facenet',
313
- enforce_detection=False,
314
- align=True
315
- )[0]['embedding']
316
 
317
- if not self.known_face_embeddings:
318
- continue
319
-
320
- distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
321
- min_dist = min(distances) if distances else float('inf')
322
- match_index = distances.index(min_dist) if min_dist < 10.0 else -1
323
-
324
- color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
325
-
326
- if match_index != -1:
327
- worker_id = self.known_face_ids[match_index]
328
- worker_name = self.known_face_names[match_index]
329
- color = (0, 255, 0) # Green for known workers
330
- self.mark_attendance(worker_id, worker_name)
331
- else:
332
- # Only attempt auto-registration for high-quality faces
333
- analysis = DeepFace.analyze(img_path=face_image, actions=['emotion'], enforce_detection=False)
334
- if analysis[0]['face_confidence'] >= 0.95:
335
- color = (0, 165, 255) # Orange for potential new worker
336
- new_worker = self._register_worker_auto(face_image)
337
- if new_worker:
338
- worker_id, worker_name = new_worker[0], new_worker[1]
339
- self.mark_attendance(worker_id, worker_name)
340
 
341
- # Draw rectangle and label
342
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
343
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
344
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
345
 
346
  return frame
347
  except Exception as e:
348
- logger.error(f"ERROR in process_frame: {e}")
349
  return frame
350
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  def _processing_loop(self, source):
352
- video_capture = cv2.VideoCapture(source)
353
- if not video_capture.isOpened():
354
- err_msg = f"❌ **Error:** Could not open video source. The file may be corrupt or in an unsupported format. Please try converting it to a standard MP4."
355
- self.error_message = err_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  self.is_processing.clear()
357
- return
358
-
359
- # Set higher FPS if possible (for live camera)
360
- if isinstance(source, int):
361
- video_capture.set(cv2.CAP_PROP_FPS, 30)
362
-
363
- while self.is_processing.is_set():
364
- ret, frame = video_capture.read()
365
- if not ret:
366
- break
367
-
368
- processed_frame = self.process_frame(frame)
369
-
370
- if not self.frame_queue.full():
371
- self.frame_queue.put(processed_frame)
372
-
373
- self.last_processed_frame = processed_frame
374
- time.sleep(0.02) # Reduced sleep for faster processing
375
-
376
- self.final_log = self.session_log.copy()
377
- video_capture.release()
378
- self.is_processing.clear()
379
 
380
  def start_processing(self, source) -> str:
381
- if self.is_processing.is_set():
 
382
  return "⚠️ Processing is already active."
383
 
384
- # Reset states for new session
385
  self.session_log.clear()
386
- self.last_recognition_time.clear()
387
  self.today_attendance.clear()
388
- self.last_detected_faces.clear()
389
  self.error_message = None
390
  self.last_processed_frame = None
391
  self.final_log = None
392
- self.frame_counter = 0
393
 
394
  self.is_processing.set()
395
  self.processing_thread = threading.Thread(
396
- target=self._processing_loop,
397
  args=(source,),
398
  daemon=True
399
  )
400
  self.processing_thread.start()
401
- return f"βœ… Started processing..."
402
 
403
  def stop_processing(self) -> str:
 
404
  self.is_processing.clear()
405
- self.error_message = None
406
- self.last_processed_frame = None
407
- self.final_log = None
408
- return "βœ… Processing stopped by user."
409
 
410
- # --- Helper & Reporting ---
411
  def _get_image_caption(self, image: Image.Image) -> str:
412
- if not HF_API_TOKEN:
413
- return "Hugging Face API token not configured."
 
 
414
  try:
415
  buffered = BytesIO()
416
  image.save(buffered, format="JPEG")
417
- img_data = buffered.getvalue()
418
- headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
419
- response = requests.post(HF_API_URL, headers=headers, data=img_data)
 
 
 
420
  response.raise_for_status()
421
- result = response.json()
422
- return result[0].get("generated_text", "No caption found.")
423
  except Exception as e:
424
- logger.error(f"Hugging Face API error: {e}")
425
- return "Caption generation failed."
426
 
427
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
428
- if not self.sf:
 
429
  return None
 
430
  try:
431
  buffered = BytesIO()
432
  image.save(buffered, format="JPEG")
433
- encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
434
  cv = self.sf.ContentVersion.create({
435
  'Title': f'Image_{worker_id}',
436
  'PathOnClient': f'{worker_id}.jpg',
437
- 'VersionData': encoded_image,
438
  'FirstPublishLocationId': record_id
439
- })
440
- return f"/{cv['id']}" # Relative URL
441
  except Exception as e:
442
- logger.error(f"Salesforce image upload error: {e}")
443
  return None
444
 
445
  def get_registered_workers_info(self) -> str:
446
- if not self.sf:
447
- return "❌ Salesforce not connected."
448
- try:
449
- records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
450
- if not records:
451
- return "No workers registered."
452
- return f"**πŸ‘₯ Registered Workers ({len(records)})**\n" + "\n".join(
453
- [f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records]
454
- )
455
- except Exception as e:
456
- return f"Error: {e}"
457
-
458
  # --- GRADIO UI ---
459
  attendance_system = AttendanceSystem()
460
 
461
  def create_interface():
462
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
463
- gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
 
464
  with gr.Tabs():
465
- with gr.Tab("βš™οΈ Controls & Status"):
466
- gr.Markdown("### 1. Choose Input Source & Start Processing")
467
- with gr.Row():
468
- with gr.Column(scale=1):
469
- selected_tab_index = gr.Number(value=0, visible=False)
470
- with gr.Tabs() as video_tabs:
471
- with gr.Tab("Live Camera", id=0):
472
- camera_source = gr.Number(label="Camera Source", value=0, precision=0)
473
- with gr.Tab("Upload Video", id=1):
474
- video_file = gr.Video(label="Upload Video File", sources=["upload"])
475
- with gr.Column(scale=1):
476
- start_btn = gr.Button("▢️ Start Processing", variant="primary")
477
- stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
478
- status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
479
- gr.Markdown("### 2. View Results in the 'Output & Log' Tab")
480
- gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
481
-
482
- with gr.Tab("πŸ“Š Output & Log"):
483
  with gr.Row():
484
  with gr.Column(scale=2):
485
- video_output = gr.Image(label="Recognition Output", interactive=False)
486
  with gr.Column(scale=1):
487
- session_log_display = gr.Markdown(label="πŸ“‹ Session Log", value="System is ready.")
488
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  with gr.Tab("πŸ‘€ Worker Management"):
490
  with gr.Row():
491
  with gr.Column():
492
- register_image = gr.Image(label="Upload Worker's Photo", type="pil")
493
- register_name = gr.Textbox(label="Worker's Full Name")
494
- register_btn = gr.Button("Register Worker", variant="primary")
495
- register_output = gr.Textbox(label="Registration Status", interactive=False)
 
 
 
 
 
496
  with gr.Column():
497
- registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
498
- refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
499
-
500
- # --- Event Handlers ---
501
- def on_tab_select(evt: gr.SelectData):
502
- return evt.index
503
-
504
- video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
 
 
505
 
506
- def start_wrapper(tab_index, cam_src, vid_path):
507
- source = cam_src if tab_index == 0 else vid_path
508
- return "Please provide an input source." if source is None else attendance_system.start_processing(source)
509
-
510
  start_btn.click(
511
- fn=start_wrapper,
512
- inputs=[selected_tab_index, camera_source, video_file],
513
- outputs=[status_box]
514
  )
515
 
516
  stop_btn.click(
517
- fn=attendance_system.stop_processing,
518
- inputs=None,
519
- outputs=[status_box]
520
  )
521
 
522
  register_btn.click(
523
- fn=attendance_system.register_worker_manual,
524
- inputs=[register_image, register_name],
525
- outputs=[register_output, registered_workers_info]
526
  )
527
 
528
- refresh_workers_btn.click(
529
- fn=attendance_system.get_registered_workers_info,
530
- outputs=[registered_workers_info]
531
  )
532
 
533
- def update_ui_generator():
534
  while True:
535
- if attendance_system.error_message:
536
- yield None, attendance_system.error_message
537
- time.sleep(2)
538
- attendance_system.error_message = None
539
- continue
540
-
 
 
 
541
  if attendance_system.is_processing.is_set():
542
- frame, log_md = None, "\n".join(reversed(attendance_system.session_log[-20:])) or "Processing..."
543
- try:
544
- if not attendance_system.frame_queue.empty():
545
- frame = attendance_system.frame_queue.get_nowait()
546
- if frame is not None:
547
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
548
- except queue.Empty:
549
- pass
550
- yield frame, log_md
551
- else:
552
- if attendance_system.last_processed_frame is not None:
553
- final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
554
- final_log_md = "\n".join(reversed(attendance_system.final_log[-20:])) or "Processing complete. No log entries."
555
- yield final_frame, final_log_md
556
- else:
557
- yield None, "System stopped. Go to 'Controls & Status' to start."
558
- time.sleep(0.05) # Faster UI updates
559
-
560
- demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
561
  return demo
562
 
563
  if __name__ == "__main__":
564
  app = create_interface()
565
  app.queue()
566
- app.launch(server_name="0.0.0.0", server_port=7860, show_error=True, debug=True)
 
 
 
 
 
18
  import cv2
19
  import gradio as gr
20
  import numpy as np
 
21
  from PIL import Image
22
  import requests
23
  from dotenv import load_dotenv
 
46
  "domain": os.getenv("SF_DOMAIN", "login")
47
  }
48
 
49
+ # Performance settings
50
+ PROCESSING_WIDTH = 640 # Width for resizing frames
51
+ FACE_DETECTION_INTERVAL = 0.1 # Seconds between face detections
52
+ MIN_FACE_CONFIDENCE = 0.90 # Minimum confidence for face detection
53
+ RECOGNITION_COOLDOWN = 5 # Seconds between recognizing same person
54
+
55
  # --- SALESFORCE CONNECTION ---
56
 
57
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
 
64
  return sf
65
  except Exception as e:
66
  logger.error(f"❌ Salesforce connection failed: {e}")
67
+ return None
68
 
69
  # --- CORE LOGIC ---
70
 
71
  class AttendanceSystem:
72
  """
73
+ Optimized face recognition attendance system with real-time performance.
74
  """
75
  def __init__(self):
76
  # State Management
77
  self.processing_thread = None
78
  self.is_processing = threading.Event()
79
+ self.frame_queue = queue.Queue(maxsize=5) # Smaller queue for faster updates
80
+ self.error_message = None
81
+ self.last_processed_frame = None
82
+ self.final_log = None
83
 
84
  # Data Storage
85
  self.known_face_embeddings: List[np.ndarray] = []
 
88
  self.next_worker_id: int = 1
89
 
90
  # Session Tracking
91
+ self.today_attendance: Dict[str, bool] = {}
92
+ self.last_detection_time = 0
93
  self.session_log: List[str] = []
94
+ self.last_log_update = 0
 
 
 
 
 
95
 
96
  # Initialize
97
  self.sf = connect_to_salesforce()
 
102
  os.makedirs("data/faces", exist_ok=True)
103
 
104
  def load_worker_data(self):
105
+ """Load worker data with timeout protection."""
106
+ try:
107
+ if self.sf:
108
  workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
109
+ if workers:
110
+ temp_embeddings, temp_names, temp_ids = [], [], []
111
+ max_id = 0
112
+ for worker in workers:
113
+ if worker.get('Face_Embedding__c'):
114
+ temp_embeddings.append(np.array(json.loads(worker['Face_Embedding__c'])))
115
+ temp_names.append(worker['Name'])
116
+ temp_ids.append(worker['Worker_ID__c'])
117
+ try:
118
+ worker_num = int(worker['Worker_ID__c'][1:])
119
+ max_id = max(max_id, worker_num)
120
+ except (ValueError, TypeError):
121
+ continue
122
+
123
+ self.known_face_embeddings = temp_embeddings
124
+ self.known_face_names = temp_names
125
+ self.known_face_ids = temp_ids
126
+ self.next_worker_id = max_id + 1
127
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from Salesforce.")
128
  return
129
+
130
+ # Fallback to local data
131
+ self._load_local_worker_data()
132
+ except Exception as e:
133
+ logger.error(f"Error loading worker data: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  self._load_local_worker_data()
135
 
136
  def _load_local_worker_data(self):
137
  try:
138
  if os.path.exists("data/workers.pkl"):
139
+ with open("data/workers.pkl", "rb") as f:
140
+ data = pickle.load(f)
141
  self.known_face_embeddings = data.get("embeddings", [])
142
  self.known_face_names = data.get("names", [])
143
  self.known_face_ids = data.get("ids", [])
144
  self.next_worker_id = data.get("next_id", 1)
145
  logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from local cache.")
146
  except Exception as e:
147
+ logger.error(f"Error loading local data: {e}")
148
 
149
  def save_local_worker_data(self):
150
  try:
151
+ worker_data = {
152
+ "embeddings": self.known_face_embeddings,
153
+ "names": self.known_face_names,
154
+ "ids": self.known_face_ids,
155
+ "next_id": self.next_worker_id
156
+ }
157
+ with open("data/workers.pkl", "wb") as f:
158
+ pickle.dump(worker_data, f)
159
  except Exception as e:
160
+ logger.error(f"Error saving local worker data: {e}")
161
 
162
  # --- Registration and Attendance ---
163
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
164
  if image is None or not name.strip():
165
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
166
+
167
  try:
168
  image_array = np.array(image)
 
 
 
 
169
 
170
+ # Quick face detection check
171
+ try:
172
+ face_objs = DeepFace.extract_faces(
173
+ img_path=image_array,
174
+ detector_backend='opencv',
175
+ enforce_detection=True,
176
+ align=False # Skip alignment for faster check
177
+ )
178
+ if not face_objs or face_objs[0]['confidence'] < MIN_FACE_CONFIDENCE:
179
+ return "❌ No clear face detected in the image!", self.get_registered_workers_info()
180
+ except:
181
+ return "❌ No face detected in the image!", self.get_registered_workers_info()
182
+
183
+ # Full processing with alignment
184
+ embedding = DeepFace.represent(
185
+ img_path=image_array,
186
+ model_name='Facenet',
187
+ enforce_detection=True,
188
+ align=True
189
+ )[0]['embedding']
190
+
191
  if self._is_duplicate_face(embedding):
192
+ return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
193
 
194
  worker_id = f"W{self.next_worker_id:04d}"
195
  name = name.strip().title()
196
  self._add_worker_to_system(worker_id, name, embedding, image_array)
 
 
197
  return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
 
 
198
  except Exception as e:
199
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
200
 
201
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
202
  try:
203
+ # Quick quality check first
204
+ face_objs = DeepFace.extract_faces(
205
+ img_path=face_image,
206
+ detector_backend='opencv',
207
+ enforce_detection=False,
208
+ align=False
209
+ )
210
+ if not face_objs or face_objs[0]['confidence'] < MIN_FACE_CONFIDENCE:
211
  return None
212
+
213
+ # Full processing if quality is good
214
+ embedding = DeepFace.represent(
215
+ img_path=face_image,
216
+ model_name='Facenet',
217
+ enforce_detection=False,
218
+ align=True
219
+ )[0]['embedding']
220
+
221
+ if self._is_duplicate_face(embedding):
222
  return None
223
+
224
  worker_id = f"W{self.next_worker_id:04d}"
225
  worker_name = f"Worker {self.next_worker_id}"
226
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
227
+
228
+ log_msg = f"πŸ†• Auto-registered: {worker_name} ({worker_id})"
229
+ self._add_to_session_log(log_msg)
 
230
  return worker_id, worker_name
231
  except Exception as e:
232
+ logger.error(f"Auto-registration error: {e}")
233
  return None
234
 
235
  def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
 
237
  self.known_face_names.append(name)
238
  self.known_face_ids.append(worker_id)
239
  self.next_worker_id += 1
240
+
241
+ # Save face image
242
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
243
  face_pil.save(f"data/faces/{worker_id}.jpg")
244
+
245
+ # Async Salesforce sync
246
+ threading.Thread(
247
+ target=self._sync_worker_to_salesforce,
248
+ args=(worker_id, name, embedding, face_pil),
249
+ daemon=True
250
+ ).start()
251
+
252
+ def _sync_worker_to_salesforce(self, worker_id: str, name: str, embedding: List[float], image: Image.Image):
253
+ if not self.sf:
254
+ return
255
+
256
+ try:
257
+ # Generate caption (async)
258
+ caption = self._get_image_caption(image)
259
+
260
+ # Create worker record
261
+ worker_record = self.sf.Worker__c.create({
262
+ 'Name': name,
263
+ 'Worker_ID__c': worker_id,
264
+ 'Face_Embedding__c': json.dumps(embedding),
265
+ 'Image_Caption__c': caption
266
+ })
267
+
268
+ # Upload image
269
+ image_url = self._upload_image_to_salesforce(image, worker_record['id'], worker_id)
270
+ if image_url:
271
+ self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
272
+
273
+ logger.info(f"Worker {worker_id} synced to Salesforce.")
274
+ self.save_local_worker_data()
275
+ except Exception as e:
276
+ logger.error(f"Salesforce sync error for {worker_id}: {e}")
277
 
278
  def _is_duplicate_face(self, embedding: List[float], threshold: float = 10.0) -> bool:
279
+ if not self.known_face_embeddings:
280
+ return False
281
+
282
+ # Convert to numpy array once
283
+ new_embedding = np.array(embedding)
284
+
285
+ # Calculate distances efficiently
286
+ distances = [np.linalg.norm(new_embedding - known) for known in self.known_face_embeddings]
287
  return min(distances) < threshold
288
 
289
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
290
  today_str = date.today().isoformat()
291
 
292
+ # Already marked today?
293
  if worker_id in self.today_attendance:
294
  return False
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  self.today_attendance[worker_id] = True
297
+ current_time = datetime.now()
298
+
299
+ # Async Salesforce sync
300
+ threading.Thread(
301
+ target=self._sync_attendance_to_salesforce,
302
+ args=(worker_id, worker_name, today_str, current_time),
303
+ daemon=True
304
+ ).start()
305
+
306
+ log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Present: {worker_name} ({worker_id})"
307
+ self._add_to_session_log(log_msg)
308
  return True
309
 
310
+ def _sync_attendance_to_salesforce(self, worker_id: str, worker_name: str, date_str: str, timestamp: datetime):
311
+ if not self.sf:
312
+ return
313
+
314
+ try:
315
+ self.sf.Attendance__c.create({
316
+ 'Worker_ID__c': worker_id,
317
+ 'Name__c': worker_name,
318
+ 'Date__c': date_str,
319
+ 'Timestamp__c': timestamp.isoformat(),
320
+ 'Status__c': "Present"
321
+ })
322
+ except Exception as e:
323
+ logger.error(f"Error saving attendance to Salesforce: {e}")
324
+
325
+ def _add_to_session_log(self, message: str):
326
+ """Add message to session log with rate limiting."""
327
+ now = time.time()
328
+ if now - self.last_log_update > 0.5: # Throttle log updates
329
+ self.session_log.append(message)
330
+ self.last_log_update = now
331
+ if len(self.session_log) > 20: # Keep log manageable
332
+ self.session_log.pop(0)
333
+
334
  # --- Video Processing ---
335
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
336
+ """Process a frame with face detection and recognition."""
 
 
337
  try:
338
+ now = time.time()
339
+
340
+ # Throttle face detection
341
+ if now - self.last_detection_time < FACE_DETECTION_INTERVAL:
342
  return frame
343
+
344
+ self.last_detection_time = now
345
+
346
+ # Resize for faster processing
347
  height, width = frame.shape[:2]
348
+ scale = PROCESSING_WIDTH / width
349
+ small_frame = cv2.resize(frame, (PROCESSING_WIDTH, int(height * scale)))
350
+
351
+ # Detect faces with fast backend
352
  face_objs = DeepFace.extract_faces(
353
+ img_path=small_frame,
354
+ detector_backend='opencv', # Fastest backend
355
  enforce_detection=False,
356
+ align=False # Skip alignment for detection
357
  )
358
 
359
  for face_obj in face_objs:
360
+ if face_obj['confidence'] < MIN_FACE_CONFIDENCE:
 
361
  continue
362
+
363
+ # Get face coordinates and scale back to original
364
  facial_area = face_obj['facial_area']
365
+ x = int(facial_area['x'] / scale)
366
+ y = int(facial_area['y'] / scale)
367
+ w = int(facial_area['w'] / scale)
368
+ h = int(facial_area['h'] / scale)
 
 
 
369
 
370
+ # Extract face region
371
  face_image = frame[y:y+h, x:x+w]
372
+ if face_image.size == 0:
 
 
 
 
 
 
373
  continue
 
 
 
 
 
 
 
 
374
 
375
+ # Recognize or register
376
+ worker_id, worker_name, color = self._process_face(face_image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
 
378
+ # Draw results
379
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
380
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
381
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
382
 
383
  return frame
384
  except Exception as e:
385
+ logger.error(f"Frame processing error: {e}")
386
  return frame
387
 
388
+ def _process_face(self, face_image: np.ndarray) -> Tuple[Optional[str], str, Tuple[int, int, int]]:
389
+ """Process a single face image and return (id, name, color)."""
390
+ try:
391
+ # Get embedding with alignment
392
+ embedding = DeepFace.represent(
393
+ img_path=face_image,
394
+ model_name='Facenet',
395
+ enforce_detection=False,
396
+ align=True
397
+ )[0]['embedding']
398
+
399
+ # Find closest match
400
+ if self.known_face_embeddings:
401
+ distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
402
+ min_dist = min(distances)
403
+ match_index = distances.index(min_dist) if min_dist < 10.0 else -1
404
+ else:
405
+ match_index = -1
406
+
407
+ # Known worker
408
+ if match_index != -1:
409
+ worker_id = self.known_face_ids[match_index]
410
+ worker_name = self.known_face_names[match_index]
411
+ self.mark_attendance(worker_id, worker_name)
412
+ return worker_id, worker_name, (0, 255, 0) # Green
413
+
414
+ # New worker - check quality before registering
415
+ face_objs = DeepFace.extract_faces(
416
+ img_path=face_image,
417
+ detector_backend='opencv',
418
+ enforce_detection=False,
419
+ align=False
420
+ )
421
+ if face_objs and face_objs[0]['confidence'] >= MIN_FACE_CONFIDENCE:
422
+ new_worker = self._register_worker_auto(face_image)
423
+ if new_worker:
424
+ worker_id, worker_name = new_worker
425
+ self.mark_attendance(worker_id, worker_name)
426
+ return worker_id, worker_name, (0, 165, 255) # Orange
427
+
428
+ return None, "Unknown", (0, 0, 255) # Red
429
+ except Exception as e:
430
+ logger.error(f"Face processing error: {e}")
431
+ return None, "Error", (0, 0, 255)
432
+
433
  def _processing_loop(self, source):
434
+ """Main processing loop optimized for performance."""
435
+ try:
436
+ video_capture = cv2.VideoCapture(source)
437
+ if not video_capture.isOpened():
438
+ self.error_message = "❌ Could not open video source."
439
+ self.is_processing.clear()
440
+ return
441
+
442
+ # Set higher FPS if possible
443
+ if isinstance(source, int): # Camera
444
+ video_capture.set(cv2.CAP_PROP_FPS, 30)
445
+ video_capture.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce latency
446
+
447
+ while self.is_processing.is_set():
448
+ ret, frame = video_capture.read()
449
+ if not ret:
450
+ break
451
+
452
+ # Process frame and add to queue
453
+ processed_frame = self.process_frame(frame)
454
+ if not self.frame_queue.full():
455
+ self.frame_queue.put(processed_frame)
456
+ self.last_processed_frame = processed_frame
457
+
458
+ self.final_log = self.session_log.copy()
459
+ except Exception as e:
460
+ logger.error(f"Processing loop error: {e}")
461
+ self.error_message = f"❌ Processing error: {str(e)}"
462
+ finally:
463
+ if 'video_capture' in locals():
464
+ video_capture.release()
465
  self.is_processing.clear()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  def start_processing(self, source) -> str:
468
+ """Start processing with clean state."""
469
+ if self.is_processing.is_set():
470
  return "⚠️ Processing is already active."
471
 
 
472
  self.session_log.clear()
 
473
  self.today_attendance.clear()
 
474
  self.error_message = None
475
  self.last_processed_frame = None
476
  self.final_log = None
477
+ self.last_detection_time = 0
478
 
479
  self.is_processing.set()
480
  self.processing_thread = threading.Thread(
481
+ target=self._processing_loop,
482
  args=(source,),
483
  daemon=True
484
  )
485
  self.processing_thread.start()
486
+ return "βœ… Started processing..."
487
 
488
  def stop_processing(self) -> str:
489
+ """Stop processing gracefully."""
490
  self.is_processing.clear()
491
+ return "βœ… Processing stopped."
 
 
 
492
 
493
+ # --- Helper Methods ---
494
  def _get_image_caption(self, image: Image.Image) -> str:
495
+ """Get image caption from Hugging Face (with timeout)."""
496
+ if not HF_API_TOKEN:
497
+ return ""
498
+
499
  try:
500
  buffered = BytesIO()
501
  image.save(buffered, format="JPEG")
502
+ response = requests.post(
503
+ HF_API_URL,
504
+ headers={"Authorization": f"Bearer {HF_API_TOKEN}"},
505
+ data=buffered.getvalue(),
506
+ timeout=5 # Short timeout
507
+ )
508
  response.raise_for_status()
509
+ return response.json()[0].get("generated_text", "")
 
510
  except Exception as e:
511
+ logger.error(f"Caption error: {e}")
512
+ return ""
513
 
514
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
515
+ """Upload image to Salesforce with timeout."""
516
+ if not self.sf:
517
  return None
518
+
519
  try:
520
  buffered = BytesIO()
521
  image.save(buffered, format="JPEG")
 
522
  cv = self.sf.ContentVersion.create({
523
  'Title': f'Image_{worker_id}',
524
  'PathOnClient': f'{worker_id}.jpg',
525
+ 'VersionData': base64.b64encode(buffered.getvalue()).decode('utf-8'),
526
  'FirstPublishLocationId': record_id
527
+ }, timeout=5) # Short timeout
528
+ return f"/{cv['id']}"
529
  except Exception as e:
530
+ logger.error(f"Image upload error: {e}")
531
  return None
532
 
533
  def get_registered_workers_info(self) -> str:
534
+ """Get formatted worker list."""
535
+ if not self.known_face_ids:
536
+ return "No workers registered."
537
+ return "**πŸ‘₯ Registered Workers**\n" + "\n".join(
538
+ f"- {name} (ID: {id})" for name, id in zip(self.known_face_names, self.known_face_ids)
539
+ )
540
+
 
 
 
 
 
541
  # --- GRADIO UI ---
542
  attendance_system = AttendanceSystem()
543
 
544
  def create_interface():
545
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
546
+ gr.Markdown("# πŸš€ Fast Face Recognition Attendance")
547
+
548
  with gr.Tabs():
549
+ with gr.Tab("πŸŽ₯ Live Recognition"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  with gr.Row():
551
  with gr.Column(scale=2):
552
+ video_output = gr.Image(label="Live Feed", streaming=True)
553
  with gr.Column(scale=1):
554
+ with gr.Row():
555
+ source_select = gr.Radio(
556
+ ["Webcam", "Video File"],
557
+ label="Source",
558
+ value="Webcam"
559
+ )
560
+ with gr.Row():
561
+ start_btn = gr.Button("▢️ Start", variant="primary")
562
+ stop_btn = gr.Button("⏹️ Stop")
563
+ status_box = gr.Textbox(label="Status", value="Ready")
564
+ session_log = gr.Textbox(
565
+ label="Session Log",
566
+ lines=10,
567
+ max_lines=20,
568
+ interactive=False
569
+ )
570
+
571
  with gr.Tab("πŸ‘€ Worker Management"):
572
  with gr.Row():
573
  with gr.Column():
574
+ gr.Markdown("### Register New Worker")
575
+ register_image = gr.Image(
576
+ label="Worker Photo",
577
+ type="pil",
578
+ sources=["webcam", "upload"]
579
+ )
580
+ register_name = gr.Textbox(label="Full Name")
581
+ register_btn = gr.Button("Register", variant="primary")
582
+ register_output = gr.Textbox(label="Status", interactive=False)
583
  with gr.Column():
584
+ gr.Markdown("### Registered Workers")
585
+ workers_info = gr.Markdown(attendance_system.get_registered_workers_info())
586
+ refresh_btn = gr.Button("πŸ”„ Refresh")
587
+
588
+ # Event handlers
589
+ source_select.change(
590
+ lambda x: gr.update(visible=x == "Webcam"),
591
+ inputs=source_select,
592
+ outputs=video_output
593
+ )
594
 
 
 
 
 
595
  start_btn.click(
596
+ lambda: attendance_system.start_processing(0 if source_select.value == "Webcam" else video_file.value),
597
+ outputs=status_box
 
598
  )
599
 
600
  stop_btn.click(
601
+ attendance_system.stop_processing,
602
+ outputs=status_box
 
603
  )
604
 
605
  register_btn.click(
606
+ attendance_system.register_worker_manual,
607
+ inputs=[register_image, register_name],
608
+ outputs=[register_output, workers_info]
609
  )
610
 
611
+ refresh_btn.click(
612
+ lambda: attendance_system.get_registered_workers_info(),
613
+ outputs=workers_info
614
  )
615
 
616
+ def update_ui():
617
  while True:
618
+ # Get latest frame
619
+ frame = None
620
+ if not attendance_system.frame_queue.empty():
621
+ frame = attendance_system.frame_queue.get_nowait()
622
+ if frame is not None:
623
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
624
+
625
+ # Get latest status
626
+ status = "Ready"
627
  if attendance_system.is_processing.is_set():
628
+ status = "Processing..."
629
+ elif attendance_system.error_message:
630
+ status = attendance_system.error_message
631
+
632
+ # Get latest log
633
+ log_text = "\n".join(reversed(attendance_system.session_log[-10:])) or "No activity yet"
634
+
635
+ yield frame, status, log_text
636
+ time.sleep(0.033) # ~30 FPS update rate
637
+
638
+ demo.load(
639
+ update_ui,
640
+ outputs=[video_output, status_box, session_log]
641
+ )
642
+
 
 
 
 
643
  return demo
644
 
645
  if __name__ == "__main__":
646
  app = create_interface()
647
  app.queue()
648
+ app.launch(
649
+ server_name="0.0.0.0",
650
+ server_port=7860,
651
+ show_error=True
652
+ )