PrashanthB461 commited on
Commit
9d779f6
Β·
verified Β·
1 Parent(s): 9a9a3e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +202 -134
app.py CHANGED
@@ -73,8 +73,8 @@ class AttendanceSystem:
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] = []
@@ -82,15 +82,15 @@ class AttendanceSystem:
82
  self.known_face_ids: List[str] = []
83
  self.next_worker_id: int = 1
84
 
85
- # Session Tracking - Enhanced for better accuracy
86
  self.last_recognition_time = {}
87
- self.recognition_cooldown = 10 # Cooldown to prevent duplicates
88
  self.session_log: List[str] = []
89
- self.session_marked_present = set() # Track who's already marked present in this session
90
- self.session_registered = set() # Track who's already auto-registered in this session
91
- self.face_recognition_buffer = {} # Buffer for multiple detections before confirming
92
- self.buffer_threshold = 3 # Increased threshold for more reliable recognition
93
- self.frame_skip_counter = 0 # Skip frames for better performance
94
 
95
  # Initialize
96
  self.sf = connect_to_salesforce()
@@ -138,7 +138,8 @@ class AttendanceSystem:
138
  def _load_local_worker_data(self):
139
  try:
140
  if os.path.exists("data/workers.pkl"):
141
- with open("data/workers.pkl", "rb") as f: data = pickle.load(f)
 
142
  self.known_face_embeddings = data.get("embeddings", [])
143
  self.known_face_names = data.get("names", [])
144
  self.known_face_ids = data.get("ids", [])
@@ -149,8 +150,14 @@ class AttendanceSystem:
149
 
150
  def save_local_worker_data(self):
151
  try:
152
- worker_data = {"embeddings": self.known_face_embeddings, "names": self.known_face_names, "ids": self.known_face_ids, "next_id": self.next_worker_id}
153
- with open("data/workers.pkl", "wb") as f: pickle.dump(worker_data, f)
 
 
 
 
 
 
154
  except Exception as e:
155
  logger.error(f"❌ Error saving local worker data: {e}")
156
 
@@ -178,7 +185,7 @@ class AttendanceSystem:
178
 
179
  def _register_worker_auto(self, face_image: np.ndarray, face_embedding: List[float]) -> Optional[Tuple[str, str]]:
180
  try:
181
- # Check for duplicates with strict threshold for auto-registration
182
  if self._is_duplicate_face(face_embedding, threshold=10.0):
183
  return None
184
 
@@ -213,9 +220,15 @@ class AttendanceSystem:
213
  caption = self._get_image_caption(face_pil)
214
  if self.sf:
215
  try:
216
- worker_record = self.sf.Worker__c.create({'Name': name, 'Worker_ID__c': worker_id, 'Face_Embedding__c': json.dumps(embedding), 'Image_Caption__c': caption})
 
 
 
 
 
217
  image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
218
- if image_url: self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
 
219
  logger.info(f"βœ… Worker {worker_id} synced to Salesforce.")
220
  except Exception as e:
221
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
@@ -227,18 +240,20 @@ class AttendanceSystem:
227
 
228
  embedding_array = np.array(embedding)
229
  for known_embedding in self.known_face_embeddings:
230
- # Use cosine similarity for more accurate face matching
231
- cosine_sim = np.dot(embedding_array, known_embedding) / (np.linalg.norm(embedding_array) * np.linalg.norm(known_embedding))
 
 
 
 
232
 
233
- # Consider it duplicate only if similarity is very high
234
- if cosine_sim > 0.85: # Increased from 0.80 to be more strict
235
  return True
236
 
237
  return False
238
 
239
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
240
- """Strict attendance marking with session tracking"""
241
- # Check if already marked present in this session
242
  if worker_id in self.session_marked_present:
243
  return False
244
 
@@ -259,7 +274,6 @@ class AttendanceSystem:
259
  except Exception as e:
260
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
261
 
262
- # Mark as present in this session
263
  self.session_marked_present.add(worker_id)
264
 
265
  log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
@@ -272,25 +286,26 @@ class AttendanceSystem:
272
  return True
273
  if self.sf:
274
  try:
