PrashanthB461 commited on
Commit
fc67533
·
verified ·
1 Parent(s): 19078d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +15 -72
app.py CHANGED
@@ -47,15 +47,10 @@ SF_CREDENTIALS = {
47
  "domain": os.getenv("SF_DOMAIN", "login")
48
  }
49
 
50
- # --- MODIFICATION: Added configuration for performance and accuracy ---
51
- # Defines how many frames to skip between face analysis. Higher value = faster but less frequent detection.
52
  FRAME_PROCESS_RATE = 3
53
- # Stricter distance threshold for Facenet. Faces with distance > this are considered different people.
54
- # Default DeepFace threshold is ~10, which is too loose. A value between 0.4 and 0.7 is recommended for Facenet.
55
  FACE_MATCH_THRESHOLD = 0.6
56
- # Minimum confidence score from the face detector to consider a face for auto-registration.
57
  AUTO_REGISTER_CONFIDENCE = 0.99
58
- # --- END MODIFICATION ---
59
 
60
  # --- SALESFORCE CONNECTION ---
61
 
@@ -83,8 +78,8 @@ class AttendanceSystem:
83
  self.is_processing = threading.Event()
84
  self.frame_queue = queue.Queue(maxsize=10)
85
  self.error_message = None
86
- self.last_processed_frame = None # Holds the final frame after processing
87
- self.final_log = None # Holds the final log after processing
88
 
89
  # Data Storage
90
  self.known_face_embeddings: List[np.ndarray] = []
@@ -96,9 +91,7 @@ class AttendanceSystem:
96
  self.last_recognition_time = {}
97
  self.recognition_cooldown = 5
98
  self.session_log: List[str] = []
99
- # --- MODIFICATION: Set to track workers already marked present in the current session for unique UI logs ---
100
  self.session_attended_ids = set()
101
- # --- END MODIFICATION ---
102
 
103
  # Initialize
104
  self.sf = connect_to_salesforce()
@@ -124,7 +117,6 @@ class AttendanceSystem:
124
  temp_names.append(worker['Name'])
125
  temp_ids.append(worker['Worker_ID__c'])
126
  try:
127
- # Robustly extract number from Worker ID like "W0042"
128
  worker_num = int(''.join(filter(str.isdigit, worker['Worker_ID__c'])))
129
  if worker_num > max_id:
130
  max_id = worker_num
@@ -169,20 +161,17 @@ class AttendanceSystem:
169
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
170
  try:
171
  image_array = np.array(image)
172
- # Ensure a face exists before proceeding
173
  DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
174
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet', enforce_detection=False)[0]['embedding']
175
 
176
  if self._is_duplicate_face(embedding):
177
  return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
178
 
179
- # --- MODIFICATION: Ensure unique ID assignment is robust ---
180
  worker_id = f"W{self.next_worker_id:04d}"
181
  name = name.strip().title()
182
 
183
  self._add_worker_to_system(worker_id, name, embedding, image_array)
184
  self.save_local_worker_data()
185
- # self.load_worker_data() # Not strictly necessary, can rely on local update
186
  return f"✅ {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
187
  except ValueError:
188
  return "❌ No face detected in the image!", self.get_registered_workers_info()
@@ -213,7 +202,7 @@ class AttendanceSystem:
213
  self.known_face_embeddings.append(np.array(embedding))
214
  self.known_face_names.append(name)
215
  self.known_face_ids.append(worker_id)
216
- self.next_worker_id += 1 # This ensures the next ID is always unique
217
 
218
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
219
  face_pil.save(f"data/faces/{worker_id}.jpg")
@@ -232,21 +221,17 @@ class AttendanceSystem:
232
  """Checks if a new face embedding is too close to any known embeddings."""
233
  if not self.known_face_embeddings: return False
234
  new_embedding = np.array(embedding)
235
- # --- MODIFICATION: Using stricter threshold for duplicate check ---
236
  distances = [np.linalg.norm(new_embedding - known_embedding) for known_embedding in self.known_face_embeddings]
237
  return min(distances) < FACE_MATCH_THRESHOLD
238
- # --- END MODIFICATION ---
239
 
240
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
241
  """Marks attendance if not already marked in this session. Returns True if newly marked."""
242
- # --- MODIFICATION: Check session log first for immediate UI uniqueness ---
243
  if worker_id in self.session_attended_ids:
244
- return False # Already logged in this session
245
- # --- END MODIFICATION ---
246
 
