Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -87,7 +87,7 @@ class AttendanceSystem:
|
|
| 87 |
self.recognition_cooldown = 10 # Cooldown to prevent duplicates
|
| 88 |
self.session_log: List[str] = []
|
| 89 |
self.session_marked_present = set() # Track who's already marked present in this session
|
| 90 |
-
self.
|
| 91 |
self.face_recognition_buffer = {} # Buffer for multiple detections before confirming
|
| 92 |
self.buffer_threshold = 2 # Reduced threshold for faster recognition
|
| 93 |
self.frame_skip_counter = 0 # Skip frames for better performance
|
|
@@ -160,40 +160,51 @@ class AttendanceSystem:
|
|
| 160 |
return "β Please provide both image and name!", self.get_registered_workers_info()
|
| 161 |
try:
|
| 162 |
image_array = np.array(image)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
|
| 164 |
-
embedding = DeepFace.represent(img_path=image_array, model_name='Facenet')[0]['embedding']
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
return f"β Face matches an existing worker!", self.get_registered_workers_info()
|
| 167 |
|
| 168 |
worker_id = f"W{self.next_worker_id:04d}"
|
| 169 |
name = name.strip().title()
|
| 170 |
self._add_worker_to_system(worker_id, name, embedding, image_array)
|
| 171 |
self.save_local_worker_data()
|
| 172 |
-
|
| 173 |
return f"β
{name} registered with ID: {worker_id}!", self.get_registered_workers_info()
|
| 174 |
-
except ValueError:
|
| 175 |
-
return "β No face detected in the image!", self.get_registered_workers_info()
|
| 176 |
except Exception as e:
|
|
|
|
| 177 |
return f"β Registration error: {e}", self.get_registered_workers_info()
|
| 178 |
|
| 179 |
def _register_worker_auto(self, face_image: np.ndarray, face_embedding: List[float]) -> Optional[Tuple[str, str]]:
|
| 180 |
try:
|
| 181 |
-
#
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
return None
|
| 184 |
-
|
| 185 |
-
worker_id = f"W{self.next_worker_id:04d}"
|
| 186 |
|
| 187 |
-
# Check
|
| 188 |
-
if
|
| 189 |
return None
|
| 190 |
|
|
|
|
| 191 |
worker_name = f"Unknown Worker {self.next_worker_id}"
|
|
|
|
| 192 |
self._add_worker_to_system(worker_id, worker_name, face_embedding, face_image)
|
| 193 |
self.save_local_worker_data()
|
| 194 |
|
| 195 |
-
# Mark as registered in this session
|
| 196 |
-
self.
|
| 197 |
|
| 198 |
log_msg = f"π [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
|
| 199 |
self.session_log.append(log_msg)
|
|
@@ -208,8 +219,19 @@ class AttendanceSystem:
|
|
| 208 |
self.known_face_names.append(name)
|
| 209 |
self.known_face_ids.append(worker_id)
|
| 210 |
self.next_worker_id += 1
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
face_pil.save(f"data/faces/{worker_id}.jpg")
|
|
|
|
| 213 |
caption = self._get_image_caption(face_pil)
|
| 214 |
if self.sf:
|
| 215 |
try:
|
|
@@ -227,12 +249,14 @@ class AttendanceSystem:
|
|
| 227 |
|
| 228 |
embedding_array = np.array(embedding)
|
| 229 |
for known_embedding in self.known_face_embeddings:
|
| 230 |
-
# Use
|
| 231 |
euclidean_dist = np.linalg.norm(embedding_array - known_embedding)
|
|
|
|
|
|
|
| 232 |
cosine_sim = np.dot(embedding_array, known_embedding) / (np.linalg.norm(embedding_array) * np.linalg.norm(known_embedding))
|
| 233 |
|
| 234 |
-
#
|
| 235 |
-
if
|
| 236 |
return True
|
| 237 |
|
| 238 |
return False
|
|
@@ -285,25 +309,17 @@ class AttendanceSystem:
|
|
| 285 |
return -1, float('inf')
|
| 286 |
|
| 287 |
best_match_idx = -1
|
| 288 |
-
|
| 289 |
|
| 290 |
for i, known_embedding in enumerate(self.known_face_embeddings):
|
| 291 |
-
# Euclidean distance
|
| 292 |
euclidean_dist = np.linalg.norm(target_embedding - known_embedding)
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
np.linalg.norm(target_embedding) * np.linalg.norm(known_embedding)
|
| 297 |
-
)
|
| 298 |
-
|
| 299 |
-
# Combined score (lower is better)
|
| 300 |
-
combined_score = euclidean_dist * (1 - cosine_sim)
|
| 301 |
-
|
| 302 |
-
if combined_score < best_score:
|
| 303 |
-
best_score = combined_score
|
| 304 |
best_match_idx = i
|
| 305 |
|
| 306 |
-
return best_match_idx,
|
| 307 |
|
| 308 |
# --- Video Processing ---
|
| 309 |
def process_frame(self, frame: np.ndarray) -> np.ndarray:
|
|
@@ -334,7 +350,7 @@ class AttendanceSystem:
|
|
| 334 |
print(f" Face #{i+1}: Confidence Score = {confidence:.2f}")
|
| 335 |
|
| 336 |
# More lenient confidence threshold
|
| 337 |
-
if confidence < 0.
|
| 338 |
print(" -> Confidence too low, skipping.")
|
| 339 |
continue
|
| 340 |
|
|
@@ -348,7 +364,7 @@ class AttendanceSystem:
|
|
| 348 |
continue
|
| 349 |
|
| 350 |
# More lenient minimum face size check
|
| 351 |
-
if w <
|
| 352 |
print(" -> Face too small, skipping.")
|
| 353 |
continue
|
| 354 |
|
|
@@ -363,26 +379,15 @@ class AttendanceSystem:
|
|
| 363 |
color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
|
| 364 |
|
| 365 |
if self.known_face_embeddings:
|
| 366 |
-
#
|
| 367 |
-
match_index,
|
| 368 |
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
# Use more lenient thresholds for recognition
|
| 377 |
-
if (match_index != -1 and match_score < 15.0) or (simple_match_index != -1 and min_dist < 12.0):
|
| 378 |
-
# Use the better match
|
| 379 |
-
if match_index != -1 and match_score < 15.0:
|
| 380 |
-
final_match_index = match_index
|
| 381 |
-
else:
|
| 382 |
-
final_match_index = simple_match_index
|
| 383 |
-
|
| 384 |
-
worker_id = self.known_face_ids[final_match_index]
|
| 385 |
-
worker_name = self.known_face_names[final_match_index]
|
| 386 |
color = (0, 255, 0) # Green
|
| 387 |
print(f" β MATCH! Recognized as {worker_name}")
|
| 388 |
|
|
@@ -402,26 +407,32 @@ class AttendanceSystem:
|
|
| 402 |
del self.face_recognition_buffer[buffer_key]
|
| 403 |
|
| 404 |
else:
|
| 405 |
-
#
|
| 406 |
-
if
|
| 407 |
color = (0, 165, 255) # Orange for potential new worker
|
| 408 |
-
print(f" β NO MATCH. Attempting
|
| 409 |
new_worker = self._register_worker_auto(face_image, embedding)
|
| 410 |
if new_worker:
|
| 411 |
worker_id, worker_name = new_worker[0], new_worker[1]
|
|
|
|
| 412 |
if self.mark_attendance(worker_id, worker_name):
|
| 413 |
self.last_recognition_time[worker_id] = time.time()
|
|
|
|
|
|
|
| 414 |
else:
|
| 415 |
print(" β Uncertain match, skipping registration.")
|
| 416 |
else:
|
| 417 |
-
# No known faces, auto-register
|
| 418 |
color = (0, 165, 255) # Orange for new worker
|
| 419 |
print(" -> No known faces in database. Auto-registering...")
|
| 420 |
new_worker = self._register_worker_auto(face_image, embedding)
|
| 421 |
if new_worker:
|
| 422 |
worker_id, worker_name = new_worker[0], new_worker[1]
|
|
|
|
| 423 |
if self.mark_attendance(worker_id, worker_name):
|
| 424 |
self.last_recognition_time[worker_id] = time.time()
|
|
|
|
|
|
|
| 425 |
|
| 426 |
# Clean old buffer entries
|
| 427 |
current_time = time.time()
|
|
@@ -463,7 +474,7 @@ class AttendanceSystem:
|
|
| 463 |
self.session_log.clear()
|
| 464 |
self.last_recognition_time.clear()
|
| 465 |
self.session_marked_present.clear() # Reset session attendance tracking
|
| 466 |
-
self.
|
| 467 |
self.face_recognition_buffer.clear() # Reset recognition buffer
|
| 468 |
self.error_message = None
|
| 469 |
self.last_processed_frame = None
|
|
|
|
| 87 |
self.recognition_cooldown = 10 # Cooldown to prevent duplicates
|
| 88 |
self.session_log: List[str] = []
|
| 89 |
self.session_marked_present = set() # Track who's already marked present in this session
|
| 90 |
+
self.session_auto_registered = set() # Track embeddings that were auto-registered in this session
|
| 91 |
self.face_recognition_buffer = {} # Buffer for multiple detections before confirming
|
| 92 |
self.buffer_threshold = 2 # Reduced threshold for faster recognition
|
| 93 |
self.frame_skip_counter = 0 # Skip frames for better performance
|
|
|
|
| 160 |
return "β Please provide both image and name!", self.get_registered_workers_info()
|
| 161 |
try:
|
| 162 |
image_array = np.array(image)
|
| 163 |
+
# Convert RGB to BGR for DeepFace
|
| 164 |
+
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
| 165 |
+
image_array = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR)
|
| 166 |
+
|
| 167 |
+
# Verify face detection first
|
| 168 |
DeepFace.analyze(img_path=image_array, actions=['emotion'], enforce_detection=True)
|
| 169 |
+
embedding = DeepFace.represent(img_path=image_array, model_name='Facenet', enforce_detection=True)[0]['embedding']
|
| 170 |
+
|
| 171 |
+
# Check for duplicates with reasonable threshold for manual registration
|
| 172 |
+
if self._is_duplicate_face(embedding, threshold=8.0):
|
| 173 |
return f"β Face matches an existing worker!", self.get_registered_workers_info()
|
| 174 |
|
| 175 |
worker_id = f"W{self.next_worker_id:04d}"
|
| 176 |
name = name.strip().title()
|
| 177 |
self._add_worker_to_system(worker_id, name, embedding, image_array)
|
| 178 |
self.save_local_worker_data()
|
| 179 |
+
|
| 180 |
return f"β
{name} registered with ID: {worker_id}!", self.get_registered_workers_info()
|
| 181 |
+
except ValueError as e:
|
| 182 |
+
return f"β No face detected in the image! Error: {str(e)}", self.get_registered_workers_info()
|
| 183 |
except Exception as e:
|
| 184 |
+
logger.error(f"Registration error: {e}")
|
| 185 |
return f"β Registration error: {e}", self.get_registered_workers_info()
|
| 186 |
|
| 187 |
def _register_worker_auto(self, face_image: np.ndarray, face_embedding: List[float]) -> Optional[Tuple[str, str]]:
|
| 188 |
try:
|
| 189 |
+
# Create a unique identifier for this embedding to track session registrations
|
| 190 |
+
embedding_hash = hash(tuple(np.round(face_embedding, 3)))
|
| 191 |
+
|
| 192 |
+
# Check if this specific face was already auto-registered in this session
|
| 193 |
+
if embedding_hash in self.session_auto_registered:
|
| 194 |
return None
|
|
|
|
|
|
|
| 195 |
|
| 196 |
+
# Check for duplicates with more lenient threshold for auto-registration
|
| 197 |
+
if self._is_duplicate_face(face_embedding, threshold=10.0):
|
| 198 |
return None
|
| 199 |
|
| 200 |
+
worker_id = f"W{self.next_worker_id:04d}"
|
| 201 |
worker_name = f"Unknown Worker {self.next_worker_id}"
|
| 202 |
+
|
| 203 |
self._add_worker_to_system(worker_id, worker_name, face_embedding, face_image)
|
| 204 |
self.save_local_worker_data()
|
| 205 |
|
| 206 |
+
# Mark this embedding as registered in this session
|
| 207 |
+
self.session_auto_registered.add(embedding_hash)
|
| 208 |
|
| 209 |
log_msg = f"π [{datetime.now().strftime('%H:%M:%S')}] Auto-registered: {worker_name} ({worker_id})"
|
| 210 |
self.session_log.append(log_msg)
|
|
|
|
| 219 |
self.known_face_names.append(name)
|
| 220 |
self.known_face_ids.append(worker_id)
|
| 221 |
self.next_worker_id += 1
|
| 222 |
+
|
| 223 |
+
# Ensure image is in RGB format for saving
|
| 224 |
+
if len(image_array.shape) == 3:
|
| 225 |
+
if image_array.shape[2] == 3:
|
| 226 |
+
# Check if BGR (OpenCV format) and convert to RGB
|
| 227 |
+
face_pil = Image.fromarray(cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB))
|
| 228 |
+
else:
|
| 229 |
+
face_pil = Image.fromarray(image_array)
|
| 230 |
+
else:
|
| 231 |
+
face_pil = Image.fromarray(image_array)
|
| 232 |
+
|
| 233 |
face_pil.save(f"data/faces/{worker_id}.jpg")
|
| 234 |
+
|
| 235 |
caption = self._get_image_caption(face_pil)
|
| 236 |
if self.sf:
|
| 237 |
try:
|
|
|
|
| 249 |
|
| 250 |
embedding_array = np.array(embedding)
|
| 251 |
for known_embedding in self.known_face_embeddings:
|
| 252 |
+
# Use euclidean distance as primary method
|
| 253 |
euclidean_dist = np.linalg.norm(embedding_array - known_embedding)
|
| 254 |
+
|
| 255 |
+
# Also check cosine similarity for additional validation
|
| 256 |
cosine_sim = np.dot(embedding_array, known_embedding) / (np.linalg.norm(embedding_array) * np.linalg.norm(known_embedding))
|
| 257 |
|
| 258 |
+
# Consider it duplicate if distance is small OR similarity is very high
|
| 259 |
+
if euclidean_dist < threshold or cosine_sim > 0.85:
|
| 260 |
return True
|
| 261 |
|
| 262 |
return False
|
|
|
|
| 309 |
return -1, float('inf')
|
| 310 |
|
| 311 |
best_match_idx = -1
|
| 312 |
+
best_distance = float('inf')
|
| 313 |
|
| 314 |
for i, known_embedding in enumerate(self.known_face_embeddings):
|
| 315 |
+
# Primary: Euclidean distance
|
| 316 |
euclidean_dist = np.linalg.norm(target_embedding - known_embedding)
|
| 317 |
|
| 318 |
+
if euclidean_dist < best_distance:
|
| 319 |
+
best_distance = euclidean_dist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
best_match_idx = i
|
| 321 |
|
| 322 |
+
return best_match_idx, best_distance
|
| 323 |
|
| 324 |
# --- Video Processing ---
|
| 325 |
def process_frame(self, frame: np.ndarray) -> np.ndarray:
|
|
|
|
| 350 |
print(f" Face #{i+1}: Confidence Score = {confidence:.2f}")
|
| 351 |
|
| 352 |
# More lenient confidence threshold
|
| 353 |
+
if confidence < 0.80:
|
| 354 |
print(" -> Confidence too low, skipping.")
|
| 355 |
continue
|
| 356 |
|
|
|
|
| 364 |
continue
|
| 365 |
|
| 366 |
# More lenient minimum face size check
|
| 367 |
+
if w < 40 or h < 40:
|
| 368 |
print(" -> Face too small, skipping.")
|
| 369 |
continue
|
| 370 |
|
|
|
|
| 379 |
color, worker_id, worker_name = (0, 0, 255), None, "Unknown"
|
| 380 |
|
| 381 |
if self.known_face_embeddings:
|
| 382 |
+
# Find best match
|
| 383 |
+
match_index, match_distance = self._find_best_match(embedding_array)
|
| 384 |
|
| 385 |
+
print(f" -> Comparing to DB... Best Match Distance: {match_distance:.4f}")
|
| 386 |
+
|
| 387 |
+
# Use reasonable threshold for recognition (12.0 works well with Facenet)
|
| 388 |
+
if match_index != -1 and match_distance < 12.0:
|
| 389 |
+
worker_id = self.known_face_ids[match_index]
|
| 390 |
+
worker_name = self.known_face_names[match_index]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
color = (0, 255, 0) # Green
|
| 392 |
print(f" β MATCH! Recognized as {worker_name}")
|
| 393 |
|
|
|
|
| 407 |
del self.face_recognition_buffer[buffer_key]
|
| 408 |
|
| 409 |
else:
|
| 410 |
+
# No match found - attempt auto-registration for new faces
|
| 411 |
+
if match_distance > 12.0: # Clearly different from existing faces
|
| 412 |
color = (0, 165, 255) # Orange for potential new worker
|
| 413 |
+
print(f" β NO MATCH (Distance: {match_distance:.4f}). Attempting auto-registration...")
|
| 414 |
new_worker = self._register_worker_auto(face_image, embedding)
|
| 415 |
if new_worker:
|
| 416 |
worker_id, worker_name = new_worker[0], new_worker[1]
|
| 417 |
+
color = (0, 255, 0) # Change to green after successful registration
|
| 418 |
if self.mark_attendance(worker_id, worker_name):
|
| 419 |
self.last_recognition_time[worker_id] = time.time()
|
| 420 |
+
else:
|
| 421 |
+
print(" -> Auto-registration skipped (duplicate or session limit)")
|
| 422 |
else:
|
| 423 |
print(" β Uncertain match, skipping registration.")
|
| 424 |
else:
|
| 425 |
+
# No known faces, auto-register the first face
|
| 426 |
color = (0, 165, 255) # Orange for new worker
|
| 427 |
print(" -> No known faces in database. Auto-registering...")
|
| 428 |
new_worker = self._register_worker_auto(face_image, embedding)
|
| 429 |
if new_worker:
|
| 430 |
worker_id, worker_name = new_worker[0], new_worker[1]
|
| 431 |
+
color = (0, 255, 0) # Change to green after successful registration
|
| 432 |
if self.mark_attendance(worker_id, worker_name):
|
| 433 |
self.last_recognition_time[worker_id] = time.time()
|
| 434 |
+
else:
|
| 435 |
+
print(" -> Auto-registration failed")
|
| 436 |
|
| 437 |
# Clean old buffer entries
|
| 438 |
current_time = time.time()
|
|
|
|
| 474 |
self.session_log.clear()
|
| 475 |
self.last_recognition_time.clear()
|
| 476 |
self.session_marked_present.clear() # Reset session attendance tracking
|
| 477 |
+
self.session_auto_registered.clear() # Reset session registration tracking
|
| 478 |
self.face_recognition_buffer.clear() # Reset recognition buffer
|
| 479 |
self.error_message = None
|
| 480 |
self.last_processed_frame = None
|