| """ |
| Smart Frame Selection to Avoid Closed Eyes |
| Uses multiple techniques to select best frames |
| """ |
|
|
| import cv2 |
| import numpy as np |
| import os |
| from typing import List, Tuple |
| import shutil |
|
|
| class SimpleEyeDetector: |
| """Simple but effective eye detection without heavy dependencies""" |
| |
| def __init__(self): |
| |
| self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') |
| self.eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml') |
| |
| def detect_blink_score(self, image_path: str) -> float: |
| """ |
| Calculate blink score (0-100) |
| Higher score = eyes more likely open |
| """ |
| img = cv2.imread(image_path) |
| if img is None: |
| return 50.0 |
| |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| |
| |
| faces = self.face_cascade.detectMultiScale(gray, 1.3, 5) |
| |
| if len(faces) == 0: |
| return 50.0 |
| |
| total_score = 0.0 |
| face_count = 0 |
| |
| for (x, y, w, h) in faces: |
| |
| face_roi = gray[y:y+h, x:x+w] |
| |
| |
| eye_region = face_roi[int(h*0.2):int(h*0.5), :] |
| |
| |
| eyes = self.eye_cascade.detectMultiScale(eye_region, 1.1, 3) |
| eye_score = 0.0 |
| |
| if len(eyes) >= 2: |
| eye_score += 40.0 |
| elif len(eyes) == 1: |
| eye_score += 20.0 |
| |
| |
| |
| eye_std = np.std(eye_region) |
| if eye_std > 20: |
| eye_score += 30.0 |
| elif eye_std > 10: |
| eye_score += 15.0 |
| |
| |
| |
| edges = cv2.Canny(eye_region, 30, 100) |
| edge_density = np.sum(edges > 0) / edges.size |
| if edge_density > 0.1: |
| eye_score += 30.0 |
| elif edge_density > 0.05: |
| eye_score += 15.0 |
| |
| total_score += eye_score |
| face_count += 1 |
| |
| return total_score / face_count if face_count > 0 else 50.0 |
| |
| def is_blurry(self, image_path: str) -> bool: |
| """Check if image is blurry using Laplacian variance""" |
| img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
| if img is None: |
| return True |
| |
| laplacian = cv2.Laplacian(img, cv2.CV_64F) |
| variance = laplacian.var() |
| |
| return variance < 100 |
|
|
| class FrameQualityAnalyzer: |
| """Analyze overall frame quality""" |
| |
| def __init__(self): |
| self.eye_detector = SimpleEyeDetector() |
| |
| def analyze_frame(self, image_path: str) -> dict: |
| """Comprehensive frame analysis""" |
| img = cv2.imread(image_path) |
| if img is None: |
| return {'total_score': 0, 'usable': False} |
| |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| |
| |
| scores = { |
| 'eye_score': 0, |
| 'sharpness_score': 0, |
| 'brightness_score': 0, |
| 'face_score': 0, |
| 'total_score': 0, |
| 'usable': True |
| } |
| |
| |
| scores['eye_score'] = self.eye_detector.detect_blink_score(image_path) |
| |
| |
| if not self.eye_detector.is_blurry(image_path): |
| scores['sharpness_score'] = 100 |
| else: |
| scores['sharpness_score'] = 30 |
| |
| |
| brightness = np.mean(gray) |
| if 60 < brightness < 200: |
| scores['brightness_score'] = 100 |
| elif 40 < brightness < 220: |
| scores['brightness_score'] = 60 |
| else: |
| scores['brightness_score'] = 20 |
| |
| |
| face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') |
| faces = face_cascade.detectMultiScale(gray, 1.3, 5) |
| if len(faces) > 0: |
| scores['face_score'] = 100 |
| else: |
| scores['face_score'] = 0 |
| |
| |
| scores['total_score'] = ( |
| scores['eye_score'] * 0.4 + |
| scores['sharpness_score'] * 0.2 + |
| scores['brightness_score'] * 0.2 + |
| scores['face_score'] * 0.2 |
| ) |
| |
| |
| scores['usable'] = scores['total_score'] > 30 |
| |
| return scores |
|
|
| def select_best_frames_avoid_blinks( |
| input_dir: str = 'frames', |
| output_dir: str = 'frames/final', |
| num_frames: int = 16, |
| extract_extra: bool = True |
| ): |
| """ |
| Select best frames avoiding blinks and closed eyes |
| |
| Args: |
| input_dir: Directory with extracted frames |
| output_dir: Directory for selected frames |
| num_frames: Number of frames to select |
| extract_extra: If True, extract 3x frames first for better selection |
| """ |
| print("👁️ Smart frame selection to avoid closed eyes...") |
| |
| |
| frame_files = sorted([f for f in os.listdir(input_dir) |
| if f.endswith(('.png', '.jpg', '.jpeg'))]) |
| |
| if len(frame_files) < num_frames: |
| print(f"⚠️ Only {len(frame_files)} frames available, need {num_frames}") |
| return |
| |
| |
| analyzer = FrameQualityAnalyzer() |
| frame_analysis = [] |
| |
| print(f"🔍 Analyzing {len(frame_files)} frames...") |
| |
| for i, frame_file in enumerate(frame_files): |
| frame_path = os.path.join(input_dir, frame_file) |
| analysis = analyzer.analyze_frame(frame_path) |
| |
| frame_analysis.append({ |
| 'path': frame_path, |
| 'filename': frame_file, |
| 'index': i, |
| **analysis |
| }) |
| |
| |
| if (i + 1) % 10 == 0: |
| print(f" Analyzed {i + 1}/{len(frame_files)} frames...") |
| |
| |
| frame_analysis.sort(key=lambda x: x['total_score'], reverse=True) |
| |
| |
| selected_frames = [] |
| selected_indices = set() |
| min_frame_distance = max(1, len(frame_files) // (num_frames * 2)) |
| |
| |
| for frame in frame_analysis: |
| if len(selected_frames) >= num_frames: |
| break |
| |
| if not frame['usable']: |
| continue |
| |
| |
| too_close = any( |
| abs(frame['index'] - idx) < min_frame_distance |
| for idx in selected_indices |
| ) |
| |
| if not too_close: |
| selected_frames.append(frame) |
| selected_indices.add(frame['index']) |
| |
| |
| print(f" Selected frame {frame['filename']}: " |
| f"Score={frame['total_score']:.1f}, " |
| f"Eyes={frame['eye_score']:.1f}") |
| |
| |
| if len(selected_frames) < num_frames: |
| print(f"⚠️ Only found {len(selected_frames)} good frames, adding more...") |
| |
| for frame in frame_analysis: |
| if frame not in selected_frames and frame['usable']: |
| selected_frames.append(frame) |
| if len(selected_frames) >= num_frames: |
| break |
| |
| |
| if len(selected_frames) < num_frames: |
| for frame in frame_analysis: |
| if frame not in selected_frames: |
| selected_frames.append(frame) |
| if len(selected_frames) >= num_frames: |
| break |
| |
| |
| selected_frames.sort(key=lambda x: x['index']) |
| |
| |
| os.makedirs(output_dir, exist_ok=True) |
| |
| |
| for i, frame in enumerate(selected_frames[:num_frames]): |
| src_path = frame['path'] |
| dst_filename = f'frame{i:03d}.png' |
| dst_path = os.path.join(output_dir, dst_filename) |
| |
| shutil.copy2(src_path, dst_path) |
| |
| print(f" ✅ {frame['filename']} → {dst_filename} " |
| f"(Score: {frame['total_score']:.1f}, Eyes: {frame['eye_score']:.1f})") |
| |
| print(f"\n✅ Selected {len(selected_frames[:num_frames])} best frames") |
| print(f"📊 Average eye score: {np.mean([f['eye_score'] for f in selected_frames[:num_frames]]):.1f}/100") |
|
|
| |
| def ensure_open_eyes_in_frames(frames_dir: str = 'frames/final'): |
| """ |
| Post-process existing frames to check for closed eyes |
| Replace bad frames with better alternatives |
| """ |
| analyzer = FrameQualityAnalyzer() |
| |
| frame_files = sorted([f for f in os.listdir(frames_dir) |
| if f.endswith(('.png', '.jpg'))]) |
| |
| print(f"\n👁️ Checking {len(frame_files)} frames for closed eyes...") |
| |
| for frame_file in frame_files: |
| frame_path = os.path.join(frames_dir, frame_file) |
| analysis = analyzer.analyze_frame(frame_path) |
| |
| if analysis['eye_score'] < 40: |
| print(f" ⚠️ {frame_file}: Low eye score ({analysis['eye_score']:.1f})") |
| |
| |
|
|
| if __name__ == "__main__": |
| |
| if os.path.exists('frames'): |
| select_best_frames_avoid_blinks('frames', 'frames/final_no_blinks', 16) |