275
- if self.sf.query(f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = '{today_str}'")['totalSize'] > 0:
 
276
  return True
277
- except Exception:
278
- pass
279
  return False
280
 
281
  def _find_best_match(self, target_embedding: np.ndarray) -> Tuple[int, float]:
282
- """Find best match using cosine similarity with strict threshold"""
283
  if not self.known_face_embeddings:
284
- return -1, float('inf')
285
 
286
  best_match_idx = -1
287
- best_score = 0.0 # Using similarity score (higher is better)
 
 
288
 
289
  for i, known_embedding in enumerate(self.known_face_embeddings):
290
- # Cosine similarity
291
- cosine_sim = np.dot(target_embedding, known_embedding) / (
292
- np.linalg.norm(target_embedding) * np.linalg.norm(known_embedding)
293
- )
294
 
295
  if cosine_sim > best_score:
296
  best_score = cosine_sim
@@ -300,215 +315,237 @@ class AttendanceSystem:
300
 
301
  # --- Video Processing ---
302
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
303
- """
304
- Strict frame processing with high confidence requirements
305
- """
306
  try:
307
- # Skip frames for better performance
308
  self.frame_skip_counter += 1
309
- if self.frame_skip_counter % 3 != 0: # Process every 3rd frame
310
  return frame
311
 
312
- # Use multiple detection backends for better results
313
  face_objs = []
314
  try:
315
- face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='opencv', enforce_detection=False)
316
- except:
 
 
 
 
 
317
  try:
318
- face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='mtcnn', enforce_detection=False)
319
- except:
320
- pass
 
 
 
 
321
 
322
  if face_objs:
323
- print(f"\n--- Frame Processed: Found {len(face_objs)} faces. ---")
324
 
325
- for i, face_obj in enumerate(face_objs):
326
  confidence = face_obj.get('confidence', 0.0)
327
- print(f" Face #{i+1}: Confidence Score = {confidence:.2f}")
328
-
329
  # Strict confidence threshold
330
- if confidence < 0.90: # Increased from 0.85 to be more strict
331
- print(" -> Confidence too low, skipping.")
332
  continue
333
 
334
- # Extract facial area
335
  facial_area = face_obj['facial_area']
336
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
337
-
338
  face_image = frame[y:y+h, x:x+w]
339
 
340
- if face_image.size == 0:
341
- continue
342
-
343
- # Minimum face size check
344
- if w < 50 or h < 50:
345
- print(" -> Face too small, skipping.")
346
  continue
347
 
348
- # Get embedding
349
  try:
350
- embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
 
 
 
 
 
351
  embedding_array = np.array(embedding)
352
  except Exception as e:
353
- print(f" -> Could not generate embedding: {e}, skipping.")
354
  continue
355
 
356
- color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
 
 
357
 
358
  if self.known_face_embeddings:
359
- # Strict matching with cosine similarity
360
  match_index, match_score = self._find_best_match(embedding_array)
361
 
362
- print(f" -> Comparing to DB... Cosine Similarity: {match_score:.4f}")
363
-
364
- # Strict threshold for recognition
365
- if match_index != -1 and match_score > 0.85: # Increased from 0.80
366
  worker_id = self.known_face_ids[match_index]
367
  worker_name = self.known_face_names[match_index]
368
- color = (0, 255, 0) # Green
369
- print(f" βœ“ MATCH! Recognized as {worker_name}")
370
 
371
- # Use buffering for consistent recognition
372
  buffer_key = f"{worker_id}"
373
  if buffer_key not in self.face_recognition_buffer:
374
- self.face_recognition_buffer[buffer_key] = {'count': 1, 'last_time': time.time()}
 
 
 
375
  else:
376
  self.face_recognition_buffer[buffer_key]['count'] += 1
377
  self.face_recognition_buffer[buffer_key]['last_time'] = time.time()
378
 
379
- # Mark attendance only after consistent detections and if confidence is high
380
- if (self.face_recognition_buffer[buffer_key]['count'] >= self.buffer_threshold and
381
  confidence >= 0.90):
382
  if self.mark_attendance(worker_id, worker_name):
383
  self.last_recognition_time[worker_id] = time.time()
384
- # Reset buffer after marking
385
  del self.face_recognition_buffer[buffer_key]
386
-
387
  else:
388
- # Only register as new if similarity is very low
389
- if match_score < 0.70: # Only register if very different from existing faces
390
- color = (0, 165, 255) # Orange for potential new worker
391
- print(f" βœ— NO MATCH. Attempting to register as new worker...")
392
  new_worker = self._register_worker_auto(face_image, embedding)
393
  if new_worker:
394
- worker_id, worker_name = new_worker[0], new_worker[1]
395
- if confidence >= 0.90 and self.mark_attendance(worker_id, worker_name):
396
- self.last_recognition_time[worker_id] = time.time()
397
- else:
398
- print(" βœ— Uncertain match, not registering as new worker.")
399
  else:
400
- # No known faces, auto-register only if confidence is high
401
  if confidence >= 0.90:
402
- color = (0, 165, 255) # Orange for new worker
403
- print(" -> No known faces in database. Auto-registering...")
404
  new_worker = self._register_worker_auto(face_image, embedding)
405
  if new_worker:
406
- worker_id, worker_name = new_worker[0], new_worker[1]
407
- if self.mark_attendance(worker_id, worker_name):
408
- self.last_recognition_time[worker_id] = time.time()
409
 
410
  # Clean old buffer entries
411
  current_time = time.time()
412
- keys_to_remove = [k for k, v in self.face_recognition_buffer.items()
413
- if current_time - v['last_time'] > 5.0]
414
- for key in keys_to_remove:
415
- del self.face_recognition_buffer[key]
416
 
 
417
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
418
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
419
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
420
 
421
  return frame
422
  except Exception as e:
423
- print(f"ERROR in process_frame: {e}")
424
  return frame
425
 
426
  def _processing_loop(self, source):
427
  video_capture = cv2.VideoCapture(source)
428
  if not video_capture.isOpened():
429
- 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."
430
  self.error_message = err_msg
431
  self.is_processing.clear()
432
  return
 
433
  while self.is_processing.is_set():
434
  ret, frame = video_capture.read()
435
- if not ret: break
 
 
436
  processed_frame = self.process_frame(frame)
437
- if not self.frame_queue.full(): self.frame_queue.put(processed_frame)
438
- self.last_processed_frame = processed_frame # Continuously update last frame
 
439
  time.sleep(0.05)
440
- self.final_log = self.session_log.copy() # Save the final log
 
441
  video_capture.release()
442
  self.is_processing.clear()
443
 
444
  def start_processing(self, source) -> str:
445
- if self.is_processing.is_set(): return "⚠️ Processing is already active."
446
- # Reset states for the new session
 
 
447
  self.session_log.clear()
448
  self.last_recognition_time.clear()
449
- self.session_marked_present.clear() # Reset session attendance tracking
450
- self.session_registered.clear() # Reset session registration tracking
451
- self.face_recognition_buffer.clear() # Reset recognition buffer
452
  self.error_message = None
453
  self.last_processed_frame = None
454
  self.final_log = None
455
  self.frame_skip_counter = 0
 
456
  self.is_processing.set()
457
- self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
458
- self.processing_thread.daemon = True
 
 
 
459
  self.processing_thread.start()
460
- return f"βœ… Started processing..."
461
 
462
  def stop_processing(self) -> str:
463
- # Reset states when stopping manually
464
  self.is_processing.clear()
465
  self.error_message = None
466
  self.last_processed_frame = None
467
  self.final_log = None
468
  self.face_recognition_buffer.clear()
469
- return "βœ… Processing stopped by user."
470
 
471
- # --- Helper & Reporting ---
472
  def _get_image_caption(self, image: Image.Image) -> str:
473
- if not HF_API_TOKEN: return "Hugging Face API token not configured."
 
474
  try:
475
  buffered = BytesIO()
476
  image.save(buffered, format="JPEG")
477
- img_data = buffered.getvalue()
478
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
479
- response = requests.post(HF_API_URL, headers=headers, data=img_data)
480
  response.raise_for_status()
481
- result = response.json()
482
- return result[0].get("generated_text", "No caption found.")
483
  except Exception as e:
484
  logger.error(f"Hugging Face API error: {e}")
485
  return "Caption generation failed."
486
 
487
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
488
- if not self.sf: return None
 
489
  try:
490
  buffered = BytesIO()
491
  image.save(buffered, format="JPEG")
492
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
493
- cv = self.sf.ContentVersion.create({'Title': f'Image_{worker_id}', 'PathOnClient': f'{worker_id}.jpg', 'VersionData': encoded_image, 'FirstPublishLocationId': record_id})
494
- return f"/{cv['id']}" # Relative URL
 
 
 
 
 
495
  except Exception as e:
496
  logger.error(f"Salesforce image upload error: {e}")
497
  return None
498
 
499
  def get_registered_workers_info(self) -> str:
500
- if not self.sf: return "❌ Salesforce not connected."
 
501
  try:
