PrashanthB461 commited on
Commit
fcd149a
·
verified ·
1 Parent(s): 87ef316

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +300 -198
app.py CHANGED
@@ -72,14 +72,10 @@ class AttendanceSystem:
72
  self.processing_thread = None
73
  self.is_processing = threading.Event()
74
  self.frame_queue = queue.Queue(maxsize=10)
75
- self.error_message = None
76
  self.last_processed_frame = None
77
  self.final_log = None
78
 
79
- # Recognition Parameters
80
- self.RECOGNITION_THRESHOLD = 1.1 # Strict threshold for a confident match.
81
- self.DUPLICATE_THRESHOLD = 1.4 # More lenient threshold to prevent re-registering known faces.
82
-
83
  # Data Storage
84
  self.known_face_embeddings: List[np.ndarray] = []
85
  self.known_face_names: List[str] = []
@@ -87,8 +83,11 @@ class AttendanceSystem:
87
  self.next_worker_id: int = 1
88
 
89
  # Session Tracking
 
 
90
  self.session_log: List[str] = []
91
- self.session_logged_ids = set()
 
92
 
93
  # Initialize
94
  self.sf = connect_to_salesforce()
@@ -99,7 +98,6 @@ class AttendanceSystem:
99
  os.makedirs("data/faces", exist_ok=True)
100
 
101
  def load_worker_data(self):
102
- """Loads worker data from Salesforce or a local file at startup."""
103
  logger.info("Loading worker data...")
104
  if self.sf:
105
  try:
@@ -125,7 +123,7 @@ class AttendanceSystem:
125
  self.known_face_names = temp_names
126
  self.known_face_ids = temp_ids
127
  self.next_worker_id = max_id + 1
128
- self.save_local_worker_data() # Create a local backup
129
  logger.info(f"✅ Loaded {len(self.known_face_ids)} workers from Salesforce.")
130
  except Exception as e:
131
  logger.error(f"❌ Error loading from Salesforce: {e}. Attempting local load.")
@@ -137,7 +135,8 @@ class AttendanceSystem:
137
  def _load_local_worker_data(self):
138
  try:
139
  if os.path.exists("data/workers.pkl"):
140
- with open("data/workers.pkl", "rb") as f: data = pickle.load(f)
 
141
  self.known_face_embeddings = data.get("embeddings", [])
142
  self.known_face_names = data.get("names", [])
143
  self.known_face_ids = data.get("ids", [])
@@ -147,12 +146,11 @@ class AttendanceSystem:
147
  logger.error(f"❌ Error loading local data: {e}")
148
 
149
  def save_local_worker_data(self):
150
- """Saves the current in-memory worker database to a local pickle file."""
151
  try:
152
  worker_data = {
153
- "embeddings": self.known_face_embeddings,
154
- "names": self.known_face_names,
155
- "ids": self.known_face_ids,
156
  "next_id": self.next_worker_id
157
  }
158
  with open("data/workers.pkl", "wb") as f:
@@ -166,181 +164,230 @@ class AttendanceSystem:
166
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
167
  try:
168
  image_array = np.array(image)
169
- embedding = DeepFace.represent(img_path=image_array, model_name='Facenet', enforce_detection=True)[0]['embedding']
170
-
171
  if self._is_duplicate_face(embedding):
172
- return "❌ Face matches an existing worker!", self.get_registered_workers_info()
173
 
174
  worker_id = f"W{self.next_worker_id:04d}"
175
  name = name.strip().title()
176
-
177
- # Instantly update memory
178
- self.known_face_ids.append(worker_id)
179
- self.known_face_names.append(name)
180
- self.known_face_embeddings.append(np.array(embedding))
181
- self.next_worker_id += 1
182
-
183
- # Sync to backend and save
184
- self._sync_worker_to_backend(worker_id, name, embedding, image_array)
185
  self.save_local_worker_data()
186
-
187
  return f"✅ {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
188
  except ValueError:
189
  return "❌ No face detected in the image!", self.get_registered_workers_info()
190
  except Exception as e:
191
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
192
 
193
- def _register_worker_auto(self, face_image: np.ndarray, embedding: List[float]) -> Optional[Tuple[str, str]]:
194
- """Instantly adds a new worker to memory and then syncs to the backend."""
195
  try:
196
- # Check for duplicates using the embedding that was already calculated
197
- if self._is_duplicate_face(embedding):
198
  return None
