| | """ |
| | 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) |