502
- records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
503
- if not records: return "No workers registered."
504
- return f"**πŸ‘₯ Registered Workers ({len(records)})**\n" + "\n".join([f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records])
505
- except Exception as e: return f"Error: {e}"
506
-
 
 
 
 
 
 
 
 
507
  # --- GRADIO UI ---
508
  attendance_system = AttendanceSystem()
 
509
  def create_interface():
510
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
511
  gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
 
512
  with gr.Tabs():
513
  with gr.Tab("βš™οΈ Controls & Status"):
514
  gr.Markdown("### 1. Choose Input Source & Start Processing")
@@ -546,29 +583,57 @@ def create_interface():
546
  refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
547
 
548
  # --- Event Handlers ---
549
- def on_tab_select(evt: gr.SelectData): return evt.index
 
 
550
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
 
551
  def start_wrapper(tab_index, cam_src, vid_path):
552
  source = cam_src if tab_index == 0 else vid_path
553
- return "Please provide an input source." if source is None else attendance_system.start_processing(source)
554
- start_btn.click(fn=start_wrapper, inputs=[selected_tab_index, camera_source, video_file], outputs=[status_box])
555
- stop_btn.click(fn=attendance_system.stop_processing, inputs=None, outputs=[status_box])
556
- register_btn.click(fn=attendance_system.register_worker_manual, inputs=[register_image, register_name], outputs=[register_output, registered_workers_info])
557
- refresh_workers_btn.click(fn=attendance_system.get_registered_workers_info, outputs=[registered_workers_info])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
  def update_ui_generator():
560
  while True:
561
  if attendance_system.error_message:
562
  yield None, attendance_system.error_message
563
- time.sleep(2); attendance_system.error_message = None
 
564
  continue
 
565
  if attendance_system.is_processing.is_set():
566
  frame, log_md = None, "\n".join(reversed(attendance_system.session_log)) or "Processing..."
567
  try:
568
  if not attendance_system.frame_queue.empty():
569
  frame = attendance_system.frame_queue.get_nowait()
570
- if frame is not None: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
571
- except queue.Empty: pass
 
 
572
  yield frame, log_md
573
  else:
574
  if attendance_system.last_processed_frame is not None:
@@ -579,7 +644,10 @@ def create_interface():
579
  yield None, "System stopped. Go to 'Controls & Status' to start."
580
  time.sleep(0.1)
581
 
582
- demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
 
 
 
583
  return demo
584
 
585
  if __name__ == "__main__":
 
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
77
+ self.final_log = None
78
 
79
  # Data Storage
80
  self.known_face_embeddings: List[np.ndarray] = []
 
82
  self.known_face_ids: List[str] = []
83
  self.next_worker_id: int = 1
84
 
85
+ # Session Tracking
86
  self.last_recognition_time = {}
87
+ self.recognition_cooldown = 10
88
  self.session_log: List[str] = []
89
+ self.session_marked_present = set()
90
+ self.session_registered = set()
91
+ self.face_recognition_buffer = {}
92
+ self.buffer_threshold = 3
93
+ self.frame_skip_counter = 0
94
 
95
  # Initialize
96
  self.sf = connect_to_salesforce()
 
138
  def _load_local_worker_data(self):
139
  try:
140
  if os.path.exists("data/workers.pkl"):
141
+ with open("data/workers.pkl", "rb") as f:
142
+ 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", [])
 
150
 
151
  def save_local_worker_data(self):
152
  try:
153
+ worker_data = {
154
+ "embeddings": self.known_face_embeddings,
155
+ "names": self.known_face_names,
156
+ "ids": self.known_face_ids,
157
+ "next_id": self.next_worker_id
158
+ }
159
+ with open("data/workers.pkl", "wb") as f:
160
+ pickle.dump(worker_data, f)
161
  except Exception as e:
162
  logger.error(f"❌ Error saving local worker data: {e}")
163
 
 
185
 
186
  def _register_worker_auto(self, face_image: np.ndarray, face_embedding: List[float]) -> Optional[Tuple[str, str]]:
187
  try:
188
+ # Check for duplicates with strict threshold
189
  if self._is_duplicate_face(face_embedding, threshold=10.0):
190
  return None
191
 
 
220
  caption = self._get_image_caption(face_pil)
221
  if self.sf:
222
  try:
