GeraldoRiberia commited on
Commit
94741c6
·
1 Parent(s): e448396

face enrollment added

Browse files
Files changed (2) hide show
  1. server.py +56 -3
  2. services/single_tracker.py +43 -6
server.py CHANGED
@@ -18,6 +18,8 @@ import math
18
  from pydantic import BaseModel, Field
19
  from pymongo import AsyncMongoClient
20
  import bcrypt
 
 
21
  from jose import JWTError, jwt
22
  from dotenv import load_dotenv
23
  from pathlib import Path
@@ -509,6 +511,41 @@ async def verify_token(current_user: UserPublic = Depends(get_current_user)):
509
  return current_user
510
 
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  @app.websocket("/ws")
513
  async def websocket_endpoint(websocket: WebSocket):
514
  global is_recording, video_writer, recording_filename, latest_obs_frame, is_obs_active, zoom_multiplier
@@ -517,6 +554,7 @@ async def websocket_endpoint(websocket: WebSocket):
517
  logger.info("New WebSocket connection established.")
518
 
519
  current_mode = "single" # Default mode
 
520
 
521
  try:
522
  while True:
@@ -530,6 +568,21 @@ async def websocket_endpoint(websocket: WebSocket):
530
  logger.info(f"Switching mode from {current_mode} to {payload['mode']}")
531
  current_mode = payload["mode"]
532
  await websocket.send_json({"type": "mode_ack", "mode": current_mode})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  elif "zoom_scale" in payload:
534
  zoom_multiplier = float(payload["zoom_scale"])
535
  logger.info(f"Updated zoom multiplier to {zoom_multiplier}")
@@ -572,9 +625,9 @@ async def websocket_endpoint(websocket: WebSocket):
572
  continue
573
 
574
  # Prepare inference function
575
- def run_inference(f, mode):
576
  if mode == "single":
577
- return single_tracker.process_frame(f)
578
  elif mode == "multi":
579
  return multi_tracker.process_frame(f)
580
  else:
@@ -584,7 +637,7 @@ async def websocket_endpoint(websocket: WebSocket):
584
  response_data = {}
585
  try:
586
  response_data = await asyncio.get_event_loop().run_in_executor(
587
- executor, run_inference, frame, current_mode
588
  )
589
  except Exception as e:
590
  logger.error(f"Error processing frame in {current_mode} mode: {e}")
 
18
  from pydantic import BaseModel, Field
19
  from pymongo import AsyncMongoClient
20
  import bcrypt
21
+ import pickle
22
+ from bson import ObjectId
23
  from jose import JWTError, jwt
24
  from dotenv import load_dotenv
25
  from pathlib import Path
 
511
  return current_user
512
 
513
 
514
+ @app.post("/api/enroll_face")
515
+ async def enroll_face(
516
+ video: UploadFile = File(...),
517
+ current_user: UserPublic = Depends(get_current_user)
518
+ ):
519
+ try:
520
+ temp_path = f"temp_enroll_{uuid.uuid4()}.mp4"
521
+ with open(temp_path, "wb") as buffer:
522
+ shutil.copyfileobj(video.file, buffer)
523
+
524
+ logger.info(f"Extracting embeddings for user {current_user.id}")
525
+
526
+ def run_extraction():
527
+ return face_service.extract_embeddings_from_video(temp_path)
528
+
529
+ embeddings, num_frames = await asyncio.get_event_loop().run_in_executor(
530
+ executor, run_extraction
531
+ )
532
+
533
+ pickled_embeddings = pickle.dumps(embeddings)
534
+ await users_collection.update_one(
535
+ {"_id": ObjectId(current_user.id)},
536
+ {"$set": {"embeddings": pickled_embeddings}}
537
+ )
538
+
539
+ os.remove(temp_path)
540
+
541
+ return {"ok": True, "message": "Face enrolled successfully", "frames_used": num_frames}
542
+ except Exception as e:
543
+ logger.error(f"Enrollment failed: {e}")
544
+ if os.path.exists(temp_path):
545
+ os.remove(temp_path)
546
+ raise HTTPException(status_code=500, detail=str(e))
547
+
548
+
549
  @app.websocket("/ws")