199
-
200
- # STEP 1: Update in-memory data INSTANTLY. This is the critical fix.
201
  worker_id = f"W{self.next_worker_id:04d}"
202
  worker_name = f"Unknown Worker {self.next_worker_id}"
 
 
203
 
204
- self.known_face_ids.append(worker_id)
205
- self.known_face_names.append(worker_name)
206
- self.known_face_embeddings.append(np.array(embedding))
207
- self.next_worker_id += 1
208
 
209
- logger.info(f"Added {worker_name} to memory. Now syncing to backend.")
210
-
211
- # STEP 2: Sync to backend (Salesforce, local files). This can be slow but won't block the next frame.
212
- self._sync_worker_to_backend(worker_id, worker_name, embedding, face_image)
213
- self.save_local_worker_data()
214
 
215
  return worker_id, worker_name
216
  except Exception as e:
217
  logger.error(f"❌ Auto-registration error: {e}")
218
  return None
219
 
220
- def _sync_worker_to_backend(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
221
- """Saves worker image and syncs data to Salesforce if available."""
 
 
 
 
 
222
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
223
  face_pil.save(f"data/faces/{worker_id}.jpg")
 
 
224
  caption = self._get_image_caption(face_pil)
225
  if self.sf:
226
  try:
227
- worker_record = self.sf.Worker__c.create({'Name': name, 'Worker_ID__c': worker_id, 'Face_Embedding__c': json.dumps(embedding), 'Image_Caption__c': caption})
 
 
 
 
 
228
  image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
229
- if image_url: self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
 
230
  logger.info(f"✅ Worker {worker_id} synced to Salesforce.")
231
  except Exception as e:
232
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
233
 
234
- def _is_duplicate_face(self, embedding: List[float]) -> bool:
235
- """Checks if a face is too similar to an already registered one."""
236
- if not self.known_face_embeddings: return False
237
  distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
238
- # Use the looser threshold to avoid re-registering known faces at different angles
239
- return min(distances) < self.DUPLICATE_THRESHOLD
240
 
241
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
242
- """Marks attendance in Salesforce if not already done today. Returns True if newly marked."""
243
- today_str = date.today().isoformat()
244
- if self._has_attended_today(worker_id, today_str):
245
  return False
246
-
 
 
 
 
 
247
  current_time = datetime.now()
 
 
248
  if self.sf:
249
  try:
250
- self.sf.Attendance__c.create({'Worker_ID__c': worker_id, 'Name__c': worker_name, 'Date__c': today_str, 'Timestamp__c': current_time.isoformat(), 'Status__c': "Present"})
251
- logger.info(f"Salesforce attendance marked for {worker_id}")
252
- return True
 
 
 
 
253
  except Exception as e:
254
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
255
  return False
256
- return True # Assume success if not connected to Salesforce
 
 
 
 
 
 
 
257
 
258
- def _has_attended_today(self, worker_id: str, today_str: str) -> bool:
259
- """Checks Salesforce to see if an attendance record exists for the worker today."""
260
  if self.sf:
261
  try:
262
- if self.sf.query(f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = {today_str}")['totalSize'] > 0:
263
- return True
264
- except Exception: pass
 
265
  return False
266
 
267
  # --- Video Processing ---
268
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
 
 
 
269
  try:
270
- face_objs = DeepFace.extract_faces(img_path=frame, detector_backend='opencv', enforce_detection=False)
271
-
 
 
 
 
 
 
 
 
 
272
  for face_obj in face_objs:
273
- if face_obj['confidence'] < 0.95: continue
 
 
 
 
274
 
275
  facial_area = face_obj['facial_area']
276
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
277
- face_image = frame[y:y+h, x:x+w]
278
 
279
- if face_image.size == 0: continue
 
280
 
281
- color, label_text = (0, 0, 255), "Unknown" # Default to Red/Unknown
282
-
283
  try:
284
- embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
- min_dist, match_index = -1, -1
287
- if self.known_face_embeddings:
288
- distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
289
- if distances:
290
- min_dist = min(distances)
291
- match_index = distances.index(min_dist)
292
-
293
- # CASE 1: CONFIDENT MATCH (Recognized Worker)
294
- if match_index != -1 and min_dist < self.RECOGNITION_THRESHOLD:
295
  worker_id = self.known_face_ids[match_index]
296
  worker_name = self.known_face_names[match_index]
297
- color, label_text = (0, 255, 0), f"{worker_name} ({worker_id})" # Green
298
 
299
- if worker_id not in self.session_logged_ids:
300
- if self.mark_attendance(worker_id, worker_name):
301
- log_msg = f"✅ [{datetime.now().strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
302
- self.session_log.append(log_msg)
303
- self.session_logged_ids.add(worker_id)
304
-
305
- # CASE 2: NEW FACE (Attempt to register)
306
- else:
307
- # Pass the calculated embedding to avoid doing it twice
308
- new_worker = self._register_worker_auto(face_image, embedding)
309
- if new_worker:
310
- worker_id, worker_name = new_worker
311
- color, label_text = (0, 165, 255), f"{worker_name} ({worker_id})" # Orange
312
-
313
- if worker_id not in self.session_logged_ids:
314
- log_msg = f"🆕 [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
315
- self.session_log.append(log_msg)
316
- self.mark_attendance(worker_id, worker_name)
317
- self.session_logged_ids.add(worker_id)
318
- # Note: If _register_worker_auto returns None (because it was a duplicate), the box remains red.
319
- except Exception as e:
320
- logger.debug(f"Could not process a face. Error: {e}")
321
-
322
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
323
- cv2.putText(frame, label_text, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
324
-
325
  return frame
326
  except Exception as e:
327
- logger.error(f"CRITICAL ERROR in process_frame: {e}")
328
  return frame
329
 
330
  def _processing_loop(self, source):
331
  video_capture = cv2.VideoCapture(source)
332
  if not video_capture.isOpened():
333
- self.error_message = "❌ **Error:** Could not open video source."
 
334
  self.is_processing.clear()
335
  return
336
 
 
 
 
 
 
 
337
  while self.is_processing.is_set():
338
  ret, frame = video_capture.read()
339
- if not ret: break
340
-
 
341
  processed_frame = self.process_frame(frame)
342
- if not self.frame_queue.full(): self.frame_queue.put(processed_frame)
343
 
 
 
 
344
  self.last_processed_frame = processed_frame
345
  time.sleep(0.05)
346
 
@@ -349,141 +396,196 @@ class AttendanceSystem:
349
  self.is_processing.clear()
350
 
351
  def start_processing(self, source) -> str:
352
- if self.is_processing.is_set(): return "⚠️ Processing is already active."
353
- self.session_log.clear()
354
- self.session_logged_ids.clear()
355
- self.error_message = None; self.last_processed_frame = None; self.final_log = None
356
  self.is_processing.set()
357
- self.processing_thread = threading.Thread(target=self._processing_loop, args=(source,)); self.processing_thread.daemon = True
 
 
 
 
358
  self.processing_thread.start()
359
- return "✅ Started processing..."
360
 
361
  def stop_processing(self) -> str:
362
  self.is_processing.clear()
363
- return "✅ Processing stopped by user."
364
 
365
- # --- Helper & Reporting ---
366
  def _get_image_caption(self, image: Image.Image) -> str:
367
- # ... (rest of this function is unchanged)
368
- if not HF_API_TOKEN: return "Hugging Face API token not configured."
369
  try:
370
  buffered = BytesIO()
371
  image.save(buffered, format="JPEG")
372
- img_data = buffered.getvalue()
373
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
374
- response = requests.post(HF_API_URL, headers=headers, data=img_data)
375
  response.raise_for_status()
376
- result = response.json()
377
- return result[0].get("generated_text", "No caption found.")
378
  except Exception as e:
379
  logger.error(f"Hugging Face API error: {e}")
380
- return "Caption generation failed."
381
 
382
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
383
- # ... (rest of this function is unchanged)
384
- if not self.sf: return None
385
  try:
386
  buffered = BytesIO()
387
  image.save(buffered, format="JPEG")
388
- encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
389
- cv = self.sf.ContentVersion.create({'Title': f'Image_{worker_id}', 'PathOnClient': f'{worker_id}.jpg', 'VersionData': encoded_image, 'FirstPublishLocationId': record_id})
390
- return f"/{cv['id']}" # Relative URL
 
 
 
 
 
391
  except Exception as e:
392
  logger.error(f"Salesforce image upload error: {e}")
393
  return None
394
 
395
  def get_registered_workers_info(self) -> str:
396
- # ... (rest of this function is unchanged)
397
- if not self.sf: return "❌ Salesforce not connected."
398
  try:
399
  records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
400
- if not records: return "No workers registered."
401
- return f"**👥 Registered Workers ({len(records)})**\n" + "\n".join([f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records])
402
- except Exception as e: return f"Error: {e}"
403
-
404
- # --- GRADIO UI (Unchanged) ---
 
 
 
 
405
  attendance_system = AttendanceSystem()
 
406
  def create_interface():
407
- # ... (The entire Gradio UI section remains the same as your original code) ...
408
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
409
- gr.Markdown("# 🎯 Advanced Face Recognition Attendance System")
 
410
  with gr.Tabs():
411
- with gr.Tab("⚙️ Controls & Status"):
412
- gr.Markdown("### 1. Choose Input Source & Start Processing")
413
  with gr.Row():
414
- with gr.Column(scale=1):
415
- selected_tab_index = gr.Number(value=0, visible=False)
416
- with gr.Tabs() as video_tabs:
417
- with gr.Tab("Live Camera", id=0):
418
- camera_source = gr.Number(label="Camera Source", value=0, precision=0)
419
- with gr.Tab("Upload Video", id=1):
420
- video_file = gr.Video(label="Upload Video File", sources=["upload"])
421
- with gr.Column(scale=1):
422
- start_btn = gr.Button("▶️ Start Processing", variant="primary")
423
- stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
424
- status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
425
- gr.Markdown("### 2. View Results in the 'Output & Log' Tab")
426
- gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
 
428
- with gr.Tab("📊 Output & Log"):
429
- with gr.Row():
430
- with gr.Column(scale=2):
431
- video_output = gr.Image(label="Recognition Output", interactive=False)
432
- with gr.Column(scale=1):
433
- session_log_display = gr.Markdown(label="📋 Session Log", value="System is ready.")
434
-
435
  with gr.Tab("👤 Worker Management"):
436
  with gr.Row():
437
  with gr.Column():
438
- register_image = gr.Image(label="Upload Worker's Photo", type="pil")
439
- register_name = gr.Textbox(label="Worker's Full Name")
440
- register_btn = gr.Button("Register Worker", variant="primary")
441
- register_output = gr.Textbox(label="Registration Status", interactive=False)
442
  with gr.Column():
443
- registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
444
- refresh_workers_btn = gr.Button("🔄 Refresh List")
445
-
446
- # --- Event Handlers ---
447
- def on_tab_select(evt: gr.SelectData): return evt.index
448
- video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
449
-
450
- def start_wrapper(tab_index, cam_src, vid_path):
451
- source = int(cam_src) if tab_index == 0 else vid_path
452
- return "Please provide an input source." if source is None else attendance_system.start_processing(source)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
- start_btn.click(fn=start_wrapper, inputs=[selected_tab_index, camera_source, video_file], outputs=[status_box])
455
- stop_btn.click(fn=attendance_system.stop_processing, inputs=None, outputs=[status_box])
456
- register_btn.click(fn=attendance_system.register_worker_manual, inputs=[register_image, register_name], outputs=[register_output, registered_workers_info])
457
- refresh_workers_btn.click(fn=attendance_system.get_registered_workers_info, outputs=[registered_workers_info])
458
 
459
- def update_ui_generator():
460
  while True:
461
  if attendance_system.error_message:
462
- yield None, attendance_system.error_message
463
- time.sleep(2); attendance_system.error_message = None
 
464
  continue
465
-
466
  if attendance_system.is_processing.is_set():
467
- frame, log_md = None, "\n".join(reversed(attendance_system.session_log)) or "Processing..."
468
- try:
469
- if not attendance_system.frame_queue.empty():
470
- frame = attendance_system.frame_queue.get_nowait()
471
- if frame is not None: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
472
- except queue.Empty: pass
473
- yield frame, log_md
 
474
  else:
475
  if attendance_system.last_processed_frame is not None:
476
  final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
477
- final_log_md = "\n".join(reversed(attendance_system.final_log)) if attendance_system.final_log else "Processing complete. No log entries."
478
- yield final_frame, final_log_md
479
  else:
480
- yield None, "System stopped. Go to 'Controls & Status' to start."
481
- time.sleep(0.1)
482
-
483
- demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display])
 
 
 
 
 
484
  return demo
485
 
486
  if __name__ == "__main__":
487
  app = create_interface()
488
  app.queue()
489
- app.launch(server_name="0.0.0.0", server_port=7860, show_error=True, debug=True)
 
72
  self.processing_thread = None
73
  self.is_processing = threading.Event()
74
  self.frame_queue = queue.Queue(maxsize=10)
75
+ self.error_message = None
76
  self.last_processed_frame = None
77
  self.final_log = None
78
 
 
 
 
 
79
  # Data Storage
80
  self.known_face_embeddings: List[np.ndarray] = []
81
  self.known_face_names: List[str] = []
 
83
  self.next_worker_id: int = 1
84
 
85
  # Session Tracking
86
+ self.last_recognition_time = {}
87
+ self.recognition_cooldown = 5
88
  self.session_log: List[str] = []
89
+ self.today_attendance = set()
90
+ self.today_str = date.today().isoformat()
91
 
92
  # Initialize
93
  self.sf = connect_to_salesforce()
 
98
  os.makedirs("data/faces", exist_ok=True)
99
 
100
  def load_worker_data(self):
 
101
  logger.info("Loading worker data...")
102
  if self.sf:
103
  try:
 
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 from Salesforce.")
128
  except Exception as e:
129
  logger.error(f"❌ Error loading from Salesforce: {e}. Attempting local load.")
 
135
  def _load_local_worker_data(self):
136
  try:
137
  if os.path.exists("data/workers.pkl"):
138
+ with open("data/workers.pkl", "rb") as f:
139
+ data = pickle.load(f)
140
  self.known_face_embeddings = data.get("embeddings", [])
141
  self.known_face_names = data.get("names", [])
142
  self.known_face_ids = data.get("ids", [])
 
146
  logger.error(f"❌ Error loading local data: {e}")
147
 
148
  def save_local_worker_data(self):
 
149
  try:
150
  worker_data = {
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
  }
156
  with open("data/workers.pkl", "wb") as f:
 
164
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
165
  try:
166
  image_array = np.array(image)
167
+ DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
168
+ embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
169
  if self._is_duplicate_face(embedding):
170
+ return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
171
 
172
  worker_id = f"W{self.next_worker_id:04d}"
173
  name = name.strip().title()
174
+ self._add_worker_to_system(worker_id, name, embedding, image_array)
 
 
 
 
 
 
 
 
175
  self.save_local_worker_data()
176
+ self.load_worker_data()
177
  return f"✅ {name} registered with ID: {worker_id}!", self.get_registered_workers_info()
178
  except ValueError:
179
  return "❌ No face detected in the image!", self.get_registered_workers_info()
180
  except Exception as e:
181
  return f"❌ Registration error: {e}", self.get_registered_workers_info()
182
 
183
+ def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
 
184
  try:
185
+ embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
186
+ if self._is_duplicate_face(embedding):
187
  return None
188
+
 
189
  worker_id = f"W{self.next_worker_id:04d}"
190
  worker_name = f"Unknown Worker {self.next_worker_id}"
191
+ self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
192
+ self.save_local_worker_data()
193
 
194
+ log_msg = f"🆕 [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
195
+ self.session_log.append(log_msg)
196
+ logger.info(log_msg)
 
197
 
198
+ # Mark attendance immediately after registration
199
+ self.mark_attendance(worker_id, worker_name)
 
 
 
200
 
201
  return worker_id, worker_name
202
  except Exception as e:
203
  logger.error(f"❌ Auto-registration error: {e}")
204
  return None
205
 
206
+ def _add_worker_to_system(self, worker_id: str, name: str, embedding: List[float], image_array: np.ndarray):
207
+ self.known_face_embeddings.append(np.array(embedding))
208
+ self.known_face_names.append(name)
209
+ self.known_face_ids.append(worker_id)
210
+ self.next_worker_id += 1
211
+
212
+ # Save face image
213
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
214
  face_pil.save(f"data/faces/{worker_id}.jpg")
215
+
216
+ # Generate caption and sync to Salesforce
217
  caption = self._get_image_caption(face_pil)
218
  if self.sf:
219
  try:
220
+ worker_record = self.sf.Worker__c.create({
221
+ 'Name': name,
222
+ 'Worker_ID__c': worker_id,
223
+ 'Face_Embedding__c': json.dumps(embedding),
224
+ 'Image_Caption__c': caption
225
+ })
226
  image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
227
+ if image_url:
228
+ self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
229
  logger.info(f"✅ Worker {worker_id} synced to Salesforce.")
230
  except Exception as e:
231
  logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
232
 
233
+ def _is_duplicate_face(self, embedding: List[float], threshold: float = 8.0) -> bool:
234
+ if not self.known_face_embeddings:
235
+ return False
236
  distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
237
+ return min(distances) < threshold
 
238
 
239
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
240
+ # Check if already marked today
241
+ if worker_id in self.today_attendance:
 
242
  return False
243
+
244
+ # Check Salesforce if not in session cache
245
+ if self._has_attended_today(worker_id):
246
+ self.today_attendance.add(worker_id)
247
+ return False
248
+
249
  current_time = datetime.now()
250
+
251
+ # Log to Salesforce if connected
252
  if self.sf:
253
  try:
254
+ self.sf.Attendance__c.create({
255
+ 'Worker_ID__c': worker_id,
256
+ 'Name__c': worker_name,
257
+ 'Date__c': self.today_str,
258
+ 'Timestamp__c': current_time.isoformat(),
259
+ 'Status__c': "Present"
260
+ })
261
  except Exception as e:
262
  logger.error(f"❌ Error saving attendance to Salesforce: {e}")
263
  return False
264
+
265
+ # Add to session log and tracking
266
+ log_msg = f"✅ [{current_time.strftime('%H:%M:%S')}] Marked Present: {worker_name} ({worker_id})"
267
+ self.session_log.append(log_msg)
268
+ self.today_attendance.add(worker_id)
269
+ self.last_recognition_time[worker_id] = time.time()
270
+
271
+ return True
272
 
273
+ def _has_attended_today(self, worker_id: str) -> bool:
 
274
  if self.sf:
275
  try:
276
+ query = f"SELECT Id FROM Attendance__c WHERE Worker_ID__c = '{worker_id}' AND Date__c = '{self.today_str}'"
277
+ return self.sf.query(query)['totalSize'] > 0
278
+ except Exception as e:
279
+ logger.error(f"❌ Error checking attendance in Salesforce: {e}")
280
  return False
281
 
282
  # --- Video Processing ---
283
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
284
+ """
285
+ Process a single video frame with reliable face recognition and attendance marking.
286
+ """
287
  try:
288
+ # Convert to RGB for better face detection
289
+ rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
290
+
291
+ # Detect faces with high confidence
292
+ face_objs = DeepFace.extract_faces(
293
+ img_path=rgb_frame,
294
+ detector_backend='opencv',
295
+ enforce_detection=False,
296
+ align=True,
297
+ target_size=(160, 160) # Standard size for Facenet
298
+
299
  for face_obj in face_objs:
300
+ confidence = face_obj['confidence']
301
+
302
+ # Skip low confidence detections
303
+ if confidence < 0.95:
304
+ continue
305
 
306
  facial_area = face_obj['facial_area']
307
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
308
+ face_image = rgb_frame[y:y+h, x:x+w]
309
 
310
+ if face_image.size == 0:
311
+ continue
312
 
313
+ # Get face embedding
 
314
  try:
315
+ embedding_obj = DeepFace.represent(
316
+ img_path=face_image,
317
+ model_name='Facenet',
318
+ enforce_detection=False,
319
+ normalization='base'
320
+ )
321
+ embedding = np.array(embedding_obj[0]['embedding'])
322
+ except Exception as e:
323
+ logger.error(f"Face embedding error: {e}")
324
+ continue
325
+
326
+ # Initialize recognition variables
327
+ recognized = False
328
+ worker_id = None
329
+ worker_name = "Unknown"
330
+ color = (0, 0, 255) # Default red for unknown
331
+
332
+ # Compare with known faces if any exist
333
+ if self.known_face_embeddings:
334
+ distances = [np.linalg.norm(embedding - known) for known in self.known_face_embeddings]
335
+ min_dist = min(distances) if distances else float('inf')
336
+ match_index = np.argmin(distances) if distances else -1
337
 
338
+ # Check if match is good enough
339
+ if min_dist < 8.0: # Lower threshold for stricter matching
340
+ recognized = True
 
 
 
 
 
 
341
  worker_id = self.known_face_ids[match_index]
342
  worker_name = self.known_face_names[match_index]
343
+ color = (0, 255, 0) # Green for known workers
344
 
345
+ # Mark attendance if not already done
346
+ if worker_id not in self.today_attendance:
347
+ self.mark_attendance(worker_id, worker_name)
348
+
349
+ # Handle unknown faces (potential new workers)
350
+ if not recognized and w > 80 and h > 80: # Minimum face size
351
+ # Attempt auto-registration
352
+ new_worker = self._register_worker_auto(face_image)
353
+ if new_worker:
354
+ worker_id, worker_name = new_worker
355
+ color = (0, 165, 255) # Orange for new workers
356
+
357
+ # Draw rectangle and label
358
+ label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
 
 
 
 
 
 
 
 
 
359
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
360
+ cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
361
+
362
  return frame
363
  except Exception as e:
364
+ logger.error(f"Error in process_frame: {e}")
365
  return frame
366
 
367
  def _processing_loop(self, source):
368
  video_capture = cv2.VideoCapture(source)
369
  if not video_capture.isOpened():
370
+ err_msg = f"❌ Could not open video source {source}"
371
+ self.error_message = err_msg
372
  self.is_processing.clear()
373
  return
374
 
375
+ # Reset for new session
376
+ self.today_attendance.clear()
377
+ self.session_log.clear()
378
+ self.last_recognition_time.clear()
379
+ self.today_str = date.today().isoformat()
380
+
381
  while self.is_processing.is_set():
382
  ret, frame = video_capture.read()
383
+ if not ret:
384
+ break
385
+
386
  processed_frame = self.process_frame(frame)
 
387
 
388
+ if not self.frame_queue.full():
389
+ self.frame_queue.put(processed_frame)
390
+
391
  self.last_processed_frame = processed_frame
392
  time.sleep(0.05)
393
 
 
396
  self.is_processing.clear()
397
 
398
  def start_processing(self, source) -> str:
399
+ if self.is_processing.is_set():
400
+ return "⚠️ Processing is already active."
401
+
 
402
  self.is_processing.set()
403
+ self.processing_thread = threading.Thread(
404
+ target=self._processing_loop,
405
+ args=(source,),
406
+ daemon=True
407
+ )
408
  self.processing_thread.start()
409
+ return f"✅ Started processing video source {source}..."
410
 
411
  def stop_processing(self) -> str:
412
  self.is_processing.clear()
413
+ return "✅ Processing stopped."
414
 
415
+ # --- Helper Methods ---
416
  def _get_image_caption(self, image: Image.Image) -> str:
417
+ if not HF_API_TOKEN:
418
+ return "Hugging Face API token not configured."
419
  try:
420
  buffered = BytesIO()
421
  image.save(buffered, format="JPEG")
 
422
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
423
+ response = requests.post(HF_API_URL, headers=headers, data=buffered.getvalue())
424
  response.raise_for_status()
425
+ return response.json()[0].get("generated_text", "No caption")
 
426
  except Exception as e:
427
  logger.error(f"Hugging Face API error: {e}")
428
+ return "Caption failed"
429
 
430
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
431
+ if not self.sf:
432
+ return None
433
  try:
434
  buffered = BytesIO()
435
  image.save(buffered, format="JPEG")
436
+ encoded = base64.b64encode(buffered.getvalue()).decode('utf-8')
437
+ cv = self.sf.ContentVersion.create({
438
+ 'Title': f'Image_{worker_id}',
439
+ 'PathOnClient': f'{worker_id}.jpg',
440
+ 'VersionData': encoded,
441
+ 'FirstPublishLocationId': record_id
442
+ })
443
+ return f"/{cv['id']}"
444
  except Exception as e:
445
  logger.error(f"Salesforce image upload error: {e}")
446
  return None
447
 
448
  def get_registered_workers_info(self) -> str:
449
+ if not self.sf:
450
+ return "❌ Salesforce not connected."
451
  try:
452
  records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
453
+ if not records:
454
+ return "No workers registered."
455
+ return f"**👥 Registered Workers ({len(records)})**\n" + "\n".join(
456
+ [f"- {w['Name']} ({w['Worker_ID__c']})" for w in records]
457
+ )
458
+ except Exception as e:
459
+ return f"Error: {e}"
460
+
461
+ # --- GRADIO UI ---
462
  attendance_system = AttendanceSystem()
463
+
464
  def create_interface():
 
465
  with gr.Blocks(theme=gr.themes.Soft(), title="Attendance System") as demo:
466
+ gr.Markdown("# 🎯 Face Recognition Attendance System")
467
+
468
  with gr.Tabs():
469
+ with gr.Tab("⚙️ Controls"):
 
470
  with gr.Row():
471
+ with gr.Column():
472
+ input_source = gr.Radio(
473
+ ["Webcam", "Video File"],
474
+ label="Input Source",
475
+ value="Webcam"
476
+ )
477
+ webcam_source = gr.Number(
478
+ label="Webcam Index",
479
+ value=0,
480
+ visible=True
481
+ )
482
+ video_file = gr.Video(
483
+ label="Video File",
484
+ sources=["upload"],
485
+ visible=False
486
+ )
487
+
488
+ def toggle_source(choice):
489
+ return {
490
+ "Webcam": [gr.Number(visible=True), gr.Video(visible=False)],
491
+ "Video File": [gr.Number(visible=False), gr.Video(visible=True)]
492
+ }[choice]
493
+
494
+ input_source.change(
495
+ fn=toggle_source,
496
+ inputs=input_source,
497
+ outputs=[webcam_source, video_file]
498
+ )
499
+
500
+ with gr.Column():
501
+ start_btn = gr.Button("▶️ Start", variant="primary")
502
+ stop_btn = gr.Button("⏹️ Stop", variant="stop")
503
+ status = gr.Textbox(label="Status", interactive=False)
504
+
505
+ gr.Markdown("**Color Legend:** <span style='color:green'>Green</span> = Known Worker | <span style='color:orange'>Orange</span> = New Worker | <span style='color:red'>Red</span> = Unknown")
506
+
507
+ with gr.Tab("📹 Live View"):
508
+ video_output = gr.Image(label="Detection Output")
509
+ session_log = gr.Textbox(label="Session Log", lines=10, interactive=False)
510
 
 
 
 
 
 
 
 
511
  with gr.Tab("👤 Worker Management"):
512
  with gr.Row():
513
  with gr.Column():
514
+ reg_image = gr.Image(label="Worker Photo", type="pil")
515
+ reg_name = gr.Textbox(label="Full Name")
516
+ reg_btn = gr.Button("Register", variant="primary")
517
+ reg_status = gr.Textbox(label="Status", interactive=False)
518
  with gr.Column():
519
+ worker_list = gr.Markdown(
520
+ value=attendance_system.get_registered_workers_info(),
521
+ label="Registered Workers"
522
+ )
523
+ refresh_btn = gr.Button("🔄 Refresh")
524
+
525
+ # Event handlers
526
+ def start_processing(source_type, webcam_idx, video_path):
527
+ source = webcam_idx if source_type == "Webcam" else video_path
528
+ if source is None:
529
+ return "Please select an input source"
530
+ return attendance_system.start_processing(source)
531
+
532
+ start_btn.click(
533
+ fn=start_processing,
534
+ inputs=[input_source, webcam_source, video_file],
535
+ outputs=[status]
536
+ )
537
+
538
+ stop_btn.click(
539
+ fn=attendance_system.stop_processing,
540
+ outputs=[status]
541
+ )
542
+
543
+ reg_btn.click(
544
+ fn=attendance_system.register_worker_manual,
545
+ inputs=[reg_image, reg_name],
546
+ outputs=[reg_status, worker_list]
547
+ )
548
 
549
+ refresh_btn.click(
550
+ fn=attendance_system.get_registered_workers_info,
551
+ outputs=[worker_list]
552
+ )
553
 
554
+ def update_ui():
555
  while True:
556
  if attendance_system.error_message:
557
+ yield None, attendance_system.error_message, ""
558
+ time.sleep(2)
559
+ attendance_system.error_message = None
560
  continue
561
+
562
  if attendance_system.is_processing.is_set():
563
+ frame = None
564
+ if not attendance_system.frame_queue.empty():
565
+ frame = attendance_system.frame_queue.get_nowait()
566
+ if frame is not None:
567
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
568
+
569
+ log_text = "\n".join(reversed(attendance_system.session_log[-10:])) if attendance_system.session_log else "No activity yet"
570
+ yield frame, "", log_text
571
  else:
572
  if attendance_system.last_processed_frame is not None:
573
  final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
574
+ final_log = "\n".join(reversed(attendance_system.final_log)) if attendance_system.final_log else "Session completed"
575
+ yield final_frame, "", final_log
576
  else:
577
+ yield None, "System ready", "Waiting to start..."
578
+ time.sleep(0.05)
579
+
580
+ demo.load(
581
+ fn=update_ui,
582
+ outputs=[video_output, status, session_log],
583
+ every=0.1
584
+ )
585
+
586
  return demo
587
 
588
  if __name__ == "__main__":
589
  app = create_interface()
590
  app.queue()
591
+ app.launch(server_name="0.0.0.0", server_port=7860)