Spaces:
Sleeping
Sleeping
| """ | |
| Real-time webcam / video file FER inference. | |
| Usage: | |
| python predict_video.py --source 0 # webcam | |
| python predict_video.py --source video.mp4 # video file | |
| python predict_video.py --source 0 --save-output out.mp4 | |
| """ | |
| import argparse | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from inference import FERPredictor | |
| from utils import EMOTION_BGR | |
| DETECT_EVERY_N = 3 # run face detection every N frames; reuse bbox in between | |
| FACE_INPUT_SCALE = 0.5 # downsample for face detection speed, upsample bbox back | |
| def _scale_bboxes(bboxes, scale): | |
| """Scale (x,y,w,h) bboxes back from a downsampled frame.""" | |
| return [(int(x / scale), int(y / scale), int(w / scale), int(h / scale)) | |
| for (x, y, w, h) in bboxes] | |
| def run(source, weights, device, save_path, face_method, no_detect): | |
| predictor = FERPredictor(weights_path=weights, device=device) | |
| # Open source | |
| cap_src = 0 if source == '0' else source | |
| cap = cv2.VideoCapture(cap_src) | |
| if not cap.isOpened(): | |
| print(f"[ERROR] Cannot open source: {source}") | |
| sys.exit(1) | |
| width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| src_fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 | |
| writer = None | |
| if save_path: | |
| fourcc = cv2.VideoWriter_fourcc(*'mp4v') | |
| writer = cv2.VideoWriter(save_path, fourcc, src_fps, (width, height)) | |
| print(f"[INFO] Saving output to: {save_path}") | |
| print("[INFO] Press Q to quit.") | |
| # State kept between frames | |
| cached_face_results: list[dict] = [] | |
| frame_idx = 0 | |
| fps_counter = 0 | |
| fps_display = 0.0 | |
| t_last = time.time() | |
| while True: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| run_detect = (frame_idx % DETECT_EVERY_N == 0) | |
| if no_detect: | |
| # Single-crop inference on the whole frame | |
| if run_detect or not cached_face_results: | |
| rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) | |
| from PIL import Image as PILImage | |
| pil_frame = PILImage.fromarray(rgb_frame) | |
| result = predictor.predict_image(pil_frame) | |
| result['bbox'] = None | |
| cached_face_results = [result] | |
| else: | |
| if run_detect: | |
| # Downsample before detection | |
| small = cv2.resize(frame, (0, 0), fx=FACE_INPUT_SCALE, fy=FACE_INPUT_SCALE) | |
| from detect_face import detect_and_crop_faces | |
| from PIL import Image as PILImage | |
| import cv2 as _cv2 | |
| pil_small = PILImage.fromarray(_cv2.cvtColor(small, _cv2.COLOR_BGR2RGB)) | |
| faces = detect_and_crop_faces( | |
| pil_small, | |
| method=face_method, | |
| margin=20, | |
| device='cuda' if predictor.device.type == 'cuda' else 'cpu' | |
| ) | |
| new_results = [] | |
| for idx, (crop, bbox) in enumerate(faces): | |
| pred = predictor.predict_image(crop) | |
| # Scale bbox back to original resolution | |
| if bbox is not None: | |
| x, y, w, h = bbox | |
| bbox = ( | |
| int(x / FACE_INPUT_SCALE), | |
| int(y / FACE_INPUT_SCALE), | |
| int(w / FACE_INPUT_SCALE), | |
| int(h / FACE_INPUT_SCALE), | |
| ) | |
| pred['bbox'] = bbox | |
| pred['face_index'] = idx | |
| new_results.append(pred) | |
| cached_face_results = new_results if new_results else cached_face_results | |
| # Draw overlays | |
| for res in cached_face_results: | |
| bbox = res.get('bbox') | |
| emotion = res['emotion'] | |
| conf = res['confidence'] | |
| color = EMOTION_BGR.get(emotion, (200, 200, 200)) | |
| if bbox is not None: | |
| x, y, w, h = bbox | |
| cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2) | |
| label = f"{emotion} {conf*100:.0f}%" | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| scale_f, thick = 0.65, 2 | |
| (tw, th), bl = cv2.getTextSize(label, font, scale_f, thick) | |
| ty = max(y - 6, th + 6) | |
| cv2.rectangle(frame, (x, ty - th - 4), (x + tw + 4, ty + bl), color, cv2.FILLED) | |
| cv2.putText(frame, label, (x + 2, ty - 2), font, scale_f, | |
| (255, 255, 255), thick, cv2.LINE_AA) | |
| else: | |
| # No bbox — overlay emotion in top-center | |
| label = f"{emotion} {conf*100:.0f}%" | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| cv2.putText(frame, label, (width // 2 - 80, 40), font, 0.9, | |
| EMOTION_BGR.get(emotion, (200, 200, 200)), 2, cv2.LINE_AA) | |
| # FPS counter | |
| fps_counter += 1 | |
| if fps_counter >= 10: | |
| t_now = time.time() | |
| fps_display = fps_counter / (t_now - t_last) | |
| t_last = t_now | |
| fps_counter = 0 | |
| cv2.putText(frame, f"FPS: {fps_display:.1f}", (10, 28), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, cv2.LINE_AA) | |
| cv2.imshow('FER Inference — Q to quit', frame) | |
| if writer: | |
| writer.write(frame) | |
| if cv2.waitKey(1) & 0xFF in (ord('q'), ord('Q'), 27): | |
| break | |
| frame_idx += 1 | |
| cap.release() | |
| if writer: | |
| writer.release() | |
| cv2.destroyAllWindows() | |
| print("[INFO] Done.") | |
| def main(): | |
| parser = argparse.ArgumentParser(description='Real-time FER on webcam or video.') | |
| parser.add_argument('--source', default='0', | |
| help='Webcam index (0) or path to video file.') | |
| parser.add_argument('--weights', default='../models/model_weights.pth', | |
| help='Path to model_weights.pth.') | |
| parser.add_argument('--device', default='auto', | |
| help='Device: auto | cpu | cuda.') | |
| parser.add_argument('--save-output', default=None, | |
| help='Save annotated video to this path.') | |
| parser.add_argument('--face-method', default='mtcnn', choices=['mtcnn', 'haar'], | |
| help='Face detection backend (default: mtcnn).') | |
| parser.add_argument('--no-detect', action='store_true', | |
| help='Skip face detection — infer on full frame.') | |
| args = parser.parse_args() | |
| run( | |
| source=args.source, | |
| weights=args.weights, | |
| device=args.device, | |
| save_path=args.save_output, | |
| face_method=args.face_method, | |
| no_detect=args.no_detect, | |
| ) | |
| if __name__ == '__main__': | |
| main() | |