550
  async def websocket_endpoint(websocket: WebSocket):
551
  global is_recording, video_writer, recording_filename, latest_obs_frame, is_obs_active, zoom_multiplier
 
554
  logger.info("New WebSocket connection established.")
555
 
556
  current_mode = "single" # Default mode
557
+ ws_user_embeddings = None
558
 
559
  try:
560
  while True:
 
568
  logger.info(f"Switching mode from {current_mode} to {payload['mode']}")
569
  current_mode = payload["mode"]
570
  await websocket.send_json({"type": "mode_ack", "mode": current_mode})
571
+ elif "type" in payload and payload["type"] == "auth":
572
+ token = payload.get("token")
573
+ try:
574
+ token_data = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
575
+ user_id = token_data.get("sub")
576
+ if user_id:
577
+ user = await users_collection.find_one({"_id": ObjectId(user_id)})
578
+ if user and "embeddings" in user and user["embeddings"]:
579
+ ws_user_embeddings = pickle.loads(user["embeddings"])
580
+ logger.info(f"Loaded custom face embeddings for user {user_id}")
581
+ await websocket.send_json({"type": "auth_ack", "status": "enrolled"})
582
+ else:
583
+ await websocket.send_json({"type": "auth_ack", "status": "no_enrollment"})
584
+ except Exception as e:
585
+ logger.error(f"WS Auth failed: {e}")
586
  elif "zoom_scale" in payload:
587
  zoom_multiplier = float(payload["zoom_scale"])
588
  logger.info(f"Updated zoom multiplier to {zoom_multiplier}")
 
625
  continue
626
 
627
  # Prepare inference function
628
+ def run_inference(f, mode, embeddings=None):
629
  if mode == "single":
630
+ return single_tracker.process_frame(f, custom_embeddings=embeddings)
631
  elif mode == "multi":
632
  return multi_tracker.process_frame(f)
633
  else:
 
637
  response_data = {}
638
  try:
639
  response_data = await asyncio.get_event_loop().run_in_executor(
640
+ executor, run_inference, frame, current_mode, ws_user_embeddings
641
  )
642
  except Exception as e:
643
  logger.error(f"Error processing frame in {current_mode} mode: {e}")
services/single_tracker.py CHANGED
@@ -73,7 +73,7 @@ class SingleTracker:
73
  if not cache_loaded:
74
  logger.warning(f"Cache invalid or not found at {self.cache_file}. Returning empty embeddings. Please run Model/face_model.py to generate cache.")
75
 
76
- def process_frame(self, frame):
77
  """
78
  Process a single BGR image frame for single face tracking.
79
  Returns a dictionary with tracking results.
@@ -97,14 +97,45 @@ class SingleTracker:
97
  if results and len(results) > 0 and results[0].boxes.id is not None:
98
  boxes = results[0].boxes.xyxy.cpu().numpy().astype(int)
99
  track_ids = results[0].boxes.id.cpu().numpy().astype(int)
 
 
 
 
100
 
101
- for box, track_id in zip(boxes, track_ids):
102
  x1, y1, x2, y2 = box.tolist()
103
  track_id = int(track_id)
104
  max_similarity = 0.0
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  # Lock resolution logic
107
- if track_id not in self.known_tracks and len(self.main_user_embeddings) > 0:
 
 
 
108
  if track_id not in self.track_retries:
109
  self.track_retries[track_id] = 0
110
 
@@ -115,7 +146,9 @@ class SingleTracker:
115
  # Strict check
116
  current_face = DeepFace.represent(face_crop, model_name=self.model_name, enforce_detection=False)[0]["embedding"]
117
 
118
- for user_embedding in self.main_user_embeddings:
 
 
119
  sim = np.dot(user_embedding, current_face) / (np.linalg.norm(user_embedding) * np.linalg.norm(current_face))
120
  if sim > max_similarity:
121
  max_similarity = sim
@@ -156,7 +189,9 @@ class SingleTracker:
156
  "x2": x2, "y2": y2,
157
  "is_target": True,
158
  "label": label,
159
- "similarity": max_similarity if 'max_similarity' in locals() else -1.0
 
 
160
  })
161
  elif track_id in self.track_retries:
162
  # Draw scanning box
@@ -167,7 +202,9 @@ class SingleTracker:
167
  "x2": x2, "y2": y2,
168
  "is_target": False,
169
  "label": label,
170
- "similarity": max_similarity if 'max_similarity' in locals() else -1.0
 
 
171
  })
172
 
173
  except Exception as e:
 
73
  if not cache_loaded:
74
  logger.warning(f"Cache invalid or not found at {self.cache_file}. Returning empty embeddings. Please run Model/face_model.py to generate cache.")
75
 
76
+ def process_frame(self, frame, custom_embeddings=None):
77
  """