223
+ worker_record = self.sf.Worker__c.create({
224
+ 'Name': name,
225
+ 'Worker_ID__c': worker_id,
226
+ 'Face_Embedding__c': json.dumps(embedding),
227
+ 'Image_Caption__c': caption
228
+ })
229
  image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
230
+ if image_url:
231
+ self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
232
  logger.info(f"βœ… Worker {worker_id} synced to Salesforce.")
233
  except Exception as e:
234
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
 
240
 
241
  embedding_array = np.array(embedding)
242
  for known_embedding in self.known_face_embeddings:
243
+ # Normalize vectors
244
+ embedding_array_norm = embedding_array / np.linalg.norm(embedding_array)
245
+ known_embedding_norm = known_embedding / np.linalg.norm(known_embedding)
246
+
247
+ # Calculate cosine similarity
248
+ cosine_sim = np.dot(embedding_array_norm, known_embedding_norm)
249
 
250
+ if cosine_sim > 0.85: # Strict threshold
 
251
  return True
252
 
253
  return False
254
 
255
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
256
+ """Attendance marking with strict checks"""
 
257
  if worker_id in self.session_marked_present:
258
  return False
259
 
 
274
  except Exception as e:
275
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
276
 
 
277
  self.session_marked_present.add(worker_id)
278
 
279
  log_msg = f"βœ… [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
 
286
  return True
287
  if self.sf:
288
  try:
289
+ query = f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = '{today_str}'"
290
+ if self.sf.query(query)['totalSize'] > 0:
291
  return True
292
+ except Exception as e:
293
+ logger.error(f"Attendance check error: {e}")
294
  return False
295
 
296
  def _find_best_match(self, target_embedding: np.ndarray) -> Tuple[int, float]:
297
+ """Find best match using cosine similarity"""
298
  if not self.known_face_embeddings:
299
+ return -1, 0.0
300
 
301
  best_match_idx = -1
302
+ best_score = 0.0
303
+
304
+ target_norm = target_embedding / np.linalg.norm(target_embedding)
305
 
306
  for i, known_embedding in enumerate(self.known_face_embeddings):
307
+ known_norm = known_embedding / np.linalg.norm(known_embedding)
308
+ cosine_sim = np.dot(target_norm, known_norm)
 
 
309
 
310
  if cosine_sim > best_score:
311
  best_score = cosine_sim
 
315
 
316
  # --- Video Processing ---
317
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
318
+ """Frame processing with strict recognition rules"""
 
 
319
  try:
320
+ # Skip frames for performance
321
  self.frame_skip_counter += 1
322
+ if self.frame_skip_counter % 3 != 0:
323
  return frame
324
 
325
+ # Detect faces with multiple backends
326
  face_objs = []
327
  try:
328
+ face_objs = DeepFace.extract_faces(
329
+ img_path=frame,
330
+ detector_backend='opencv',
331
+ enforce_detection=False
332
+ )
333
+ except Exception as e:
334
+ logger.warning(f"OpenCV detector failed: {e}")
335
  try:
336
+ face_objs = DeepFace.extract_faces(
337
+ img_path=frame,
338
+ detector_backend='mtcnn',
339
+ enforce_detection=False
340
+ )
341
+ except Exception as e:
342
+ logger.warning(f"MTCNN detector failed: {e}")
343
 
344
  if face_objs:
345
+ logger.debug(f"Found {len(face_objs)} faces in frame")
346
 
347
+ for face_obj in face_objs:
348
  confidence = face_obj.get('confidence', 0.0)
349
+
 
350
  # Strict confidence threshold
351
+ if confidence < 0.90:
 
352
  continue
353
 
 
354
  facial_area = face_obj['facial_area']
355
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
 
356
  face_image = frame[y:y+h, x:x+w]
357
 
358
+ if face_image.size == 0 or w < 50 or h < 50:
 
 
 
 
 
359
  continue
360
 
 
361
  try:
362
+ embedding_obj = DeepFace.represent(
363
+ img_path=face_image,
364
+ model_name='Facenet',
365
+ enforce_detection=False
366
+ )
367
+ embedding = embedding_obj[0]['embedding']
368
  embedding_array = np.array(embedding)
369
  except Exception as e:
370
+ logger.warning(f"Embedding generation failed: {e}")
371
  continue
372
 
373
+ color = (0, 0, 255) # Default red for unknown
374
+ worker_id = None
375
+ worker_name = "Unknown"
376
 
377
  if self.known_face_embeddings:
 
378
  match_index, match_score = self._find_best_match(embedding_array)
