PrashanthB461 commited on
Commit
8b3db3a
Β·
verified Β·
1 Parent(s): e5c009b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +301 -390
app.py CHANGED
@@ -18,8 +18,8 @@ import pickle
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
24
  from deepface import DeepFace
25
  from retrying import retry
@@ -31,7 +31,7 @@ from simple_salesforce import Salesforce
31
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
32
  logger = logging.getLogger(__name__)
33
 
34
- # Load environment variables from .env file
35
  load_dotenv()
36
 
37
  # Hugging Face API configuration
@@ -46,32 +46,22 @@ SF_CREDENTIALS = {
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)
58
  def connect_to_salesforce() -> Optional[Salesforce]:
59
- """Establish a connection to Salesforce with retry logic."""
60
  try:
61
  sf = Salesforce(**SF_CREDENTIALS)
62
- sf.describe() # Test connection
63
- logger.info("βœ… Successfully connected to Salesforce.")
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
@@ -82,16 +72,23 @@ class AttendanceSystem:
82
  self.final_log = None
83
 
84
  # Data Storage
85
- self.known_face_embeddings: List[np.ndarray] = []
86
- self.known_face_names: List[str] = []
87
- self.known_face_ids: List[str] = []
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,35 +99,36 @@ class AttendanceSystem:
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):
@@ -142,91 +140,52 @@ class AttendanceSystem:
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}")
@@ -242,240 +201,185 @@ class AttendanceSystem:
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,
@@ -483,39 +387,29 @@ class AttendanceSystem:
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")
@@ -524,129 +418,146 @@ class AttendanceSystem:
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
- )
 
18
  import cv2
19
  import gradio as gr
20
  import numpy as np
 
21
  import requests
22
+ from PIL import Image
23
  from dotenv import load_dotenv
24
  from deepface import DeepFace
25
  from retrying import retry
 
31
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
32
  logger = logging.getLogger(__name__)
33
 
34
+ # Load environment variables
35
  load_dotenv()
36
 
37
  # Hugging Face API configuration
 
46
  "domain": os.getenv("SF_DOMAIN", "login")
47
  }
48
 
 
 
 
 
 
 
49
  # --- SALESFORCE CONNECTION ---
50
 
51
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
52
  def connect_to_salesforce() -> Optional[Salesforce]:
 
53
  try:
54
  sf = Salesforce(**SF_CREDENTIALS)
55
+ sf.describe()
56
+ logger.info("βœ… Connected to Salesforce")
57
  return sf
58
  except Exception as e:
59
  logger.error(f"❌ Salesforce connection failed: {e}")
60
+ raise
61
 
62
  # --- CORE LOGIC ---
63
 
64
  class AttendanceSystem:
 
 
 
65
  def __init__(self):
66
  # State Management
67
  self.processing_thread = None
 
72
  self.final_log = None
73
 
74
  # Data Storage
75
+ self.known_face_embeddings = []
76
+ self.known_face_names = []
77
+ self.known_face_ids = []
78
+ self.next_worker_id = 1
79
 
80
  # Session Tracking
81
+ self.last_recognition_time = {}
82
+ self.recognition_cooldown = 5
83
+ self.session_log = []
84
+ self.today_attendance = {}
85
+ self.last_detected_faces = {}
86
+
87
+ # Performance Optimizations
88
+ self.frame_skip = 1 # Process every other frame
89
+ self.frame_counter = 0
90
+ self.last_face_detection_time = 0
91
+ self.face_detection_interval = 0.5 # Seconds between face detections
92
 
93
  # Initialize
94
  self.sf = connect_to_salesforce()
 
99
  os.makedirs("data/faces", exist_ok=True)
100
 
101
  def load_worker_data(self):
102
+ logger.info("Loading worker data...")
103
+ if self.sf:
104
+ try:
105
  workers = self.sf.query_all("SELECT Worker_ID__c, Name, Face_Embedding__c FROM Worker__c")['records']
