#!/opt/homebrew/bin/python3.11 """ Face Processor - Face Detection & Demographics Uses InsightFace for detection, age, and gender analysis. Falls back to OpenCV Haar Cascade if InsightFace fails. """ import sys import json import argparse import os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from redis_publisher import RedisPublisher def process_face(video_path: str, output_path: str, uuid: str = ""): """Process video for face detection and demographics analysis""" publisher = RedisPublisher(uuid) if uuid else None if publisher: publisher.info("face", "FACE_START") try: import cv2 import numpy as np import insightface except ImportError as e: error_msg = f"Missing dependency: {e.name}" if publisher: publisher.error("face", error_msg) result = {"frame_count": 0, "fps": 0.0, "frames": []} with open(output_path, "w") as f: json.dump(result, f, indent=2) return result # 1. Initialize InsightFace use_insightface = False app = None try: if publisher: publisher.info("face", "LOADING_INSIGHTFACE") # 'buffalo_l' is a robust model. det_size can be adjusted. app = insightface.app.FaceAnalysis( name="buffalo_l", providers=["CPUExecutionProvider"] ) app.prepare(ctx_id=0, det_size=(320, 320)) use_insightface = True if publisher: publisher.info("face", "INSIGHTFACE_LOADED") except Exception as e: print(f"[WARNING] InsightFace failed to load: {e}") use_insightface = False # 2. Fallback to Haar Cascade face_cascade = None if not use_insightface: if publisher: publisher.info("face", "LOADING_HAAR_CASCADE") face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) if face_cascade.empty(): if publisher: publisher.error("face", "Could not load Haar Cascade") result = {"frame_count": 0, "fps": 0.0, "frames": []} with open(output_path, "w") as f: json.dump(result, f, indent=2) return result if publisher: publisher.info("face", "HAAR_CASCADE_LOADED") if publisher: publisher.info("face", "PROCESSING_VIDEO") cap = cv2.VideoCapture(video_path) if not cap.isOpened(): if publisher: publisher.error("face", "Could not open video") result = {"frame_count": 0, "fps": 0.0, "frames": []} with open(output_path, "w") as f: json.dump(result, f, indent=2) return result fps = cap.get(cv2.CAP_PROP_FPS) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # Optimization: Process every N frames to speed up analysis # Since we just need attributes for the person identity, we don't need every single frame. sample_interval = 30 if total_frames > 0: estimated_samples = total_frames // sample_interval else: estimated_samples = 0 frame_count = 0 processed_count = 0 frames_data = [] if publisher: publisher.progress("face", 0, estimated_samples, "Starting") while True: ret, frame = cap.read() if not ret: break frame_count += 1 # Sampling if frame_count % sample_interval != 0: continue processed_count += 1 timestamp = (frame_count - 1) / fps if fps > 0 else 0 face_list = [] try: if use_insightface and app: # InsightFace Detection & Analysis faces = app.get(frame) for face in faces: bbox = face.bbox.astype(int) bx, by, bw, bh = ( bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1], ) # Extract Attributes age = int(face.age) if hasattr(face, "age") else None gender_val = face.gender if hasattr(face, "gender") else None gender = ( "female" if gender_val == 0 else ("male" if gender_val == 1 else None) ) face_list.append( { "x": int(bx), "y": int(by), "width": int(bw), "height": int(bh), "confidence": float(face.det_score) if hasattr(face, "det_score") else 0.9, "attributes": {"age": age, "gender": gender}, } ) else: # Haar Cascade Fallback (No Age/Gender) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30) ) for x, y, w, h in faces: face_list.append( { "x": int(x), "y": int(y), "width": int(w), "height": int(h), "confidence": 0.8, "attributes": {"age": None, "gender": None}, } ) except Exception as e: print(f"[ERROR] Frame processing error: {e}") if face_list: frames_data.append( { "frame": frame_count - 1, "timestamp": round(timestamp, 3), "faces": face_list, } ) if publisher: publisher.progress( "face", processed_count, estimated_samples, f"Frame {frame_count}", ) cap.release() result = {"frame_count": total_frames, "fps": fps, "frames": frames_data} if publisher: publisher.complete("face", f"{len(frames_data)} frames processed") with open(output_path, "w") as f: json.dump(result, f, indent=2) return result if __name__ == "__main__": parser = argparse.ArgumentParser(description="Face Detection & Demographics") parser.add_argument("video_path", help="Path to video file") parser.add_argument("output_path", help="Output JSON path") parser.add_argument("--uuid", "-u", help="UUID for Redis progress", default="") args = parser.parse_args() process_face(args.video_path, args.output_path, args.uuid)