379
 
380
+ # Strict matching threshold
381
+ if match_index != -1 and match_score > 0.85:
 
 
382
  worker_id = self.known_face_ids[match_index]
383
  worker_name = self.known_face_names[match_index]
384
+ color = (0, 255, 0) # Green for known
 
385
 
386
+ # Buffer recognition
387
  buffer_key = f"{worker_id}"
388
  if buffer_key not in self.face_recognition_buffer:
389
+ self.face_recognition_buffer[buffer_key] = {
390
+ 'count': 1,
391
+ 'last_time': time.time()
392
+ }
393
  else:
394
  self.face_recognition_buffer[buffer_key]['count'] += 1
395
  self.face_recognition_buffer[buffer_key]['last_time'] = time.time()
396
 
397
+ # Mark attendance after consistent detections
398
+ if (self.face_recognition_buffer[buffer_key]['count'] >= self.buffer_threshold and
399
  confidence >= 0.90):
400
  if self.mark_attendance(worker_id, worker_name):
401
  self.last_recognition_time[worker_id] = time.time()
 
402
  del self.face_recognition_buffer[buffer_key]
 
403
  else:
404
+ # Only register new if very different from existing faces
405
+ if match_score < 0.70: # Low similarity threshold
406
+ color = (0, 165, 255) # Orange for new
 
407
  new_worker = self._register_worker_auto(face_image, embedding)
408
  if new_worker:
409
+ worker_id, worker_name = new_worker
410
+ if confidence >= 0.90:
411
+ self.mark_attendance(worker_id, worker_name)
 
 
412
  else:
413
+ # No known faces, auto-register with high confidence
414
  if confidence >= 0.90:
415
+ color = (0, 165, 255) # Orange for new
 
416
  new_worker = self._register_worker_auto(face_image, embedding)
417
  if new_worker:
418
+ worker_id, worker_name = new_worker
419
+ self.mark_attendance(worker_id, worker_name)
 
420
 
421
  # Clean old buffer entries
422
  current_time = time.time()
423
+ for key in list(self.face_recognition_buffer.keys()):
424
+ if current_time - self.face_recognition_buffer[key]['last_time'] > 5.0:
425
+ del self.face_recognition_buffer[key]
 
426
 
427
+ # Draw bounding box and label
428
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
429
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
430
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
431
 
432
  return frame
433
  except Exception as e:
434
+ logger.error(f"Frame processing error: {e}")
435
  return frame
436
 
437
  def _processing_loop(self, source):
438
  video_capture = cv2.VideoCapture(source)
439
  if not video_capture.isOpened():
440
+ err_msg = "❌ Could not open video source"
441
  self.error_message = err_msg
442
  self.is_processing.clear()
443
  return
444
+
445
  while self.is_processing.is_set():
446
  ret, frame = video_capture.read()
447
+ if not ret:
448
+ break
449
+
450
  processed_frame = self.process_frame(frame)
451
+ if not self.frame_queue.full():
452
+ self.frame_queue.put(processed_frame)
453
+ self.last_processed_frame = processed_frame
454
  time.sleep(0.05)
455
+
456
+ self.final_log = self.session_log.copy()
457
  video_capture.release()
458
  self.is_processing.clear()
459
 
460
  def start_processing(self, source) -> str:
461
+ if self.is_processing.is_set():
462
+ return "⚠️ Processing is already active."
463
+
464
+ # Reset session state
465
  self.session_log.clear()
466
  self.last_recognition_time.clear()
467
+ self.session_marked_present.clear()
468
+ self.session_registered.clear()
469
+ self.face_recognition_buffer.clear()
470
  self.error_message = None
471
  self.last_processed_frame = None
472
  self.final_log = None
473
  self.frame_skip_counter = 0
474
+
475
  self.is_processing.set()
476
+ self.processing_thread = threading.Thread(
477
+ target=self._processing_loop,
478
+ args=(source,),
479
+ daemon=True
480
+ )
481
  self.processing_thread.start()
482
+ return "βœ… Started processing..."
483
 
484
  def stop_processing(self) -> str:
 
485
  self.is_processing.clear()
486
  self.error_message = None
487
  self.last_processed_frame = None
488
  self.final_log = None
489
  self.face_recognition_buffer.clear()
490
+ return "βœ… Processing stopped."
491
 
492
+ # --- Helper Methods ---
493
  def _get_image_caption(self, image: Image.Image) -> str:
494
+ if not HF_API_TOKEN:
495
+ return "Hugging Face API token not configured."
496
  try:
497
  buffered = BytesIO()
498
  image.save(buffered, format="JPEG")
 
499
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
500
+ response = requests.post(HF_API_URL, headers=headers, data=buffered.getvalue())
501
  response.raise_for_status()
502
+ return response.json()[0].get("generated_text", "No caption found.")
 
503
  except Exception as e:
504
  logger.error(f"Hugging Face API error: {e}")
505
  return "Caption generation failed."
506
 
507
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
508
+ if not self.sf:
509
+ return None
510
  try:
511
  buffered = BytesIO()
512
  image.save(buffered, format="JPEG")
513
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
514
+ cv = self.sf.ContentVersion.create({
515
+ 'Title': f'Image_{worker_id}',
516
+ 'PathOnClient': f'{worker_id}.jpg',
517
+ 'VersionData': encoded_image,
518
+ 'FirstPublishLocationId': record_id
519
+ })
520
+ return f"/{cv['id']}"
521
  except Exception as e:
522
  logger.error(f"Salesforce image upload error: {e}")
523
  return None
524
 
525
  def get_registered_workers_info(self) -> str:
526
+ if not self.sf:
527
+ return "❌ Salesforce not connected."
528
  try:
529
+ records = self.sf.query_all(
530
+ "SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name"
531
+ )['records']
532
+ if not records:
533
+ return "No workers registered."
534
+ worker_list = "\n".join(
535
+ f"- **{w['Name']}** (ID: {w['Worker_ID__c']})"
536
+ for w in records
537
+ )
538
+ return f"**πŸ‘₯ Registered Workers ({len(records)})**\n{worker_list}"
539
+ except Exception as e:
540
+ return f"Error: {e}"
541
+
542
  # --- GRADIO UI ---
543
  attendance_system = AttendanceSystem()
544
+
545
  def create_interface():
546
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
547
  gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
548
+
549
  with gr.Tabs():
550
  with gr.Tab("βš™οΈ Controls & Status"):
551
  gr.Markdown("### 1. Choose Input Source & Start Processing")
 
583
  refresh_workers_btn = gr.Button("πŸ”„ Refresh List")
584
 
585
  # --- Event Handlers ---
586
+ def on_tab_select(evt: gr.SelectData):
587
+ return evt.index
588
+
589
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
590
+
591
  def start_wrapper(tab_index, cam_src, vid_path):
592
  source = cam_src if tab_index == 0 else vid_path
593
+ if source is None:
594
+ return "Please provide an input source."
595
+ return attendance_system.start_processing(source)
596
+
597
+ start_btn.click(
598
+ fn=start_wrapper,
599
+ inputs=[selected_tab_index, camera_source, video_file],
600
+ outputs=[status_box]
601
+ )
602
+
603
+ stop_btn.click(
604
+ fn=attendance_system.stop_processing,
605
+ inputs=None,
606
+ outputs=[status_box]
607
+ )
608
+
609
+ register_btn.click(
610
+ fn=attendance_system.register_worker_manual,
611
+ inputs=[register_image, register_name],
612
+ outputs=[register_output, registered_workers_info]
613
+ )
614
+
615
+ refresh_workers_btn.click(
616
+ fn=attendance_system.get_registered_workers_info,
617
+ outputs=[registered_workers_info]
618
+ )
619
 
620
  def update_ui_generator():
621
  while True:
622
  if attendance_system.error_message:
623
  yield None, attendance_system.error_message
624
+ time.sleep(2)
625
+ attendance_system.error_message = None
626
  continue
627
+
628
  if attendance_system.is_processing.is_set():
629
  frame, log_md = None, "\n".join(reversed(attendance_system.session_log)) or "Processing..."
630
  try:
631
  if not attendance_system.frame_queue.empty():
632
  frame = attendance_system.frame_queue.get_nowait()
633
+ if frame is not None:
634
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
635
+ except queue.Empty:
636
+ pass
637
  yield frame, log_md
638
  else:
639
  if attendance_system.last_processed_frame is not None:
 
644
  yield None, "System stopped. Go to 'Controls & Status' to start."
645
  time.sleep(0.1)
646
 
647
+ demo.load(
648
+ fn=update_ui_generator,
649
+ outputs=[video_output, session_log_display]
650
+ )
651
  return demo
652
 
653
  if __name__ == "__main__":