247
  today_str = date.today().isoformat()
248
  if self._has_attended_today_in_sf(worker_id, today_str):
249
- self.session_attended_ids.add(worker_id) # Add to session log to prevent re-logging
250
  return False
251
 
252
  current_time = datetime.now()
@@ -258,7 +243,7 @@ class AttendanceSystem:
258
 
259
  log_msg = f"✅ [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
260
  self.session_log.append(log_msg)
261
- self.session_attended_ids.add(worker_id) # Mark as attended for this session
262
  return True
263
 
264
  def _has_attended_today_in_sf(self, worker_id: str, today_str: str) -> bool:
@@ -277,14 +262,11 @@ class AttendanceSystem:
277
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
278
  """Main function to process a single video frame."""
279
  try:
280
- # Using 'opencv' backend which is generally fastest
281
  face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='opencv', enforce_detection=False)
282
 
283
- # Iterate through each face found in the frame
284
  for face_obj in face_objs:
285
  confidence = face_obj['confidence']
286
 
287
- # Only process faces with a reasonable confidence score
288
  if confidence < 0.95:
289
  continue
290
 
@@ -294,52 +276,41 @@ class AttendanceSystem:
294
 
295
  if face_image.size == 0: continue
296
 
297
- # Get the embedding for the detected face
298
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
299
 
300
  if not self.known_face_embeddings:
301
- # If no workers are registered yet, attempt to register this one
302
  if confidence > AUTO_REGISTER_CONFIDENCE:
303
  self._register_worker_auto(face_image)
304
  continue
305
 
306
- # Compare the face to all known faces
307
  distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
308
  min_dist = min(distances)
309
- # --- MODIFICATION: Using the stricter threshold for matching ---
310
  match_index = distances.index(min_dist) if min_dist < FACE_MATCH_THRESHOLD else -1
311
- # --- END MODIFICATION ---
312
 
313
  worker_id, worker_name = None, "Unknown"
314
  color = (0, 0, 255) # Red for Unknown
315
 
316
  if match_index != -1:
317
- # --- KNOWN FACE FOUND ---
318
  worker_id = self.known_face_ids[match_index]
319
  worker_name = self.known_face_names[match_index]
320
  color = (0, 255, 0) # Green for Known/Present
321
  if self.mark_attendance(worker_id, worker_name):
322
  self.last_recognition_time[worker_id] = time.time()
323
  else:
324
- # --- UNKNOWN FACE ---
325
  color = (0, 165, 255) # Orange for newly registered
326
- # --- MODIFICATION: Only auto-register very clear faces ---
327
  if confidence > AUTO_REGISTER_CONFIDENCE:
328
  new_worker = self._register_worker_auto(face_image)
329
  if new_worker:
330
  worker_id, worker_name = new_worker
331
  if self.mark_attendance(worker_id, worker_name):
332
  self.last_recognition_time[worker_id] = time.time()
333
- # --- END MODIFICATION ---
334
 
335
- # Draw bounding box and label on the frame
336
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
337
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
338
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
339
 
340
  return frame
341
  except Exception as e:
342
- # Log error but don't crash the loop
343
  logger.error(f"ERROR in process_frame: {e}")
344
  return frame
345
 
@@ -351,32 +322,25 @@ class AttendanceSystem:
351
  self.is_processing.clear()
352
  return
353
 
354
- # --- MODIFICATION: Frame skipping for performance ---
355
  frame_count = 0
356
  last_annotated_frame = None
357
- # --- END MODIFICATION ---
358
 
359
  while self.is_processing.is_set():
360
  ret, frame = video_capture.read()
361
  if not ret: break
362
 
363
- # --- MODIFICATION: Implement frame skipping ---
364
  frame_count += 1
365
  if frame_count % FRAME_PROCESS_RATE == 0:
366
- # Process this frame fully
367
  processed_frame = self.process_frame(frame)
368
- last_annotated_frame = processed_frame.copy() # Save the annotated frame
369
  else:
370
- # For skipped frames, just use the last annotated frame to keep the UI responsive
371
  processed_frame = last_annotated_frame if last_annotated_frame is not None else frame
372
- # --- END MODIFICATION ---
373
 
374
  if not self.frame_queue.full():
375
  self.frame_queue.put(processed_frame)
376
 
377
- # Continuously update last frame to show at the end
378
  self.last_processed_frame = processed_frame