106
+ if not workers:
107
+ self._load_local_worker_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  return
109
+
110
+ temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
111
+ for worker in workers:
112
+ if worker.get('Face_Embedding__c'):
113
+ temp_embeddings.append(np.array(json.loads(worker['Face_Embedding__c'])))
114
+ temp_names.append(worker['Name'])
115
+ temp_ids.append(worker['Worker_ID__c'])
116
+ try:
117
+ worker_num = int(worker['Worker_ID__c'][1:])
118
+ max_id = max(max_id, worker_num)
119
+ except (ValueError, TypeError):
120
+ continue
121
+
122
+ self.known_face_embeddings = temp_embeddings
123
+ self.known_face_names = temp_names
124
+ self.known_face_ids = temp_ids
125
+ self.next_worker_id = max_id + 1
126
+ self.save_local_worker_data()
127
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers")
128
+ except Exception as e:
129
+ logger.error(f"❌ Error loading from Salesforce: {e}")
130
+ self._load_local_worker_data()
131
+ else:
132
  self._load_local_worker_data()
133
 
134
  def _load_local_worker_data(self):
 
140
  self.known_face_names = data.get("names", [])
141
  self.known_face_ids = data.get("ids", [])
142
  self.next_worker_id = data.get("next_id", 1)
143
+ logger.info(f"βœ… Loaded {len(self.known_face_ids)} workers from cache")
144
  except Exception as e:
145
+ logger.error(f"❌ Error loading local data: {e}")
146
 
147
  def save_local_worker_data(self):
148
  try:
 
 
 
 
 
 
149
  with open("data/workers.pkl", "wb") as f:
150
+ pickle.dump({
151
+ "embeddings": self.known_face_embeddings,
152
+ "names": self.known_face_names,
153
+ "ids": self.known_face_ids,
154
+ "next_id": self.next_worker_id
155
+ }, f)
156
  except Exception as e:
157
+ logger.error(f"❌ Error saving local data: {e}")
158
 
159
  # --- Registration and Attendance ---
160
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
161
  if image is None or not name.strip():
162
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
 
163
  try:
164
  image_array = np.array(image)
165
+ embedding = DeepFace.represent(img_path=image_array, model_name='Facenet', enforce_detection=True)[0]['embedding']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  if self._is_duplicate_face(embedding):
167
+ return f"❌ Face matches existing worker!", self.get_registered_workers_info()
168
 
169
  worker_id = f"W{self.next_worker_id:04d}"
170
  name = name.strip().title()
171
  self._add_worker_to_system(worker_id, name, embedding, image_array)
172
  return f"βœ… {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
173
+ except ValueError:
174
+ return "❌ No face detected!", self.get_registered_workers_info()
175
  except Exception as e:
176
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
177
 
178
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
179
  try:
180
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  if self._is_duplicate_face(embedding):
182
  return None
183
+
184
  worker_id = f"W{self.next_worker_id:04d}"
185
  worker_name = f"Worker {self.next_worker_id}"
186
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
187
+ log_msg = f"πŸ†• [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
188
+ self.session_log.append(log_msg)
 
189
  return worker_id, worker_name
190
  except Exception as e:
191
  logger.error(f"Auto-registration error: {e}")
 
201
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
202
  face_pil.save(f"data/faces/{worker_id}.jpg")
203
 
