Sivainti commited on
Commit
e001806
·
verified ·
1 Parent(s): ee81baa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -139
app.py CHANGED
@@ -12,6 +12,7 @@ from datetime import datetime, date
12
  from io import BytesIO
13
  from typing import Tuple, Optional, List, Dict
14
  import pickle
 
15
 
16
  # Third-Party Imports
17
  import cv2
@@ -26,18 +27,14 @@ from simple_salesforce import Salesforce
26
 
27
  # --- CONFIGURATION ---
28
 
29
- # Setup logging
30
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
31
  logger = logging.getLogger(__name__)
32
 
33
- # Load environment variables from .env file
34
  load_dotenv()
35
 
36
- # Hugging Face API configuration
37
  HF_API_URL = "https://api-inference.huggingface.co/models/Salesforce/blip-image-captioning-base"
38
  HF_API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN")
39
 
40
- # Salesforce configuration
41
  SF_CREDENTIALS = {
42
  "username": os.getenv("SF_USERNAME", "smartlabour@attendance.system"),
43
  "password": os.getenv("SF_PASSWORD", "#Prashanth@1234"),
@@ -49,7 +46,6 @@ SF_CREDENTIALS = {
49
 
50
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
51
  def connect_to_salesforce() -> Optional[Salesforce]:
52
- """Establish a connection to Salesforce with retry logic."""
53
  try:
54
  sf = Salesforce(**SF_CREDENTIALS)
55
  sf.describe()
@@ -62,39 +58,30 @@ def connect_to_salesforce() -> Optional[Salesforce]:
62
  # --- CORE LOGIC ---
63
 
64
  class AttendanceSystem:
65
- """
66
- Manages all backend logic for the face recognition attendance system.
67
- """
68
  def __init__(self):
69
- # State Management
70
  self.processing_thread = None
71
  self.is_processing = threading.Event()
72
  self.frame_queue = queue.Queue(maxsize=10)
73
- self.error_message = None
74
  self.last_processed_frame = None
75
  self.final_log = None
76
  self.worker_statuses = {}
77
-
78
- # Data Storage
79
- self.known_face_embeddings: List[np.ndarray] = []
80
- self.known_face_names: List[str] = []
81
- self.known_face_ids: List[str] = []
82
- self.next_worker_id: int = 1
83
-
84
- # Session Tracking
85
- self.last_recognition_time: Dict[str, float] = {}
86
  self.recognition_cooldown = 5
87
- self.session_log: List[str] = []
88
- self.today_attendance: Dict[str, bool] = {}
89
- self.last_detected_faces: Dict[str, float] = {}
90
- self.processed_workers: set = set()
91
-
92
- # Performance optimization
93
- self.frame_skip = 4 # Process every 5th frame
94
  self.frame_counter = 0
95
-
96
- # Initialize
97
  self.sf = connect_to_salesforce()
 
98
  self._create_directories()
99
  self.load_worker_data()
100
 
@@ -109,7 +96,6 @@ class AttendanceSystem:
109
  if not workers:
110
  self._load_local_worker_data()
111
  return
112
-
113
  temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
114
  for worker in workers:
115
  if worker.get('Face_Embedding__c'):
@@ -122,7 +108,6 @@ class AttendanceSystem:
122
  max_id = worker_num
123
  except (ValueError, TypeError):
124
  continue
125
-
126
  self.known_face_embeddings = temp_embeddings
127
  self.known_face_names = temp_names
128
  self.known_face_ids = temp_ids
@@ -139,7 +124,8 @@ class AttendanceSystem:
139
  def _load_local_worker_data(self):
140
  try:
141
  if os.path.exists("data/workers.pkl"):
142
- with open("data/workers.pkl", "rb") as f: data = pickle.load(f)
 
143
  self.known_face_embeddings = data.get("embeddings", [])
144
  self.known_face_names = data.get("names", [])
145
  self.known_face_ids = data.get("ids", [])
@@ -150,24 +136,31 @@ class AttendanceSystem:
150
 
151
  def save_local_worker_data(self):
152
  try:
153
- worker_data = {"embeddings": self.known_face_embeddings, "names": self.known_face_names, "ids": self.known_face_ids, "next_id": self.next_worker_id}
154
- with open("data/workers.pkl", "wb") as f: pickle.dump(worker_data, f)
 
 
 
 
 
 
155
  except Exception as e:
156
  logger.error(f"❌ Error saving local worker data: {e}")
157
-
158
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
159
  if image is None or not name.strip():
160
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
161
  try:
162
  image_array = np.array(image)
 
 
 
163
  analysis = DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
164
  if analysis[0]['face_confidence'] < 0.9:
165
  return "❌ Face not clear enough for registration!", self.get_registered_workers_info()
166
-
167
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
168
- if self._is_duplicate_face(embedding):
169
- return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
170
-
171
  worker_id = f"W{self.next_worker_id:04d}"
172
  name = name.strip().title()
173
  self._add_worker_to_system(worker_id, name, embedding, image_array)
@@ -181,10 +174,12 @@ class AttendanceSystem:
181
 
182
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
183
  try:
 
 
 
184
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
185
- if self._is_duplicate_face(embedding):
186
  return None
187
-
188
  worker_id = f"W{self.next_worker_id:04d}"
189
  worker_name = f"Worker {self.next_worker_id}"
190
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
@@ -204,46 +199,43 @@ class AttendanceSystem:
204
  self.known_face_ids.append(worker_id)
205
  self.next_worker_id += 1
206
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
207
- face_pil.save(f"data/faces/{worker_id}.jpg")
208
  caption = self._get_image_caption(face_pil)
209
  if self.sf:
210
- try:
211
- worker_record = self.sf.Worker__c.create({'Name': name, 'Worker_ID__c': worker_id, 'Face_Embedding__c': json.dumps(embedding), 'Image_Caption__c': caption})
212
- image_url = self._upload_image_to_salesforce(face_pil, worker_record['id'], worker_id)
213
- if image_url: self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
214
- logger.info(f"✅ Worker {worker_id} synced to Salesforce.")
215
- except Exception as e:
216
- logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
217
 
218
- def _is_duplicate_face(self, embedding: List[float], threshold: float = 10.0) -> bool:
219
- if not self.known_face_embeddings: return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
221
  return min(distances) < threshold
222
 
223
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
224
  if worker_id in self.processed_workers:
225
  return False
226
-
227
  today_str = date.today().isoformat()
228
  if worker_id in self.today_attendance:
229
  return False
230
-
231
  current_time = time.time()
232
  if worker_id in self.last_recognition_time and (current_time - self.last_recognition_time[worker_id] < self.recognition_cooldown):
233
  return False
234
-
235
  if self.sf:
236
- try:
237
- self.sf.Attendance__c.create({
238
- 'Worker_ID__c': worker_id,
239
- 'Name__c': worker_name,
240
- 'Date__c': today_str,
241
- 'Timestamp__c': datetime.now().isoformat(),
242
- 'Status__c': "Present"
243
- })
244
- except Exception as e:
245
- logger.error(f"❌ Error saving attendance to Salesforce: {e}")
246
-
247
  self.today_attendance[worker_id] = True
248
  self.last_recognition_time[worker_id] = current_time
249
  self.processed_workers.add(worker_id)
@@ -252,63 +244,67 @@ class AttendanceSystem:
252
  self.session_log.append(log_msg)
253
  return True
254
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
256
  try:
257
  self.frame_counter += 1
 
258
  if self.frame_counter % (self.frame_skip + 1) != 0:
259
  return frame
260
-
261
  height, width = frame.shape[:2]
262
- new_width = 480
263
  new_height = int((new_width / width) * height)
264
  small_frame = cv2.resize(frame, (new_width, new_height))
265
-
 
 
266
  face_objs = DeepFace.extract_faces(
267
- img_path=small_frame,
268
- detector_backend='fastmtcnn',
269
  enforce_detection=False,
270
  align=True
271
  )
272
-
273
  for face_obj in face_objs:
274
  confidence = face_obj['confidence']
275
  if confidence < 0.9:
276
  continue
277
-
278
  facial_area = face_obj['facial_area']
279
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
280
-
281
  x = int(x * width / new_width)
282
  y = int(y * height / new_height)
283
  w = int(w * width / new_width)
284
  h = int(h * height / new_height)
285
-
286
  face_image = frame[y:y+h, x:x+w]
287
- if face_image.size == 0:
288
  continue
289
-
290
  current_time = time.time()
291
  face_key = f"{x}_{y}_{w}_{h}"
292
  if face_key in self.last_detected_faces and (current_time - self.last_detected_faces[face_key] < 2.0):
293
  continue
294
  self.last_detected_faces[face_key] = current_time
295
-
296
  embedding = DeepFace.represent(
297
- img_path=face_image,
298
- model_name='Facenet',
299
  enforce_detection=False,
300
  align=True
301
  )[0]['embedding']
302
-
303
  if not self.known_face_embeddings:
304
  continue
305
-
306
  distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
307
  min_dist = min(distances) if distances else float('inf')
308
- match_index = distances.index(min_dist) if min_dist < 10.0 else -1
309
-
310
  color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
311
-
312
  if match_index != -1:
313
  worker_id = self.known_face_ids[match_index]
314
  worker_name = self.known_face_names[match_index]
@@ -321,11 +317,9 @@ class AttendanceSystem:
321
  if new_worker:
322
  worker_id, worker_name = new_worker
323
  self.mark_attendance(worker_id, worker_name)
324
-
325
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
326
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
327
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
328
-
329
  return frame
330
  except Exception as e:
331
  logger.error(f"ERROR in process_frame: {e}")
@@ -338,31 +332,27 @@ class AttendanceSystem:
338
  self.error_message = err_msg
339
  self.is_processing.clear()
340
  return
341
-
 
342
  if isinstance(source, int):
343
  video_capture.set(cv2.CAP_PROP_FPS, 30)
344
-
345
  while self.is_processing.is_set():
346
  ret, frame = video_capture.read()
347
- if not ret:
348
  break
349
-
350
  processed_frame = self.process_frame(frame)
351
-
352
- if not self.frame_queue.full():
353
  self.frame_queue.put(processed_frame)
354
-
355
  self.last_processed_frame = processed_frame
356
- time.sleep(0.01)
357
-
358
  self.final_log = self.session_log.copy()
359
  video_capture.release()
360
  self.is_processing.clear()
 
361
 
362
  def start_processing(self, source) -> str:
363
- if self.is_processing.is_set():
364
  return "⚠️ Processing is already active."
365
-
366
  self.session_log.clear()
367
  self.last_recognition_time.clear()
368
  self.today_attendance.clear()
@@ -373,10 +363,11 @@ class AttendanceSystem:
373
  self.last_processed_frame = None
374
  self.final_log = None
375
  self.frame_counter = 0
376
-
 
377
  self.is_processing.set()
378
  self.processing_thread = threading.Thread(
379
- target=self._processing_loop,
380
  args=(source,),
381
  daemon=True
382
  )
@@ -387,15 +378,16 @@ class AttendanceSystem:
387
  self.is_processing.clear()
388
  self.error_message = None
389
  self.last_processed_frame = None
390
- self.final_log = None
 
391
  return "✅ Processing stopped by user."
392
 
393
  def _get_image_caption(self, image: Image.Image) -> str:
394
- if not HF_API_TOKEN:
395
  return "Hugging Face API token not configured."
396
  try:
397
  buffered = BytesIO()
398
- image.save(buffered, format="JPEG")
399
  img_data = buffered.getvalue()
400
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
401
  response = requests.post(HF_API_URL, headers=headers, data=img_data)
@@ -407,11 +399,11 @@ class AttendanceSystem:
407
  return "Caption generation failed."
408
 
409
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
410
- if not self.sf:
411
  return None
412
  try:
413
  buffered = BytesIO()
414
- image.save(buffered, format="JPEG")
415
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
416
  cv = self.sf.ContentVersion.create({
417
  'Title': f'Image_{worker_id}',
@@ -425,16 +417,16 @@ class AttendanceSystem:
425
  return None
426
 
427
  def get_registered_workers_info(self) -> str:
428
- if not self.sf:
429
  return "❌ Salesforce not connected."
430
  try:
431
  records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
432
- if not records:
433
  return "No workers registered."
434
  return f"**👥 Registered Workers ({len(records)})**\n" + "\n".join(
435
  [f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records]
436
  )
437
- except Exception as e:
438
  return f"Error: {e}"
439
 
440
  def get_worker_statuses(self) -> str:
@@ -444,7 +436,14 @@ class AttendanceSystem:
444
  [status for status in self.worker_statuses.values()]
445
  )
446
 
 
 
 
 
 
 
447
  # --- GRADIO UI ---
 
448
  attendance_system = AttendanceSystem()
449
 
450
  def create_interface():
@@ -465,9 +464,9 @@ def create_interface():
465
  start_btn = gr.Button("▶️ Start Processing", variant="primary")
466
  stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
467
  status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
 
468
  gr.Markdown("### 2. View Results in the 'Output & Log' Tab")
469
  gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
470
-
471
  with gr.Tab("📊 Output & Log"):
472
  with gr.Row():
473
  with gr.Column(scale=2):
@@ -475,7 +474,6 @@ def create_interface():
475
  with gr.Column(scale=1):
476
  session_log_display = gr.Markdown(label="📋 Session Log", value="System is ready.")
477
  worker_status_display = gr.Markdown(label="👥 Worker Statuses", value=attendance_system.get_worker_statuses())
478
-
479
  with gr.Tab("👤 Worker Management"):
480
  with gr.Row():
481
  with gr.Column():
@@ -486,70 +484,61 @@ def create_interface():
486
  with gr.Column():
487
  registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
488
  refresh_workers_btn = gr.Button("🔄 Refresh List")
489
-
490
  # --- Event Handlers ---
491
- def on_tab_select(evt: gr.SelectData):
492
  return evt.index
493
-
494
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
495
-
496
  def start_wrapper(tab_index, cam_src, vid_path):
497
  source = cam_src if tab_index == 0 else vid_path
498
  return "Please provide an input source." if source is None else attendance_system.start_processing(source)
499
-
500
  start_btn.click(
501
- fn=start_wrapper,
502
- inputs=[selected_tab_index, camera_source, video_file],
503
  outputs=[status_box]
504
  )
505
-
506
  stop_btn.click(
507
- fn=attendance_system.stop_processing,
508
- inputs=None,
509
  outputs=[status_box]
510
  )
511
-
512
  register_btn.click(
513
- fn=attendance_system.register_worker_manual,
514
- inputs=[register_image, register_name],
515
  outputs=[register_output, registered_workers_info]
516
  )
517
-
518
  refresh_workers_btn.click(
519
- fn=attendance_system.get_registered_workers_info,
520
  outputs=[registered_workers_info]
521
  )
522
-
523
  def update_ui_generator():
524
  while True:
525
  if attendance_system.error_message:
526
- yield None, attendance_system.error_message, attendance_system.get_worker_statuses()
527
  time.sleep(2)
528
  attendance_system.error_message = None
529
  continue
530
-
531
  if attendance_system.is_processing.is_set():
532
  frame, log_md = None, "\n".join(reversed(attendance_system.session_log[-20:])) or "Processing..."
533
  status_md = attendance_system.get_worker_statuses()
 
534
  try:
535
  if not attendance_system.frame_queue.empty():
536
  frame = attendance_system.frame_queue.get_nowait()
537
- if frame is not None:
538
  frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
539
- except queue.Empty:
540
  pass
541
- yield frame, log_md, status_md
542
  else:
543
- if attendance_system.last_processed_frame is not None:
544
- final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB)
545
- final_log_md = "\n".join(reversed(attendance_system.final_log[-20:])) or "Processing complete. No log entries."
546
- status_md = attendance_system.get_worker_statuses()
547
- yield final_frame, final_log_md, status_md
548
- else:
549
- yield None, "System stopped. Go to 'Controls & Status' to start.", attendance_system.get_worker_statuses()
550
- time.sleep(0.1)
551
-
552
- demo.load(fn=update_ui_generator, outputs=[video_output, session_log_display, worker_status_display])
553
  return demo
554
 
555
  if __name__ == "__main__":
 
12
  from io import BytesIO
13
  from typing import Tuple, Optional, List, Dict
14
  import pickle
15
+ from concurrent.futures import ThreadPoolExecutor
16
 
17
  # Third-Party Imports
18
  import cv2
 
27
 
28
  # --- CONFIGURATION ---
29
 
 
30
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
31
  logger = logging.getLogger(__name__)
32
 
 
33
  load_dotenv()
34
 
 
35
  HF_API_URL = "https://api-inference.huggingface.co/models/Salesforce/blip-image-captioning-base"
36
  HF_API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN")
37
 
 
38
  SF_CREDENTIALS = {
39
  "username": os.getenv("SF_USERNAME", "smartlabour@attendance.system"),
40
  "password": os.getenv("SF_PASSWORD", "#Prashanth@1234"),
 
46
 
47
  @retry(stop_max_attempt_number=3, wait_fixed=2000)
48
  def connect_to_salesforce() -> Optional[Salesforce]:
 
49
  try:
50
  sf = Salesforce(**SF_CREDENTIALS)
51
  sf.describe()
 
58
  # --- CORE LOGIC ---
59
 
60
  class AttendanceSystem:
 
 
 
61
  def __init__(self):
 
62
  self.processing_thread = None
63
  self.is_processing = threading.Event()
64
  self.frame_queue = queue.Queue(maxsize=10)
65
+ self.error_message = None
66
  self.last_processed_frame = None
67
  self.final_log = None
68
  self.worker_statuses = {}
69
+ self.known_face_embeddings = []
70
+ self.known_face_names = []
71
+ self.known_face_ids = []
72
+ self.next_worker_id = 1
73
+ self.last_recognition_time = {}
 
 
 
 
74
  self.recognition_cooldown = 5
75
+ self.session_log = []
76
+ self.today_attendance = {}
77
+ self.last_detected_faces = {}
78
+ self.processed_workers = set()
79
+ self.frame_skip = 9 # Process every 10th frame
 
 
80
  self.frame_counter = 0
81
+ self.total_frames = 0
82
+ self.processed_frames = 0
83
  self.sf = connect_to_salesforce()
84
+ self.executor = ThreadPoolExecutor(max_workers=2) # For async Salesforce uploads
85
  self._create_directories()
86
  self.load_worker_data()
87
 
 
96
  if not workers:
97
  self._load_local_worker_data()
98
  return
 
99
  temp_embeddings, temp_names, temp_ids, max_id = [], [], [], 0
100
  for worker in workers:
101
  if worker.get('Face_Embedding__c'):
 
108
  max_id = worker_num
109
  except (ValueError, TypeError):
110
  continue
 
111
  self.known_face_embeddings = temp_embeddings
112
  self.known_face_names = temp_names
113
  self.known_face_ids = temp_ids
 
124
  def _load_local_worker_data(self):
125
  try:
126
  if os.path.exists("data/workers.pkl"):
127
+ with open("data/workers.pkl", "rb") as f:
128
+ data = pickle.load(f)
129
  self.known_face_embeddings = data.get("embeddings", [])
130
  self.known_face_names = data.get("names", [])
131
  self.known_face_ids = data.get("ids", [])
 
136
 
137
  def save_local_worker_data(self):
138
  try:
139
+ worker_data = {
140
+ "embeddings": self.known_face_embeddings,
141
+ "names": self.known_face_names,
142
+ "ids": self.known_face_ids,
143
+ "next_id": self.next_worker_id
144
+ }
145
+ with open("data/workers.pkl", "wb") as f:
146
+ pickle.dump(worker_data, f)
147
  except Exception as e:
148
  logger.error(f"❌ Error saving local worker data: {e}")
149
+
150
  def register_worker_manual(self, image: Image.Image, name: str) -> Tuple[str, str]:
151
  if image is None or not name.strip():
152
  return "❌ Please provide both image and name!", self.get_registered_workers_info()
153
  try:
154
  image_array = np.array(image)
155
+ # Preprocess image
156
+ image_array = cv2.equalizeHist(cv2.cvtColor(image_array, cv2.COLOR_RGB2GRAY))
157
+ image_array = cv2.cvtColor(image_array, cv2.COLOR_GRAY2RGB)
158
  analysis = DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
159
  if analysis[0]['face_confidence'] < 0.9:
160
  return "❌ Face not clear enough for registration!", self.get_registered_workers_info()
 
161
  embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
162
+ if self._is_duplicate_face(embedding, threshold=8.0): # Adjusted threshold
163
+ return f"❌ Face matches an existing worker!", self.get_registered_workers_info()
 
164
  worker_id = f"W{self.next_worker_id:04d}"
165
  name = name.strip().title()
166
  self._add_worker_to_system(worker_id, name, embedding, image_array)
 
174
 
175
  def _register_worker_auto(self, face_image: np.ndarray) -> Optional[Tuple[str, str]]:
176
  try:
177
+ # Preprocess image
178
+ face_image = cv2.equalizeHist(cv2.cvtColor(face_image, cv2.COLOR_BGR2GRAY))
179
+ face_image = cv2.cvtColor(face_image, cv2.COLOR_GRAY2RGB)
180
  embedding = DeepFace.represent(img_path=face_image, model_name='Facenet', enforce_detection=False)[0]['embedding']
181
+ if self._is_duplicate_face(embedding, threshold=8.0):
182
  return None
 
183
  worker_id = f"W{self.next_worker_id:04d}"
184
  worker_name = f"Worker {self.next_worker_id}"
185
  self._add_worker_to_system(worker_id, worker_name, embedding, face_image)
 
199
  self.known_face_ids.append(worker_id)
200
  self.next_worker_id += 1
201
  face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
202
+ face_pil.save(f"data/faces/{worker_id}.jpg", quality=85) # Compress image
203
  caption = self._get_image_caption(face_pil)
204
  if self.sf:
205
+ self.executor.submit(self._async_salesforce_upload, worker_id, name, embedding, caption, face_pil)
 
 
 
 
 
 
206
 
207
+ def _async_salesforce_upload(self, worker_id: str, name: str, embedding: List[float], caption: str, image: Image.Image):
208
+ try:
209
+ worker_record = self.sf.Worker__c.create({
210
+ 'Name': name,
211
+ 'Worker_ID__c': worker_id,
212
+ 'Face_Embedding__c': json.dumps(embedding),
213
+ 'Image_Caption__c': caption
214
+ })
215
+ image_url = self._upload_image_to_salesforce(image, worker_record['id'], worker_id)
216
+ if image_url:
217
+ self.sf.Worker__c.update(worker_record['id'], {'Image_URL__c': image_url})
218
+ logger.info(f"✅ Worker {worker_id} synced to Salesforce.")
219
+ except Exception as e:
220
+ logger.error(f"❌ Salesforce sync error for {worker_id}: {e}")
221
+
222
+ def _is_duplicate_face(self, embedding: List[float], threshold: float = 8.0) -> bool:
223
+ if not self.known_face_embeddings:
224
+ return False
225
  distances = [np.linalg.norm(np.array(embedding) - known_embedding) for known_embedding in self.known_face_embeddings]
226
  return min(distances) < threshold
227
 
228
  def mark_attendance(self, worker_id: str, worker_name: str) -> bool:
229
  if worker_id in self.processed_workers:
230
  return False
 
231
  today_str = date.today().isoformat()
232
  if worker_id in self.today_attendance:
233
  return False
 
234
  current_time = time.time()
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
  if self.sf:
238
+ self.executor.submit(self._async_attendance_upload, worker_id, worker_name, today_str)
 
 
 
 
 
 
 
 
 
 
239
  self.today_attendance[worker_id] = True
240
  self.last_recognition_time[worker_id] = current_time
241
  self.processed_workers.add(worker_id)
 
244
  self.session_log.append(log_msg)
245
  return True
246
 
247
+ def _async_attendance_upload(self, worker_id: str, worker_name: str, today_str: str):
248
+ try:
249
+ self.sf.Attendance__c.create({
250
+ 'Worker_ID__c': worker_id,
251
+ 'Name__c': worker_name,
252
+ 'Date__c': today_str,
253
+ 'Timestamp__c': datetime.now().isoformat(),
254
+ 'Status__c': "Present"
255
+ })
256
+ except Exception as e:
257
+ logger.error(f"❌ Error saving attendance to Salesforce: {e}")
258
+
259
  def process_frame(self, frame: np.ndarray) -> np.ndarray:
260
  try:
261
  self.frame_counter += 1
262
+ self.processed_frames += 1
263
  if self.frame_counter % (self.frame_skip + 1) != 0:
264
  return frame
 
265
  height, width = frame.shape[:2]
266
+ new_width = 320 # Reduced resolution
267
  new_height = int((new_width / width) * height)
268
  small_frame = cv2.resize(frame, (new_width, new_height))
269
+ # Preprocess frame
270
+ small_frame = cv2.equalizeHist(cv2.cvtColor(small_frame, cv2.COLOR_BGR2GRAY))
271
+ small_frame = cv2.cvtColor(small_frame, cv2.COLOR_GRAY2RGB)
272
  face_objs = DeepFace.extract_faces(
273
+ img_path=small_frame,
274
+ detector_backend='ssd', # Faster detector
275
  enforce_detection=False,
276
  align=True
277
  )
 
278
  for face_obj in face_objs:
279
  confidence = face_obj['confidence']
280
  if confidence < 0.9:
281
  continue
 
282
  facial_area = face_obj['facial_area']
283
  x, y, w, h = facial_area['x'], facial_area['y'], facial_area['w'], facial_area['h']
 
284
  x = int(x * width / new_width)
285
  y = int(y * height / new_height)
286
  w = int(w * width / new_width)
287
  h = int(h * height / new_height)
 
288
  face_image = frame[y:y+h, x:x+w]
289
+ if face_image.size == 0:
290
  continue
 
291
  current_time = time.time()
292
  face_key = f"{x}_{y}_{w}_{h}"
293
  if face_key in self.last_detected_faces and (current_time - self.last_detected_faces[face_key] < 2.0):
294
  continue
295
  self.last_detected_faces[face_key] = current_time
 
296
  embedding = DeepFace.represent(
297
+ img_path=face_image,
298
+ model_name='Facenet',
299
  enforce_detection=False,
300
  align=True
301
  )[0]['embedding']
 
302
  if not self.known_face_embeddings:
303
  continue
 
304
  distances = [np.linalg.norm(np.array(embedding) - known) for known in self.known_face_embeddings]
305
  min_dist = min(distances) if distances else float('inf')
306
+ match_index = distances.index(min_dist) if min_dist < 8.0 else -1
 
307
  color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
 
308
  if match_index != -1:
309
  worker_id = self.known_face_ids[match_index]
310
  worker_name = self.known_face_names[match_index]
 
317
  if new_worker:
318
  worker_id, worker_name = new_worker
319
  self.mark_attendance(worker_id, worker_name)
 
320
  label = f"{worker_name}" + (f" ({worker_id})" if worker_id else "")
321
  cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
322
  cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
 
323
  return frame
324
  except Exception as e:
325
  logger.error(f"ERROR in process_frame: {e}")
 
332
  self.error_message = err_msg
333
  self.is_processing.clear()
334
  return
335
+ self.total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
336
+ self.processed_frames = 0
337
  if isinstance(source, int):
338
  video_capture.set(cv2.CAP_PROP_FPS, 30)
 
339
  while self.is_processing.is_set():
340
  ret, frame = video_capture.read()
341
+ if not ret:
342
  break
 
343
  processed_frame = self.process_frame(frame)
344
+ if not self.frame_queue.full():
 
345
  self.frame_queue.put(processed_frame)
 
346
  self.last_processed_frame = processed_frame
347
+ time.sleep(0.005) # Reduced sleep time
 
348
  self.final_log = self.session_log.copy()
349
  video_capture.release()
350
  self.is_processing.clear()
351
+ self.executor.shutdown(wait=True) # Ensure all async tasks complete
352
 
353
  def start_processing(self, source) -> str:
354
+ if self.is_processing.is_set():
355
  return "⚠️ Processing is already active."
 
356
  self.session_log.clear()
357
  self.last_recognition_time.clear()
358
  self.today_attendance.clear()
 
363
  self.last_processed_frame = None
364
  self.final_log = None
365
  self.frame_counter = 0
366
+ self.total_frames = 0
367
+ self.processed_frames = 0
368
  self.is_processing.set()
369
  self.processing_thread = threading.Thread(
370
+ target=self._processing_loop,
371
  args=(source,),
372
  daemon=True
373
  )
 
378
  self.is_processing.clear()
379
  self.error_message = None
380
  self.last_processed_frame = None
381
+ self.final_log = self.session_log.copy()
382
+ self.executor.shutdown(wait=True)
383
  return "✅ Processing stopped by user."
384
 
385
  def _get_image_caption(self, image: Image.Image) -> str:
386
+ if not HF_API_TOKEN:
387
  return "Hugging Face API token not configured."
388
  try:
389
  buffered = BytesIO()
390
+ image.save(buffered, format="JPEG", quality=85)
391
  img_data = buffered.getvalue()
392
  headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
393
  response = requests.post(HF_API_URL, headers=headers, data=img_data)
 
399
  return "Caption generation failed."
400
 
401
  def _upload_image_to_salesforce(self, image: Image.Image, record_id: str, worker_id: str) -> Optional[str]:
402
+ if not self.sf:
403
  return None
404
  try:
405
  buffered = BytesIO()
406
+ image.save(buffered, format="JPEG", quality=85)
407
  encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
408
  cv = self.sf.ContentVersion.create({
409
  'Title': f'Image_{worker_id}',
 
417
  return None
418
 
419
  def get_registered_workers_info(self) -> str:
420
+ if not self.sf:
421
  return "❌ Salesforce not connected."
422
  try:
423
  records = self.sf.query_all("SELECT Name, Worker_ID__c FROM Worker__c ORDER BY Name")['records']
424
+ if not records:
425
  return "No workers registered."
426
  return f"**👥 Registered Workers ({len(records)})**\n" + "\n".join(
427
  [f"- **{w['Name']}** (ID: {w['Worker_ID__c']})" for w in records]
428
  )
429
+ except Exception as e:
430
  return f"Error: {e}"
431
 
432
  def get_worker_statuses(self) -> str:
 
436
  [status for status in self.worker_statuses.values()]
437
  )
438
 
439
+ def get_processing_progress(self) -> str:
440
+ if self.total_frames > 0:
441
+ progress = (self.processed_frames / self.total_frames) * 100
442
+ return f"Processing: {progress:.1f}% complete"
443
+ return "Processing..."
444
+
445
  # --- GRADIO UI ---
446
+
447
  attendance_system = AttendanceSystem()
448
 
449
  def create_interface():
 
464
  start_btn = gr.Button("▶️ Start Processing", variant="primary")
465
  stop_btn = gr.Button("⏹️ Stop Processing", variant="stop")
466
  status_box = gr.Textbox(label="Status", interactive=False, value="System Ready.")
467
+ progress_box = gr.Textbox(label="Progress", interactive=False, value="Ready")
468
  gr.Markdown("### 2. View Results in the 'Output & Log' Tab")
469
  gr.Markdown("**🎨 Color Coding:** <font color='green'>Green</font> = Known, <font color='orange'>Orange</font> = New, <font color='red'>Red</font> = Unknown")
 
470
  with gr.Tab("📊 Output & Log"):
471
  with gr.Row():
472
  with gr.Column(scale=2):
 
474
  with gr.Column(scale=1):
475
  session_log_display = gr.Markdown(label="📋 Session Log", value="System is ready.")
476
  worker_status_display = gr.Markdown(label="👥 Worker Statuses", value=attendance_system.get_worker_statuses())
 
477
  with gr.Tab("👤 Worker Management"):
478
  with gr.Row():
479
  with gr.Column():
 
484
  with gr.Column():
485
  registered_workers_info = gr.Markdown(value=attendance_system.get_registered_workers_info())
486
  refresh_workers_btn = gr.Button("🔄 Refresh List")
 
487
  # --- Event Handlers ---
488
+ def on_tab_select(evt: gr.SelectData):
489
  return evt.index
 
490
  video_tabs.select(fn=on_tab_select, inputs=None, outputs=[selected_tab_index])
 
491
  def start_wrapper(tab_index, cam_src, vid_path):
492
  source = cam_src if tab_index == 0 else vid_path
493
  return "Please provide an input source." if source is None else attendance_system.start_processing(source)
 
494
  start_btn.click(
495
+ fn=start_wrapper,
496
+ inputs=[selected_tab_index, camera_source, video_file],
497
  outputs=[status_box]
498
  )
 
499
  stop_btn.click(
500
+ fn=attendance_system.stop_processing,
501
+ inputs=None,
502
  outputs=[status_box]
503
  )
 
504
  register_btn.click(
505
+ fn=attendance_system.register_worker_manual,
506
+ inputs=[register_image, register_name],
507
  outputs=[register_output, registered_workers_info]
508
  )
 
509
  refresh_workers_btn.click(
510
+ fn=attendance_system.get_registered_workers_info,
511
  outputs=[registered_workers_info]
512
  )
 
513
  def update_ui_generator():
514
  while True:
515
  if attendance_system.error_message:
516
+ yield None, attendance_system.error_message, attendance_system.get_worker_statuses(), attendance_system.error_message
517
  time.sleep(2)
518
  attendance_system.error_message = None
519
  continue
 
520
  if attendance_system.is_processing.is_set():
521
  frame, log_md = None, "\n".join(reversed(attendance_system.session_log[-20:])) or "Processing..."
522
  status_md = attendance_system.get_worker_statuses()
523
+ progress = attendance_system.get_processing_progress()
524
  try:
525
  if not attendance_system.frame_queue.empty():
526
  frame = attendance_system.frame_queue.get_nowait()
527
+ if frame is not None:
528
  frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
529
+ except queue.Empty:
530
  pass
531
+ yield frame, log_md, status_md, progress
532
  else:
533
+ final_log_md = "\n".join(reversed(attendance_system.final_log[-20:])) if attendance_system.final_log else "Processing complete. No log entries."
534
+ status_md = attendance_system.get_worker_statuses()
535
+ final_frame = cv2.cvtColor(attendance_system.last_processed_frame, cv2.COLOR_BGR2RGB) if attendance_system.last_processed_frame is not None else None
536
+ yield final_frame, final_log_md, status_md, "Processing complete."
537
+ time.sleep(0.05) # Faster UI updates
538
+ demo.load(
539
+ fn=update_ui_generator,
540
+ outputs=[video_output, session_log_display, worker_status_display, progress_box]
541
+ )
 
542
  return demo
543
 
544
  if __name__ == "__main__":