379
- time.sleep(0.01) # Small sleep to yield CPU
380
 
381
  self.final_log = self.session_log.copy()
382
  video_capture.release()
@@ -384,15 +348,13 @@ class AttendanceSystem:
384
 
385
  def start_processing(self, source) -> str:
386
  if self.is_processing.is_set(): return "⚠️ Processing is already active."
387
- # Reset states for the new session
388
  self.session_log.clear()
389
  self.last_recognition_time.clear()
390
  self.error_message = None
391
  self.last_processed_frame = None
392
  self.final_log = None
393
- # --- MODIFICATION: Clear the session attendance tracker ---
394
  self.session_attended_ids.clear()
395
- # --- END MODIFICATION ---
396
 
397
  self.is_processing.set()
398
  self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
@@ -404,7 +366,6 @@ class AttendanceSystem:
404
  if not self.is_processing.is_set():
405
  return "⚠️ Processing is not currently active."
406
  self.is_processing.clear()
407
- # Give the thread a moment to finish
408
  if self.processing_thread:
409
  self.processing_thread.join(timeout=2)
410
  return "✅ Processing stopped by user."
@@ -431,24 +392,20 @@ class AttendanceSystem:
431
  buffered = BytesIO()
432
  image.save(buffered, format="JPEG")
433
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
434
- # Create a ContentVersion (File)
435
  cv = self.sf.ContentVersion.create({
436
  'Title': f'Image_{worker_id}',
437
  'PathOnClient': f'{worker_id}.jpg',
438
  'VersionData': encoded_image,
439
- 'FirstPublishLocationId': record_id # Link to the Worker__c record
440
  })
441
- # To get a usable URL, you query the ContentDocumentLink
442
  content_doc_link = self.sf.query(f"SELECT ContentDocumentId FROM ContentDocumentLink WHERE LinkedEntityId = '{record_id}'")['records'][0]
443
  content_doc_id = content_doc_link['ContentDocumentId']
444
- # This relative URL can be used within Salesforce
445
  return f"/sfc/servlet.shepherd/document/download/{content_doc_id}"
446
  except Exception as e:
447
  logger.error(f"Salesforce image upload error: {e}")
448
  return None
449
 
450
  def get_registered_workers_info(self) -> str:
451
- # Refresh local data from Salesforce before displaying
452
  self.load_worker_data()
453
  if not self.known_face_ids: return "No workers registered."
454
 
@@ -522,10 +479,9 @@ def create_interface():
522
 
523
  def update_ui_generator():
524
  while True:
525
- # Handle error messages first
526
  if attendance_system.error_message:
527
  error = attendance_system.error_message
528
- attendance_system.error_message = None # Clear after sending
529
  yield None, error, f"Status: {error}"
530
  time.sleep(2)
531
  continue
@@ -535,21 +491,18 @@ def create_interface():
535
 
536
  if attendance_system.is_processing.is_set():
537
  try:
538
- # Non-blocking get from the queue
539
  if not attendance_system.frame_queue.empty():
540
  frame_bgr = attendance_system.frame_queue.get_nowait()
541
  if frame_bgr is not None:
542
  frame = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
543
  except queue.Empty:
544
- pass # No new frame, just update log
545
  yield frame, log_md, "Status: Processing..."
546
  else:
547
- # Processing has stopped
548
  final_frame = None
549
  if attendance_system.last_processed_frame is not None:
550
  final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
551
 
552
- final_log_md = "Processing complete. Here is the final log:"
553
  if attendance_system.final_log:
554
  final_log_md = "\n".join(reversed(attendance_system.final_log))
555
  else:
@@ -557,22 +510,12 @@ def create_interface():
557
 
558
  yield final_frame, final_log_md, "Status: Stopped. Go to 'Controls & Status' to start."
559
 
560
- time.sleep(0.1) # UI update frequency
561
-
562
- # Use a separate generator for status to avoid conflicts
563
- def update_status_generator():
564
- while True:
565
- if attendance_system.is_processing.is_set():
566
- yield "Status: Processing..."
567
- else:
568
- yield "Status: Stopped. Go to 'Controls & Status' to start."
569
- time.sleep(1)
570
 
571
  # Bind the generator to update the UI components
572
  demo.load(
573
  fn=update_ui_generator,
574
- outputs=[video_output, session_log_display, status_box],
575
- every=0.1
576
  )
577
 