204
+ # Sync with Salesforce if available
205
+ if self.sf:
206
+ try:
207
+ caption = self._get_image_caption(face_pil)
208
+ worker_record = self.sf.Worker__c.create({
209
+ 'Name': name,
210
+ 'Worker_ID__c': worker_id,
211
+ 'Face_Embedding__c': json.dumps(embedding),
212
+ 'Image_Caption__c': caption
213
+ })
214
+ image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
215
+ if image_url:
216
+ self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
217
+ except Exception as e:
218
+ logger.error(f"Salesforce sync error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  def _is_duplicate_face(self, embedding: List[float], threshold: float = 10.0) -> bool:
221
  if not self.known_face_embeddings:
222
  return False
223
+ distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
 
 
 
 
 
224
  return min(distances) < threshold
225
 
226
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
227
  today_str = date.today().isoformat()
228
+ current_time = time.time()
229
 
230
+ # Check if already marked today
231
  if worker_id in self.today_attendance:
232
  return False
233
 
234
+ # Check cooldown period
235
+ if worker_id in self.last_recognition_time and (current_time - self.last_recognition_time[worker_id] < self.recognition_cooldown):
236
+ return False
237
+
238
+ # Mark attendance
239
+ current_time_dt = datetime.now()
240
+ if self.sf:
241
+ try:
242
+ self.sf.Attendance__c.create({
243
+ 'Worker_ID__c': worker_id,
244
+ 'Name__c': worker_name,
245
+ 'Date__c': today_str,
246
+ 'Timestamp__c': current_time_dt.isoformat(),
247
+ 'Status__c': "Present"
248
+ })
249
+ except Exception as e:
250
+ logger.error(f"Salesforce error: {e}")
251
 
252
+ self.today_attendance[worker_id] = True
253
+ self.last_recognition_time[worker_id] = current_time
254
+ log_msg = f"βœ… [{current_time_dt.strftime('%H:%M:%S')}] Present: {worker_name} ({worker_id})"
255
+ self.session_log.append(log_msg)
256
  return True
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  # --- Video Processing ---
259
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
 
260
  try:
261
+ current_time = time.time()
262
 
263
+ # Skip frames if processing too frequently
264
+ if current_time - self.last_face_detection_time < self.face_detection_interval:
265
  return frame
266
 
267
+ self.last_face_detection_time = current_time
268
 
269
  # Resize for faster processing
270
+ small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5)
 
 
271
 
272
+ # Face detection with optimized parameters
273
  face_objs = DeepFace.extract_faces(
274
  img_path=small_frame,
275
+ detector_backend='opencv',
276
  enforce_detection=False,
277
+ align=False # Alignment takes extra time
278
  )
279
 
280
  for face_obj in face_objs:
281
+ if face_obj['confidence'] < 0.9: # Lower confidence threshold for speed
282
  continue
283
+
 
284
  facial_area = face_obj['facial_area']
285
+ x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
 
 
 
286
 
287
+ # Scale coordinates back to original frame
288
+ x, y, w, h = x*2, y*2, w*2, h*2
289
  face_image = frame[y:y+h, x:x+w]
290
+
291
  if face_image.size == 0:
292
  continue
293
+
294
+ # Skip if we recently processed this face position
295
+ face_key = f"{x}_{y}"
296
+ if face_key in self.last_detected_faces and (current_time - self.last_detected_faces[face_key] < 2.0):
297
+ continue
298
+ self.last_detected_faces[face_key] = current_time
299
+
300
+ # Face recognition
301
+ embedding = DeepFace.represent(
302
+ img_path=face_image,
303
+ model_name='Facenet',
304
+ enforce_detection=False,
305
+ align=False # Skip alignment for speed
306
+ )[0]['embedding']
307
 
308
+ if not self.known_face_embeddings:
309
+ continue
310
+
311
+ # Find closest match
312
+ distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
313
+ min_dist = min(distances) if distances else float('inf')
314
+ match_index = distances.index(min_dist) if min_dist < 10.0 else -1
315
+
316
+ color = (0, 0, 255) # Default red for unknown
317
+ label = "Unknown"
318
 
319
+ if match_index != -1:
320
+ worker_id = self.known_face_ids[match_index]
321
+ worker_name = self.known_face_names[match_index]
322
+ color = (0, 255, 0) # Green for known
323
+ label = f"{worker_name} ({worker_id})"
324
+ self.mark_attendance(worker_id, worker_name)
325
+ else:
326
+ # Only auto-register if face is clear
327
+ color = (0, 165, 255) # Orange for new
328
+ new_worker = self._register_worker_auto(face_image)
329
+ if new_worker:
330
+ worker_id, worker_name = new_worker
331
+ label = f"{worker_name} ({worker_id})"
332
+ self.mark_attendance(worker_id, worker_name)
333
+
334
+ # Draw rectangle and label
335
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
336
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
337
 
338
  return frame
339
  except Exception as e:
340
+ logger.error(f"Processing error: {e}")
341
  return frame
342
 
343
+ def _processing_loop(self, source):
344
+ cap = cv2.VideoCapture(source)
345
+ if not cap.isOpened():
346
+ self.error_message = "❌ Could not open video source"
347
+ self.is_processing.clear()
348
+ return
 
 
 
 
349
 
350
+ # Set higher FPS if possible
351
+ if isinstance(source, int):
352
+ cap.set(cv2.CAP_PROP_FPS, 30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
+ while self.is_processing.is_set():
355
+ ret, frame = cap.read()
356
+ if not ret:
357
+ break
358
 
359
+ processed_frame = self.process_frame(frame)
360
+
361
+ # Only update queue if not full to prevent lag
362
+ if self.frame_queue.qsize() < 3:
363
+ self.frame_queue.put(processed_frame)
364
+
365
+ self.last_processed_frame = processed_frame
366
+
367
+ cap.release()
368
+ self.final_log = self.session_log.copy()
369
+ self.is_processing.clear()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
  def start_processing(self, source) -> str:
 
372
  if self.is_processing.is_set():
373
+ return "⚠️ Already processing"
374
+
375
+ # Reset session state
376
  self.session_log.clear()
377
  self.today_attendance.clear()
378
+ self.last_detected_faces.clear()
379
  self.error_message = None
380
  self.last_processed_frame = None
381
  self.final_log = None
382
+
 
383
  self.is_processing.set()
384
  self.processing_thread = threading.Thread(
385
  target=self._processing_loop,
 
387
  daemon=True
388
  )
389
  self.processing_thread.start()
390
+ return "βœ… Processing started"
391
 
392
  def stop_processing(self) -> str:
 
393
  self.is_processing.clear()
394
+ return "βœ… Processing stopped"
395
 
396
  # --- Helper Methods ---
397
  def _get_image_caption(self, image: Image.Image) -> str:
 
398
  if not HF_API_TOKEN:
399
+ return "No API token"
 
400
  try:
401
  buffered = BytesIO()
402
  image.save(buffered, format="JPEG")
403
+ headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
404
+ response = requests.post(HF_API_URL, headers=headers, data=buffered.getvalue())
405
+ return response.json()[0].get("generated_text", "No caption")
 
 
 
 
 
406
  except Exception as e:
407
  logger.error(f"Caption error: {e}")
408
+ return "Caption failed"
409
 
410
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
 
411
  if not self.sf:
412
  return None
 
413
  try:
414
  buffered = BytesIO()
415
  image.save(buffered, format="JPEG")
 
418
  'PathOnClient': f'{worker_id}.jpg',
419
  'VersionData': base64.b64encode(buffered.getvalue()).decode('utf-8'),
420
  'FirstPublishLocationId': record_id
421
+ })
422
  return f"/{cv['id']}"
423
  except Exception as e:
424
+ logger.error(f"Upload error: {e}")
425
  return None
426
 
427
  def get_registered_workers_info(self) -> str:
428
+ if not self.sf:
429
+ return "❌ Not connected to Salesforce"
430
+ try:
431
+ workers = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
432
+ if not workers:
433
+ return "No workers registered"
434
+ return "**πŸ‘₯ Registered Workers**\n" + "\n".join(
435
+ f"- {w['Name']} ({w['Worker_ID__c']})" for w in workers
436
+ )
437
+ except Exception as e:
438
+ return f"Error: {e}"
439
 
440
  # --- GRADIO UI ---
441
  attendance_system = AttendanceSystem()
442
 
443
  def create_interface():
444
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
445
+ gr.Markdown("# 🎯 Face Recognition Attendance System")
446
 
447
  with gr.Tabs():
448
+ with gr.Tab("βš™οΈ Controls"):
449
  with gr.Row():
450
+ with gr.Column():
451
+ video_source = gr.Radio(
452
+ ["Camera", "Video File"],
453
+ label="Input Source",
454
+ value="Camera"
455
+ )
456
+ camera_device = gr.Number(
457
+ label="Camera Device",
458
+ value=0,
459
+ visible=True,
460
+ precision=0
461
+ )
462
+ video_file = gr.Video(
463
+ label="Upload Video",
464
+ visible=False
 
 
 
465
  )
