Spaces:
Sleeping
Sleeping
| import cv2 | |
| import base64 | |
| import asyncio | |
| import threading | |
| import logging | |
| from services.face_engine import face_engine | |
| from services.recognizer import embedding_store | |
| logger = logging.getLogger(__name__) | |
| class CameraStreamer: | |
| def __init__(self): | |
| self.cap = None | |
| self.is_running = False | |
| self.thread = None | |
| self._event_queue = None # queue to pass events to websocket | |
| self.active_subject_id = None | |
| # Frame sampling control | |
| self.frame_count = 0 | |
| self.process_every_n = 20 | |
| self.last_faces = [] # Store last known faces for smooth rendering | |
| def event_queue(self): | |
| if self._event_queue is None: | |
| # Lazily initialize queue so it binds to the correct uvicorn event loop | |
| self._event_queue = asyncio.Queue() | |
| return self._event_queue | |
| def start(self, subject_id=None): | |
| if self.is_running: | |
| return | |
| self.active_subject_id = subject_id | |
| # Capture the main event loop to safely put items in the queue from the thread | |
| try: | |
| self.main_loop = asyncio.get_running_loop() | |
| except RuntimeError: | |
| self.main_loop = asyncio.get_event_loop() | |
| # Open default webcam | |
| self.cap = cv2.VideoCapture(0) | |
| self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) | |
| self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) | |
| if not self.cap.isOpened(): | |
| logger.error("Failed to open webcam.") | |
| return False | |
| self.is_running = True | |
| self.thread = threading.Thread(target=self._capture_loop, daemon=True) | |
| self.thread.start() | |
| logger.info(f"Camera started for subject {subject_id}") | |
| return True | |
| def stop(self): | |
| self.is_running = False | |
| if self.thread: | |
| self.thread.join(timeout=2.0) | |
| if self.cap: | |
| self.cap.release() | |
| logger.info("Camera stopped") | |
| def _capture_loop(self): | |
| while self.is_running: | |
| try: | |
| ret, frame = self.cap.read() | |
| if not ret: | |
| continue | |
| self.frame_count += 1 | |
| if self.frame_count % self.process_every_n == 0: | |
| # Run detection and recognition | |
| detected_faces = face_engine.detect_faces(frame) | |
| self.last_faces = [] # Reset last faces | |
| for face in detected_faces: | |
| # Check liveness | |
| if face_engine.check_liveness(face['crop']): | |
| embedding = face_engine.get_embedding(face['crop']) | |
| try: | |
| roll_no, confidence = face_engine.identify(embedding) | |
| except Exception as e: | |
| logger.error(f"Identify error: {e}") | |
| roll_no, confidence = "unknown", 0.0 | |
| if roll_no != "unknown": | |
| # Queue identified event | |
| event = { | |
| "type": "detected", | |
| "roll_no": roll_no, | |
| "name": "Student", # Can be enriched by DB lookup | |
| "confidence": confidence, | |
| "subject_id": self.active_subject_id | |
| } | |
| # Send event to queue using threadsafe method | |
| self.main_loop.call_soon_threadsafe(self.event_queue.put_nowait, event) | |
| self.last_faces.append({ | |
| "bbox": face['bbox'], | |
| "label": f"{roll_no} ({int(confidence*100)}%)", | |
| "color": (0, 255, 0) | |
| }) | |
| else: | |
| event = { | |
| "type": "unknown", | |
| "confidence": confidence | |
| } | |
| self.main_loop.call_soon_threadsafe(self.event_queue.put_nowait, event) | |
| self.last_faces.append({ | |
| "bbox": face['bbox'], | |
| "label": "Unknown", | |
| "color": (0, 0, 255) | |
| }) | |
| else: | |
| # Failed liveness check | |
| self.last_faces.append({ | |
| "bbox": face['bbox'], | |
| "label": "Spoof Detected", | |
| "color": (0, 165, 255) | |
| }) | |
| # Draw last known faces | |
| for f in self.last_faces: | |
| x1, y1, x2, y2 = f['bbox'] | |
| cv2.rectangle(frame, (x1, y1), (x2, y2), f['color'], 2) | |
| cv2.putText(frame, f['label'], (x1, max(0, y1 - 10)), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.6, f['color'], 2) | |
| # Encode frame to JPEG | |
| _, buffer = cv2.imencode('.jpg', frame) | |
| b64_frame = base64.b64encode(buffer).decode('utf-8') | |
| # Send frame event | |
| frame_event = { | |
| "type": "frame", | |
| "data": b64_frame | |
| } | |
| self.main_loop.call_soon_threadsafe(self.event_queue.put_nowait, frame_event) | |
| except Exception as e: | |
| logger.error(f"Error in capture loop: {e}") | |
| import time | |
| time.sleep(0.5) # Prevent spamming logs if failing continuously | |
| # Cleanup handled in stop() | |
| camera_streamer = CameraStreamer() | |