578
  return demo
 
47
  "domain": os.getenv("SF_DOMAIN", "login")
48
  }
49
 
50
+ # Configuration for performance and accuracy
 
51
  FRAME_PROCESS_RATE = 3
 
 
52
  FACE_MATCH_THRESHOLD = 0.6
 
53
  AUTO_REGISTER_CONFIDENCE = 0.99
 
54
 
55
  # --- SALESFORCE CONNECTION ---
56
 
 
78
  self.is_processing = threading.Event()
79
  self.frame_queue = queue.Queue(maxsize=10)
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] = []
 
91
  self.last_recognition_time = {}
92
  self.recognition_cooldown = 5
93
  self.session_log: List[str] = []
 
94
  self.session_attended_ids = set()
 
95
 
96
  # Initialize
97
  self.sf = connect_to_salesforce()
 
117
  temp_names.append(worker['Name'])
118
  temp_ids.append(worker['Worker_ID__c'])
119
  try:
 
120
  worker_num = int(''.join(filter(str.isdigit, worker['Worker_ID__c'])))
121
  if worker_num > max_id:
122
  max_id = worker_num
 
161
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
162
  try:
163
  image_array = np.array(image)
 
164
  DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
165
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet', enforce_detection=False)[0]['embedding']
166
 
167
  if self._is_duplicate_face(embedding):
168
  return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
169
 
 
170
  worker_id = f"W{self.next_worker_id:04d}"
171
  name = name.strip().title()
172
 
173
  self._add_worker_to_system(worker_id, name, embedding, image_array)
174
  self.save_local_worker_data()
 
175
  return f"✅ {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
176
  except ValueError:
177
  return "❌ No face detected in the image!", self.get_registered_workers_info()
 
202
  self.known_face_embeddings.append(np.array(embedding))
203
  self.known_face_names.append(name)
204
  self.known_face_ids.append(worker_id)
205
+ self.next_worker_id += 1
206
 
207
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
208
  face_pil.save(f"data/faces/{worker_id}.jpg")
 
221
  """Checks if a new face embedding is too close to any known embeddings."""
222
  if not self.known_face_embeddings: return False
223
  new_embedding = np.array(embedding)
 
224
  distances = [np.linalg.norm(new_embedding - known_embedding) for known_embedding in self.known_face_embeddings]
225
  return min(distances) < FACE_MATCH_THRESHOLD
 
226
 
227
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
228
  """Marks attendance if not already marked in this session. Returns True if newly marked."""
 
229
  if worker_id in self.session_attended_ids:
230
+ return False
 
231
 
232
  today_str = date.today().isoformat()
233
  if self._has_attended_today_in_sf(worker_id, today_str):
234
+ self.session_attended_ids.add(worker_id)
235
  return False
236
 
237
  current_time = datetime.now()
 
243
 
244
  log_msg = f"✅ [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
245
  self.session_log.append(log_msg)
246
+ self.session_attended_ids.add(worker_id)
247
  return True
248
 
249
  def _has_attended_today_in_sf(self, worker_id: str, today_str: str) -> bool:
 
262
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
263
  """Main function to process a single video frame."""
264
  try:
 
265
  face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='opencv', enforce_detection=False)
266
 
 
267
  for face_obj in face_objs:
268
  confidence = face_obj['confidence']
269
 
 
270
  if confidence < 0.95:
271
  continue
272
 
 
276
 
277
  if face_image.size == 0: continue
278
 
 
279
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
280
 
281
  if not self.known_face_embeddings:
 
282
  if confidence > AUTO_REGISTER_CONFIDENCE:
283
  self._register_worker_auto(face_image)
284
  continue
285
 
 
286
  distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
287
  min_dist = min(distances)
 
288
  match_index = distances.index(min_dist) if min_dist < FACE_MATCH_THRESHOLD else -1
 
289
 
290
  worker_id, worker_name = None, "Unknown"
291
  color = (0, 0, 255) # Red for Unknown
292
 
293
  if match_index != -1:
 
294
  worker_id = self.known_face_ids[match_index]
295
  worker_name = self.known_face_names[match_index]
296
  color = (0, 255, 0) # Green for Known/Present
297
  if self.mark_attendance(worker_id, worker_name):
298
  self.last_recognition_time[worker_id] = time.time()
299
  else:
 
300
  color = (0, 165, 255) # Orange for newly registered
 
301
  if confidence > AUTO_REGISTER_CONFIDENCE:
302
  new_worker = self._register_worker_auto(face_image)
303
  if new_worker:
304
  worker_id, worker_name = new_worker
305
  if self.mark_attendance(worker_id, worker_name):
306
  self.last_recognition_time[worker_id] = time.time()
 
307
 
 
308
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
309
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
310
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
311
 
312
  return frame
313
  except Exception as e:
 
314
  logger.error(f"ERROR in process_frame: {e}")
315
  return frame
316
 
 
322
  self.is_processing.clear()
323
  return
324
 
 
325
  frame_count = 0
326
  last_annotated_frame = None
 
327
 
328
  while self.is_processing.is_set():
329
  ret, frame = video_capture.read()
330
  if not ret: break
331
 
 
332
  frame_count += 1
333
  if frame_count % FRAME_PROCESS_RATE == 0:
 
334
  processed_frame = self.process_frame(frame)
335
+ last_annotated_frame = processed_frame.copy()
336
  else:
 
337
  processed_frame = last_annotated_frame if last_annotated_frame is not None else frame
 
338
 
339
  if not self.frame_queue.full():
340
  self.frame_queue.put(processed_frame)
341
 
 
342
  self.last_processed_frame = processed_frame
343
+ time.sleep(0.01)
344
 
345
  self.final_log = self.session_log.copy()
346
  video_capture.release()
 
348
 
349
  def start_processing(self, source) -> str:
350
  if self.is_processing.is_set(): return "⚠️ Processing is already active."
351
+
352
  self.session_log.clear()
353
  self.last_recognition_time.clear()
354
  self.error_message = None
355
  self.last_processed_frame = None
356
  self.final_log = None
 
357
  self.session_attended_ids.clear()
 
358
 
359
  self.is_processing.set()
360
  self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,))
 
366
  if not self.is_processing.is_set():
367
  return "⚠️ Processing is not currently active."
368
  self.is_processing.clear()
 
369
  if self.processing_thread:
370
  self.processing_thread.join(timeout=2)
371
  return "✅ Processing stopped by user."
 
392
  buffered = BytesIO()
393
  image.save(buffered, format="JPEG")
394
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
 
395
  cv = self.sf.ContentVersion.create({
396
  'Title': f'Image_{worker_id}',
397
  'PathOnClient': f'{worker_id}.jpg',
398
  'VersionData': encoded_image,
399
+ 'FirstPublishLocationId': record_id
400
  })
 
401
  content_doc_link = self.sf.query(f"SELECT ContentDocumentId FROM ContentDocumentLink WHERE LinkedEntityId = '{record_id}'")['records'][0]
402
  content_doc_id = content_doc_link['ContentDocumentId']
 
403
  return f"/sfc/servlet.shepherd/document/download/{content_doc_id}"
404
  except Exception as e:
405
  logger.error(f"Salesforce image upload error: {e}")
406
  return None
407
 
408
  def get_registered_workers_info(self) -> str:
 
409
  self.load_worker_data()
410
  if not self.known_face_ids: return "No workers registered."
411
 
 
479
 
480
  def update_ui_generator():
481
  while True:
 
482
  if attendance_system.error_message:
483
  error = attendance_system.error_message
484
+ attendance_system.error_message = None
485
  yield None, error, f"Status: {error}"
486
  time.sleep(2)
487
  continue
 
491
 
492
  if attendance_system.is_processing.is_set():
493
  try:
 
494
  if not attendance_system.frame_queue.empty():
495
  frame_bgr = attendance_system.frame_queue.get_nowait()
496
  if frame_bgr is not None:
497
  frame = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
498
  except queue.Empty:
499
+ pass
500
  yield frame, log_md, "Status: Processing..."
501
  else:
 
502
  final_frame = None
503
  if attendance_system.last_processed_frame is not None:
504
  final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
505
 
 
506
  if attendance_system.final_log:
507
  final_log_md = "\n".join(reversed(attendance_system.final_log))
508
  else:
 
510
 
511
  yield final_frame, final_log_md, "Status: Stopped. Go to 'Controls & Status' to start."
512
 
513
+ time.sleep(0.1)
 
 
 
 
 
 
 
 
 
514
 
515
  # Bind the generator to update the UI components
516
  demo.load(
517
  fn=update_ui_generator,
518
+ outputs=[video_output, session_log_display, status_box]
 
519
  )
520
 
521
  return demo