466
+ with gr.Column():
467
+ start_btn = gr.Button("▢️ Start", variant="primary")
468
+ stop_btn = gr.Button("⏹️ Stop", variant="stop")
469
+ status = gr.Textbox(label="Status", value="Ready")
470
 
471
+ gr.Markdown("**Color Legend:** <span style='color:green'>Green</span> = Known | <span style='color:orange'>Orange</span> = New | <span style='color:red'>Red</span> = Unknown")
472
+
473
+ with gr.Tab("πŸ“Š Output"):
474
  with gr.Row():
475
+ video_output = gr.Image(label="Live View", interactive=False)
476
  with gr.Column():
477
+ session_log = gr.Markdown("### Session Log")
478
+ refresh_log = gr.Button("πŸ”„ Refresh Log")
479
+
480
+ with gr.Tab("πŸ‘€ Management"):
481
+ with gr.Row():
 
 
 
 
482
  with gr.Column():
483
+ reg_image = gr.Image(label="Worker Photo", type="pil")
484
+ reg_name = gr.Textbox(label="Full Name")
485
+ reg_btn = gr.Button("Register", variant="primary")
486
+ reg_status = gr.Textbox(label="Status")
487
+ with gr.Column():
488
+ workers_list = gr.Markdown(attendance_system.get_registered_workers_info())
489
+ refresh_workers = gr.Button("πŸ”„ Refresh List")
490
+
491
  # Event handlers
492
+ video_source.change(
493
+ lambda x: (gr.update(visible=x == "Camera"), gr.update(visible=x == "Video File")),
494
+ inputs=video_source,
495
+ outputs=[camera_device, video_file]
496
  )
497
+
498
+ def start_processing(source, cam_device, video_path):
499
+ src = cam_device if source == "Camera" else video_path
500
+ return attendance_system.start_processing(src)
501
+
502
  start_btn.click(
503
+ start_processing,
504
+ inputs=[video_source, camera_device, video_file],
505
+ outputs=status
506
  )
507
+
508
  stop_btn.click(
509
  attendance_system.stop_processing,
510
+ inputs=None,
511
+ outputs=status
512
  )
513
+
514
+ reg_btn.click(
515
  attendance_system.register_worker_manual,
516
+ inputs=[reg_image, reg_name],
517
+ outputs=[reg_status, workers_list]
518
  )
519
+
520
+ refresh_workers.click(
521
+ attendance_system.get_registered_workers_info,
522
+ outputs=workers_list
523
  )
524
+
525
+ refresh_log.click(
526
+ lambda: "\n".join(reversed(attendance_system.session_log[-20:])) or "No entries",
527
+ outputs=session_log
528
+ )
529
+
530
  def update_ui():
531
  while True:
532
+ if attendance_system.error_message:
533
+ yield None, attendance_system.error_message
534
+ time.sleep(2)
535
+ attendance_system.error_message = None
536
+ continue
537
+
 
 
 
538
  if attendance_system.is_processing.is_set():
539
+ frame = None
540
+ try:
541
+ frame = attendance_system.frame_queue.get_nowait()
542
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if frame is not None else None
543
+ except queue.Empty:
544
+ pass
545
+
546
+ log = "\n".join(reversed(attendance_system.session_log[-20:])) or "Processing..."
547
+ yield frame, log
548
+ else:
549
+ if attendance_system.last_processed_frame is not None:
550
+ frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
551
+ log = "\n".join(reversed(attendance_system.final_log[-20:])) or "Processing complete"
552
+ yield frame, log
553
+ else:
554
+ yield None, "System ready"
555
+ time.sleep(0.03) # Faster UI updates
556
+
557
+ demo.load(update_ui, outputs=[video_output, session_log])
558
  return demo
559
 
560
  if __name__ == "__main__":
561
  app = create_interface()
562
  app.queue()
563
+ app.launch(server_name="0.0.0.0", server_port=7860)