78
  Process a single BGR image frame for single face tracking.
79
  Returns a dictionary with tracking results.
 
97
  if results and len(results) > 0 and results[0].boxes.id is not None:
98
  boxes = results[0].boxes.xyxy.cpu().numpy().astype(int)
99
  track_ids = results[0].boxes.id.cpu().numpy().astype(int)
100
+
101
+ keypoints = None
102
+ if hasattr(results[0], 'keypoints') and results[0].keypoints is not None:
103
+ keypoints = results[0].keypoints.xy.cpu().numpy()
104
 
105
+ for idx, (box, track_id) in enumerate(zip(boxes, track_ids)):
106
  x1, y1, x2, y2 = box.tolist()
107
  track_id = int(track_id)
108
  max_similarity = 0.0
109
 
110
+ # Compute Head Pose
111
+ yaw = 0.0
112
+ pitch = 0.0
113
+ if keypoints is not None and len(keypoints) > idx:
114
+ kpts = keypoints[idx]
115
+ if len(kpts) >= 5:
116
+ lex, ley = kpts[0]
117
+ rex, rey = kpts[1]
118
+ nx, ny = kpts[2]
119
+ lmx, lmy = kpts[3]
120
+ rmx, rmy = kpts[4]
121
+
122
+ # Yaw: (-) turned left, (+) turned right
123
+ l_nose = abs(nx - lex)
124
+ r_nose = abs(nx - rex)
125
+ yaw = (l_nose - r_nose) / (l_nose + r_nose + 1e-6)
126
+
127
+ # Pitch: (-) looking up, (+) looking down
128
+ eye_cy = (ley + rey) / 2
129
+ mouth_cy = (lmy + rmy) / 2
130
+ n_eye = ny - eye_cy
131
+ n_mouth = mouth_cy - ny
132
+ pitch = (n_eye - n_mouth) / (n_eye + n_mouth + 1e-6)
133
+
134
  # Lock resolution logic
135
+
136
+ embeddings_to_check = custom_embeddings if custom_embeddings is not None and len(custom_embeddings) > 0 else self.main_user_embeddings
137
+
138
+ if track_id not in self.known_tracks and len(embeddings_to_check) > 0:
139
  if track_id not in self.track_retries:
140
  self.track_retries[track_id] = 0
141
 
 
146
  # Strict check
147
  current_face = DeepFace.represent(face_crop, model_name=self.model_name, enforce_detection=False)[0]["embedding"]
148
 
149
+ embeddings_to_check = custom_embeddings if custom_embeddings is not None and len(custom_embeddings) > 0 else self.main_user_embeddings
150
+
151
+ for user_embedding in embeddings_to_check:
152
  sim = np.dot(user_embedding, current_face) / (np.linalg.norm(user_embedding) * np.linalg.norm(current_face))
153
  if sim > max_similarity:
154
  max_similarity = sim
 
189
  "x2": x2, "y2": y2,
190
  "is_target": True,
191
  "label": label,
192
+ "similarity": max_similarity if 'max_similarity' in locals() else -1.0,
193
+ "yaw": float(yaw),
194
+ "pitch": float(pitch)
195
  })
196
  elif track_id in self.track_retries:
197
  # Draw scanning box
 
202
  "x2": x2, "y2": y2,
203
  "is_target": False,
204
  "label": label,
205
+ "similarity": max_similarity if 'max_similarity' in locals() else -1.0,
206
+ "yaw": float(yaw),
207
+ "pitch": float(pitch)
208
  })
209
 
210
  except Exception as e: